597 lines
16 KiB
Rust
597 lines
16 KiB
Rust
//! Message Queue Configuration
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::time::Duration;
|
|
|
|
use super::{ExchangeType, MqError, MqResult};
|
|
|
|
/// Message queue configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MessageQueueConfig {
|
|
/// Whether message queue is enabled
|
|
#[serde(default = "default_enabled")]
|
|
pub enabled: bool,
|
|
|
|
/// Message queue type (rabbitmq or redis)
|
|
#[serde(default = "default_type")]
|
|
pub r#type: String,
|
|
|
|
/// RabbitMQ configuration
|
|
#[serde(default)]
|
|
pub rabbitmq: RabbitMqConfig,
|
|
}
|
|
|
|
impl Default for MessageQueueConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
r#type: "rabbitmq".to_string(),
|
|
rabbitmq: RabbitMqConfig::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// RabbitMQ configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RabbitMqConfig {
|
|
/// RabbitMQ host
|
|
#[serde(default = "default_host")]
|
|
pub host: String,
|
|
|
|
/// RabbitMQ port
|
|
#[serde(default = "default_port")]
|
|
pub port: u16,
|
|
|
|
/// RabbitMQ username
|
|
#[serde(default = "default_username")]
|
|
pub username: String,
|
|
|
|
/// RabbitMQ password
|
|
#[serde(default = "default_password")]
|
|
pub password: String,
|
|
|
|
/// RabbitMQ virtual host
|
|
#[serde(default = "default_vhost")]
|
|
pub vhost: String,
|
|
|
|
/// Connection pool size
|
|
#[serde(default = "default_pool_size")]
|
|
pub pool_size: usize,
|
|
|
|
/// Connection timeout in seconds
|
|
#[serde(default = "default_connection_timeout")]
|
|
pub connection_timeout_secs: u64,
|
|
|
|
/// Heartbeat interval in seconds
|
|
#[serde(default = "default_heartbeat")]
|
|
pub heartbeat_secs: u64,
|
|
|
|
/// Reconnection delay in seconds
|
|
#[serde(default = "default_reconnect_delay")]
|
|
pub reconnect_delay_secs: u64,
|
|
|
|
/// Maximum reconnection attempts (0 = infinite)
|
|
#[serde(default = "default_max_reconnect_attempts")]
|
|
pub max_reconnect_attempts: u32,
|
|
|
|
/// Confirm publish (wait for broker confirmation)
|
|
#[serde(default = "default_confirm_publish")]
|
|
pub confirm_publish: bool,
|
|
|
|
/// Publish timeout in seconds
|
|
#[serde(default = "default_publish_timeout")]
|
|
pub publish_timeout_secs: u64,
|
|
|
|
/// Consumer prefetch count
|
|
#[serde(default = "default_prefetch_count")]
|
|
pub prefetch_count: u16,
|
|
|
|
/// Consumer timeout in seconds
|
|
#[serde(default = "default_consumer_timeout")]
|
|
pub consumer_timeout_secs: u64,
|
|
|
|
/// Queue configurations
|
|
#[serde(default)]
|
|
pub queues: QueuesConfig,
|
|
|
|
/// Exchange configurations
|
|
#[serde(default)]
|
|
pub exchanges: ExchangesConfig,
|
|
|
|
/// Dead letter queue configuration
|
|
#[serde(default)]
|
|
pub dead_letter: DeadLetterConfig,
|
|
|
|
/// Worker queue message TTL in milliseconds (default 5 minutes)
|
|
#[serde(default = "default_worker_queue_ttl")]
|
|
pub worker_queue_ttl_ms: u64,
|
|
}
|
|
|
|
impl Default for RabbitMqConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
host: default_host(),
|
|
port: default_port(),
|
|
username: default_username(),
|
|
password: default_password(),
|
|
vhost: default_vhost(),
|
|
pool_size: default_pool_size(),
|
|
connection_timeout_secs: default_connection_timeout(),
|
|
heartbeat_secs: default_heartbeat(),
|
|
reconnect_delay_secs: default_reconnect_delay(),
|
|
max_reconnect_attempts: default_max_reconnect_attempts(),
|
|
confirm_publish: default_confirm_publish(),
|
|
publish_timeout_secs: default_publish_timeout(),
|
|
prefetch_count: default_prefetch_count(),
|
|
consumer_timeout_secs: default_consumer_timeout(),
|
|
queues: QueuesConfig::default(),
|
|
exchanges: ExchangesConfig::default(),
|
|
dead_letter: DeadLetterConfig::default(),
|
|
worker_queue_ttl_ms: default_worker_queue_ttl(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RabbitMqConfig {
|
|
/// Get connection URL
|
|
pub fn connection_url(&self) -> String {
|
|
format!(
|
|
"amqp://{}:{}@{}:{}/{}",
|
|
self.username, self.password, self.host, self.port, self.vhost
|
|
)
|
|
}
|
|
|
|
/// Get connection timeout as Duration
|
|
pub fn connection_timeout(&self) -> Duration {
|
|
Duration::from_secs(self.connection_timeout_secs)
|
|
}
|
|
|
|
/// Get heartbeat as Duration
|
|
pub fn heartbeat(&self) -> Duration {
|
|
Duration::from_secs(self.heartbeat_secs)
|
|
}
|
|
|
|
/// Get reconnect delay as Duration
|
|
pub fn reconnect_delay(&self) -> Duration {
|
|
Duration::from_secs(self.reconnect_delay_secs)
|
|
}
|
|
|
|
/// Get publish timeout as Duration
|
|
pub fn publish_timeout(&self) -> Duration {
|
|
Duration::from_secs(self.publish_timeout_secs)
|
|
}
|
|
|
|
/// Get consumer timeout as Duration
|
|
pub fn consumer_timeout(&self) -> Duration {
|
|
Duration::from_secs(self.consumer_timeout_secs)
|
|
}
|
|
|
|
/// Get worker queue TTL as Duration
|
|
pub fn worker_queue_ttl(&self) -> Duration {
|
|
Duration::from_millis(self.worker_queue_ttl_ms)
|
|
}
|
|
|
|
/// Validate configuration
|
|
pub fn validate(&self) -> MqResult<()> {
|
|
if self.host.is_empty() {
|
|
return Err(MqError::Config("Host cannot be empty".to_string()));
|
|
}
|
|
if self.username.is_empty() {
|
|
return Err(MqError::Config("Username cannot be empty".to_string()));
|
|
}
|
|
if self.pool_size == 0 {
|
|
return Err(MqError::Config(
|
|
"Pool size must be greater than 0".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Queue configurations
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct QueuesConfig {
|
|
/// Events queue configuration
|
|
pub events: QueueConfig,
|
|
|
|
/// Executions queue configuration (legacy - to be deprecated)
|
|
pub executions: QueueConfig,
|
|
|
|
/// Enforcement created queue configuration
|
|
pub enforcements: QueueConfig,
|
|
|
|
/// Execution requests queue configuration
|
|
pub execution_requests: QueueConfig,
|
|
|
|
/// Execution status updates queue configuration
|
|
pub execution_status: QueueConfig,
|
|
|
|
/// Execution completed queue configuration
|
|
pub execution_completed: QueueConfig,
|
|
|
|
/// Inquiry responses queue configuration
|
|
pub inquiry_responses: QueueConfig,
|
|
|
|
/// Notifications queue configuration
|
|
pub notifications: QueueConfig,
|
|
}
|
|
|
|
impl Default for QueuesConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
events: QueueConfig {
|
|
name: "attune.events.queue".to_string(),
|
|
durable: true,
|
|
exclusive: false,
|
|
auto_delete: false,
|
|
},
|
|
executions: QueueConfig {
|
|
name: "attune.executions.queue".to_string(),
|
|
durable: true,
|
|
exclusive: false,
|
|
auto_delete: false,
|
|
},
|
|
enforcements: QueueConfig {
|
|
name: "attune.enforcements.queue".to_string(),
|
|
durable: true,
|
|
exclusive: false,
|
|
auto_delete: false,
|
|
},
|
|
execution_requests: QueueConfig {
|
|
name: "attune.execution.requests.queue".to_string(),
|
|
durable: true,
|
|
exclusive: false,
|
|
auto_delete: false,
|
|
},
|
|
execution_status: QueueConfig {
|
|
name: "attune.execution.status.queue".to_string(),
|
|
durable: true,
|
|
exclusive: false,
|
|
auto_delete: false,
|
|
},
|
|
execution_completed: QueueConfig {
|
|
name: "attune.execution.completed.queue".to_string(),
|
|
durable: true,
|
|
exclusive: false,
|
|
auto_delete: false,
|
|
},
|
|
inquiry_responses: QueueConfig {
|
|
name: "attune.inquiry.responses.queue".to_string(),
|
|
durable: true,
|
|
exclusive: false,
|
|
auto_delete: false,
|
|
},
|
|
notifications: QueueConfig {
|
|
name: "attune.notifications.queue".to_string(),
|
|
durable: true,
|
|
exclusive: false,
|
|
auto_delete: false,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Queue configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct QueueConfig {
|
|
/// Queue name
|
|
pub name: String,
|
|
|
|
/// Durable (survives broker restart)
|
|
#[serde(default = "default_true")]
|
|
pub durable: bool,
|
|
|
|
/// Exclusive (only accessible by this connection)
|
|
#[serde(default)]
|
|
pub exclusive: bool,
|
|
|
|
/// Auto-delete (deleted when last consumer disconnects)
|
|
#[serde(default)]
|
|
pub auto_delete: bool,
|
|
}
|
|
|
|
/// Exchange configurations
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExchangesConfig {
|
|
/// Events exchange configuration
|
|
pub events: ExchangeConfig,
|
|
|
|
/// Executions exchange configuration
|
|
pub executions: ExchangeConfig,
|
|
|
|
/// Notifications exchange configuration
|
|
pub notifications: ExchangeConfig,
|
|
}
|
|
|
|
impl Default for ExchangesConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
events: ExchangeConfig {
|
|
name: "attune.events".to_string(),
|
|
r#type: ExchangeType::Topic,
|
|
durable: true,
|
|
auto_delete: false,
|
|
},
|
|
executions: ExchangeConfig {
|
|
name: "attune.executions".to_string(),
|
|
r#type: ExchangeType::Topic,
|
|
durable: true,
|
|
auto_delete: false,
|
|
},
|
|
notifications: ExchangeConfig {
|
|
name: "attune.notifications".to_string(),
|
|
r#type: ExchangeType::Fanout,
|
|
durable: true,
|
|
auto_delete: false,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Exchange configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExchangeConfig {
|
|
/// Exchange name
|
|
pub name: String,
|
|
|
|
/// Exchange type
|
|
pub r#type: ExchangeType,
|
|
|
|
/// Durable (survives broker restart)
|
|
#[serde(default = "default_true")]
|
|
pub durable: bool,
|
|
|
|
/// Auto-delete (deleted when last queue unbinds)
|
|
#[serde(default)]
|
|
pub auto_delete: bool,
|
|
}
|
|
|
|
/// Dead letter queue configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DeadLetterConfig {
|
|
/// Enable dead letter queues
|
|
#[serde(default = "default_enabled")]
|
|
pub enabled: bool,
|
|
|
|
/// Dead letter exchange name
|
|
#[serde(default = "default_dlx_exchange")]
|
|
pub exchange: String,
|
|
|
|
/// Message TTL in dead letter queue (milliseconds)
|
|
#[serde(default = "default_dlq_ttl")]
|
|
pub ttl_ms: u64,
|
|
}
|
|
|
|
impl Default for DeadLetterConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
exchange: "attune.dlx".to_string(),
|
|
ttl_ms: 86400000, // 24 hours
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DeadLetterConfig {
|
|
/// Get TTL as Duration
|
|
pub fn ttl(&self) -> Duration {
|
|
Duration::from_millis(self.ttl_ms)
|
|
}
|
|
}
|
|
|
|
/// Publisher configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PublisherConfig {
|
|
/// Confirm publish (wait for broker confirmation)
|
|
#[serde(default = "default_confirm_publish")]
|
|
pub confirm_publish: bool,
|
|
|
|
/// Publish timeout in seconds
|
|
#[serde(default = "default_publish_timeout")]
|
|
pub timeout_secs: u64,
|
|
|
|
/// Default exchange name
|
|
pub exchange: String,
|
|
}
|
|
|
|
impl PublisherConfig {
|
|
/// Get timeout as Duration
|
|
pub fn timeout(&self) -> Duration {
|
|
Duration::from_secs(self.timeout_secs)
|
|
}
|
|
}
|
|
|
|
/// Consumer configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ConsumerConfig {
|
|
/// Queue name to consume from
|
|
pub queue: String,
|
|
|
|
/// Consumer tag (identifier)
|
|
pub tag: String,
|
|
|
|
/// Prefetch count (number of unacknowledged messages)
|
|
#[serde(default = "default_prefetch_count")]
|
|
pub prefetch_count: u16,
|
|
|
|
/// Auto-acknowledge messages
|
|
#[serde(default)]
|
|
pub auto_ack: bool,
|
|
|
|
/// Exclusive consumer
|
|
#[serde(default)]
|
|
pub exclusive: bool,
|
|
}
|
|
|
|
// Default value functions
|
|
|
|
fn default_enabled() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_type() -> String {
|
|
"rabbitmq".to_string()
|
|
}
|
|
|
|
fn default_host() -> String {
|
|
"localhost".to_string()
|
|
}
|
|
|
|
fn default_port() -> u16 {
|
|
5672
|
|
}
|
|
|
|
fn default_username() -> String {
|
|
"guest".to_string()
|
|
}
|
|
|
|
fn default_password() -> String {
|
|
"guest".to_string()
|
|
}
|
|
|
|
fn default_vhost() -> String {
|
|
"/".to_string()
|
|
}
|
|
|
|
fn default_pool_size() -> usize {
|
|
10
|
|
}
|
|
|
|
fn default_connection_timeout() -> u64 {
|
|
30
|
|
}
|
|
|
|
fn default_heartbeat() -> u64 {
|
|
60
|
|
}
|
|
|
|
fn default_reconnect_delay() -> u64 {
|
|
5
|
|
}
|
|
|
|
fn default_max_reconnect_attempts() -> u32 {
|
|
10
|
|
}
|
|
|
|
fn default_confirm_publish() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_publish_timeout() -> u64 {
|
|
5
|
|
}
|
|
|
|
fn default_prefetch_count() -> u16 {
|
|
10
|
|
}
|
|
|
|
fn default_consumer_timeout() -> u64 {
|
|
300
|
|
}
|
|
|
|
fn default_dlx_exchange() -> String {
|
|
"attune.dlx".to_string()
|
|
}
|
|
|
|
fn default_dlq_ttl() -> u64 {
|
|
86400000 // 24 hours in milliseconds
|
|
}
|
|
|
|
fn default_worker_queue_ttl() -> u64 {
|
|
300000 // 5 minutes in milliseconds
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_default_config() {
|
|
let config = MessageQueueConfig::default();
|
|
assert!(config.enabled);
|
|
assert_eq!(config.r#type, "rabbitmq");
|
|
assert_eq!(config.rabbitmq.host, "localhost");
|
|
assert_eq!(config.rabbitmq.port, 5672);
|
|
}
|
|
|
|
#[test]
|
|
fn test_connection_url() {
|
|
let config = RabbitMqConfig::default();
|
|
let url = config.connection_url();
|
|
assert!(url.starts_with("amqp://"));
|
|
assert!(url.contains("localhost"));
|
|
assert!(url.contains("5672"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate() {
|
|
let mut config = RabbitMqConfig::default();
|
|
assert!(config.validate().is_ok());
|
|
|
|
config.host = String::new();
|
|
assert!(config.validate().is_err());
|
|
|
|
config.host = "localhost".to_string();
|
|
config.pool_size = 0;
|
|
assert!(config.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_duration_conversions() {
|
|
let config = RabbitMqConfig::default();
|
|
assert_eq!(config.connection_timeout().as_secs(), 30);
|
|
assert_eq!(config.heartbeat().as_secs(), 60);
|
|
assert_eq!(config.reconnect_delay().as_secs(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dead_letter_config() {
|
|
let config = DeadLetterConfig::default();
|
|
assert!(config.enabled);
|
|
assert_eq!(config.exchange, "attune.dlx");
|
|
assert_eq!(config.ttl().as_secs(), 86400); // 24 hours
|
|
}
|
|
|
|
#[test]
|
|
fn test_worker_queue_ttl() {
|
|
let config = RabbitMqConfig::default();
|
|
assert_eq!(config.worker_queue_ttl().as_secs(), 300); // 5 minutes
|
|
assert_eq!(config.worker_queue_ttl_ms, 300000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_queues() {
|
|
let queues = QueuesConfig::default();
|
|
assert_eq!(queues.events.name, "attune.events.queue");
|
|
assert_eq!(queues.executions.name, "attune.executions.queue");
|
|
assert_eq!(
|
|
queues.execution_completed.name,
|
|
"attune.execution.completed.queue"
|
|
);
|
|
assert_eq!(
|
|
queues.inquiry_responses.name,
|
|
"attune.inquiry.responses.queue"
|
|
);
|
|
assert_eq!(queues.notifications.name, "attune.notifications.queue");
|
|
assert!(queues.events.durable);
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_exchanges() {
|
|
let exchanges = ExchangesConfig::default();
|
|
assert_eq!(exchanges.events.name, "attune.events");
|
|
assert_eq!(exchanges.executions.name, "attune.executions");
|
|
assert_eq!(exchanges.notifications.name, "attune.notifications");
|
|
assert!(matches!(exchanges.events.r#type, ExchangeType::Topic));
|
|
assert!(matches!(exchanges.executions.r#type, ExchangeType::Topic));
|
|
assert!(matches!(
|
|
exchanges.notifications.r#type,
|
|
ExchangeType::Fanout
|
|
));
|
|
}
|
|
}
|