rust_task_queue/
config.rs

1//! Configuration management for the task queue framework
2//!
3//! This module provides a comprehensive configuration system that supports:
4//! - Environment variables
5//! - TOML and YAML configuration files
6//! - Global configuration initialization
7//! - Integration with Actix Web
8
9use crate::autoscaler::AutoScalerConfig;
10use crate::TaskQueueError;
11use serde::{Deserialize, Serialize};
12use std::sync::OnceLock;
13
14/// Global configuration instance
15static GLOBAL_CONFIG: OnceLock<TaskQueueConfig> = OnceLock::new();
16
17/// Main configuration structure for the task queue system
18#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19pub struct TaskQueueConfig {
20    /// Redis connection configuration
21    pub redis: RedisConfig,
22
23    /// Worker configuration
24    pub workers: WorkerConfig,
25
26    /// Auto-scaling configuration
27    pub autoscaler: AutoScalerConfig,
28
29    /// Auto-registration settings
30    pub auto_register: AutoRegisterConfig,
31
32    /// Scheduler configuration
33    pub scheduler: SchedulerConfig,
34
35    /// Actix Web integration settings
36    #[cfg(feature = "actix-integration")]
37    pub actix: ActixConfig,
38
39    /// Axum integration settings
40    #[cfg(feature = "axum-integration")]
41    pub axum: AxumConfig,
42}
43
44/// Redis connection configuration
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct RedisConfig {
47    /// Redis connection URL
48    pub url: String,
49
50    /// Connection pool size
51    pub pool_size: Option<u32>,
52
53    /// Connection timeout in seconds
54    pub connection_timeout: Option<u64>,
55
56    /// Command timeout in seconds
57    pub command_timeout: Option<u64>,
58}
59
60/// Worker configuration
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct WorkerConfig {
63    /// Initial number of workers to start
64    pub initial_count: usize,
65
66    /// Maximum number of concurrent tasks per worker
67    pub max_concurrent_tasks: Option<usize>,
68
69    /// Worker heartbeat interval in seconds
70    pub heartbeat_interval: Option<u64>,
71
72    /// Grace period for shutdown in seconds
73    pub shutdown_grace_period: Option<u64>,
74}
75
76/// Auto-registration configuration
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct AutoRegisterConfig {
79    /// Enable automatic task registration
80    pub enabled: bool,
81}
82
83/// Scheduler configuration
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct SchedulerConfig {
86    /// Enable the scheduler
87    pub enabled: bool,
88
89    /// Scheduler tick interval in seconds
90    pub tick_interval: Option<u64>,
91
92    /// Maximum number of scheduled tasks to process per tick
93    pub max_tasks_per_tick: Option<usize>,
94}
95
96/// Actix Web integration configuration
97#[cfg(feature = "actix-integration")]
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ActixConfig {
100    /// Enable automatic route configuration
101    pub auto_configure_routes: bool,
102
103    /// Base path for task queue routes
104    pub route_prefix: String,
105
106    /// Enable metrics endpoint
107    pub enable_metrics: bool,
108
109    /// Enable health check endpoint
110    pub enable_health_check: bool,
111}
112
113/// Axum integration configuration
114#[cfg(feature = "axum-integration")]
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct AxumConfig {
117    /// Enable automatic route configuration
118    pub auto_configure_routes: bool,
119
120    /// Base path for task queue routes
121    pub route_prefix: String,
122
123    /// Enable metrics endpoint
124    pub enable_metrics: bool,
125
126    /// Enable health check endpoint
127    pub enable_health_check: bool
128}
129
130impl Default for RedisConfig {
131    fn default() -> Self {
132        Self {
133            url: std::env::var("REDIS_URL")
134                .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()),
135            pool_size: None,
136            connection_timeout: None,
137            command_timeout: None,
138        }
139    }
140}
141
142impl Default for WorkerConfig {
143    fn default() -> Self {
144        Self {
145            initial_count: std::env::var("INITIAL_WORKER_COUNT")
146                .ok()
147                .and_then(|s| s.parse().ok())
148                .unwrap_or(2),
149            max_concurrent_tasks: None,
150            heartbeat_interval: None,
151            shutdown_grace_period: None,
152        }
153    }
154}
155
156impl Default for AutoRegisterConfig {
157    fn default() -> Self {
158        Self {
159            enabled: std::env::var("AUTO_REGISTER_TASKS")
160                .map(|s| s.to_lowercase() == "true")
161                .unwrap_or(false),
162        }
163    }
164}
165
166impl Default for SchedulerConfig {
167    fn default() -> Self {
168        Self {
169            enabled: std::env::var("ENABLE_SCHEDULER")
170                .map(|s| s.to_lowercase() == "true")
171                .unwrap_or(false),
172            tick_interval: None,
173            max_tasks_per_tick: None,
174        }
175    }
176}
177
178#[cfg(feature = "actix-integration")]
179impl Default for ActixConfig {
180    fn default() -> Self {
181        Self {
182            auto_configure_routes: std::env::var("AUTO_CONFIGURE_ROUTES")
183                .map(|s| s.to_lowercase() == "true")
184                .unwrap_or(true),
185            route_prefix: std::env::var("ROUTE_PREFIX")
186                .unwrap_or_else(|_| "/api/v1/tasks".to_string()),
187            enable_metrics: std::env::var("ENABLE_METRICS")
188                .map(|s| s.to_lowercase() == "true")
189                .unwrap_or(true),
190            enable_health_check: std::env::var("ENABLE_HEALTH_CHECK")
191                .map(|s| s.to_lowercase() == "true")
192                .unwrap_or(true),
193        }
194    }
195}
196
197#[cfg(feature = "axum-integration")]
198impl Default for AxumConfig {
199    fn default() -> Self {
200        Self {
201            auto_configure_routes: std::env::var("AXUM_AUTO_CONFIGURE_ROUTES")
202                .map(|s| s.to_lowercase() == "true")
203                .unwrap_or(true),
204            route_prefix: std::env::var("AXUM_ROUTE_PREFIX")
205                .unwrap_or_else(|_| "/api/v1/tasks".to_string()),
206            enable_metrics: std::env::var("AXUM_ENABLE_METRICS")
207                .map(|s| s.to_lowercase() == "true")
208                .unwrap_or(true),
209            enable_health_check: std::env::var("AXUM_ENABLE_HEALTH_CHECK")
210                .map(|s| s.to_lowercase() == "true")
211                .unwrap_or(true),
212        }
213    }
214}
215
216impl TaskQueueConfig {
217    /// Validate the entire configuration
218    pub fn validate(&self) -> Result<(), TaskQueueError> {
219        // Validate Redis URL
220        if self.redis.url.is_empty() {
221            return Err(TaskQueueError::Configuration(
222                "Redis URL cannot be empty".to_string(),
223            ));
224        }
225
226        // Basic URL format validation
227        if !self.redis.url.starts_with("redis://") && !self.redis.url.starts_with("rediss://") {
228            return Err(TaskQueueError::Configuration(
229                "Redis URL must start with redis:// or rediss://".to_string(),
230            ));
231        }
232
233        // Validate worker configuration
234        if self.workers.initial_count == 0 {
235            return Err(TaskQueueError::Configuration(
236                "Initial worker count must be greater than 0".to_string(),
237            ));
238        }
239
240        if self.workers.initial_count > 1000 {
241            return Err(TaskQueueError::Configuration(
242                "Initial worker count cannot exceed 1000".to_string(),
243            ));
244        }
245
246        if let Some(max_concurrent) = self.workers.max_concurrent_tasks {
247            if max_concurrent == 0 || max_concurrent > 1000 {
248                return Err(TaskQueueError::Configuration(
249                    "Max concurrent tasks per worker must be between 1 and 1000".to_string(),
250                ));
251            }
252        }
253
254        if let Some(heartbeat) = self.workers.heartbeat_interval {
255            if heartbeat == 0 || heartbeat > 3600 {
256                return Err(TaskQueueError::Configuration(
257                    "Heartbeat interval must be between 1 and 3600 seconds".to_string(),
258                ));
259            }
260        }
261
262        if let Some(grace_period) = self.workers.shutdown_grace_period {
263            if grace_period > 300 {
264                return Err(TaskQueueError::Configuration(
265                    "Shutdown grace period cannot exceed 300 seconds".to_string(),
266                ));
267            }
268        }
269
270        // Validate pool size
271        if let Some(pool_size) = self.redis.pool_size {
272            if pool_size == 0 || pool_size > 1000 {
273                return Err(TaskQueueError::Configuration(
274                    "Redis pool size must be between 1 and 1000".to_string(),
275                ));
276            }
277        }
278
279        // Validate timeouts
280        if let Some(timeout) = self.redis.connection_timeout {
281            if timeout == 0 || timeout > 300 {
282                return Err(TaskQueueError::Configuration(
283                    "Connection timeout must be between 1 and 300 seconds".to_string(),
284                ));
285            }
286        }
287
288        if let Some(timeout) = self.redis.command_timeout {
289            if timeout == 0 || timeout > 300 {
290                return Err(TaskQueueError::Configuration(
291                    "Command timeout must be between 1 and 300 seconds".to_string(),
292                ));
293            }
294        }
295
296        // Validate scheduler configuration
297        if let Some(tick_interval) = self.scheduler.tick_interval {
298            if tick_interval == 0 || tick_interval > 3600 {
299                return Err(TaskQueueError::Configuration(
300                    "Scheduler tick interval must be between 1 and 3600 seconds".to_string(),
301                ));
302            }
303        }
304
305        if let Some(max_tasks) = self.scheduler.max_tasks_per_tick {
306            if max_tasks == 0 || max_tasks > 10000 {
307                return Err(TaskQueueError::Configuration(
308                    "Max tasks per tick must be between 1 and 10000".to_string(),
309                ));
310            }
311        }
312
313        // Validate autoscaler configuration
314        self.autoscaler.validate()?;
315
316        // Validate Actix configuration
317        #[cfg(feature = "actix-integration")]
318        {
319            if self.actix.route_prefix.is_empty() {
320                return Err(TaskQueueError::Configuration(
321                    "Actix route prefix cannot be empty".to_string(),
322                ));
323            }
324
325            if !self.actix.route_prefix.starts_with('/') {
326                return Err(TaskQueueError::Configuration(
327                    "Actix route prefix must start with '/'".to_string(),
328                ));
329            }
330        }
331
332        // Validate Axum configuration
333        #[cfg(feature = "axum-integration")]
334        {
335            if self.axum.route_prefix.is_empty() {
336                return Err(TaskQueueError::Configuration(
337                    "Axum route prefix cannot be empty".to_string(),
338                ));
339            }
340
341            if !self.axum.route_prefix.starts_with('/') {
342                return Err(TaskQueueError::Configuration(
343                    "Axum route prefix must start with '/'".to_string(),
344                ));
345            }
346        }
347
348        Ok(())
349    }
350
351    /// Load configuration from environment variables only
352    pub fn from_env() -> Result<Self, TaskQueueError> {
353        let mut config = Self::default();
354
355        // Load additional environment variables that aren't in defaults
356        if let Ok(pool_size) = std::env::var("REDIS_POOL_SIZE") {
357            config.redis.pool_size = Some(pool_size.parse().map_err(|_| {
358                TaskQueueError::Configuration("Invalid REDIS_POOL_SIZE".to_string())
359            })?);
360        }
361
362        if let Ok(timeout) = std::env::var("REDIS_CONNECTION_TIMEOUT") {
363            config.redis.connection_timeout = Some(timeout.parse().map_err(|_| {
364                TaskQueueError::Configuration("Invalid REDIS_CONNECTION_TIMEOUT".to_string())
365            })?);
366        }
367
368        // Validate the configuration
369        config.validate()?;
370
371        Ok(config)
372    }
373
374    /// Load configuration from a file (TOML or YAML based on extension)
375    #[cfg(feature = "config")]
376    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TaskQueueError> {
377        let path = path.as_ref();
378        let contents = std::fs::read_to_string(path).map_err(|e| {
379            TaskQueueError::Configuration(format!("Failed to read config file: {}", e))
380        })?;
381
382        let config: TaskQueueConfig = if path.extension().and_then(|s| s.to_str()) == Some("toml") {
383            toml::from_str(&contents).map_err(|e| {
384                TaskQueueError::Configuration(format!("Failed to parse TOML config: {}", e))
385            })?
386        } else {
387            serde_yaml::from_str(&contents).map_err(|e| {
388                TaskQueueError::Configuration(format!("Failed to parse YAML config: {}", e))
389            })?
390        };
391
392        // Validate the configuration
393        config.validate()?;
394
395        Ok(config)
396    }
397
398    /// Load configuration with automatic source detection and validation
399    #[cfg(feature = "config")]
400    pub fn load() -> Result<Self, TaskQueueError> {
401        use config::{Config, Environment, File};
402
403        let mut builder = Config::builder()
404            // Start with defaults
405            .add_source(Config::try_from(&Self::default()).map_err(|e| {
406                TaskQueueError::Configuration(format!("Failed to create default config: {}", e))
407            })?);
408
409        // Check for config file in common locations
410        for config_path in &[
411            "task-queue.toml",
412            "task-queue.yaml",
413            "task-queue.yml",
414            "config/task-queue.toml",
415            "config/task-queue.yaml",
416            "config/task-queue.yml",
417        ] {
418            if std::path::Path::new(config_path).exists() {
419                builder = builder.add_source(File::with_name(config_path));
420                break;
421            }
422        }
423
424        // Override with environment variables (prefixed with TASK_QUEUE_)
425        builder = builder.add_source(
426            Environment::with_prefix("TASK_QUEUE")
427                .separator("_")
428                .try_parsing(true),
429        );
430
431        // Also support unprefixed common variables like REDIS_URL
432        if let Ok(redis_url) = std::env::var("REDIS_URL") {
433            builder = builder.set_override("redis.url", redis_url).map_err(|e| {
434                TaskQueueError::Configuration(format!("Failed to set REDIS_URL override: {}", e))
435            })?;
436        }
437
438        let config = builder
439            .build()
440            .map_err(|e| TaskQueueError::Configuration(format!("Failed to build config: {}", e)))?;
441
442        let config: TaskQueueConfig = config.try_deserialize().map_err(|e| {
443            TaskQueueError::Configuration(format!("Failed to deserialize config: {}", e))
444        })?;
445
446        // Validate the configuration
447        config.validate()?;
448
449        Ok(config)
450    }
451
452    /// Load configuration without the config crate (fallback)
453    #[cfg(not(feature = "config"))]
454    pub fn load() -> Result<Self, TaskQueueError> {
455        Self::from_env()
456    }
457
458    /// Initialize global configuration if not already done
459    pub fn init_global() -> Result<&'static Self, TaskQueueError> {
460        match GLOBAL_CONFIG.get() {
461            Some(config) => Ok(config),
462            None => match GLOBAL_CONFIG.set(Self::load()?) {
463                Ok(()) => Ok(GLOBAL_CONFIG.get().unwrap()),
464                Err(_) => Ok(GLOBAL_CONFIG.get().unwrap()),
465            },
466        }
467    }
468
469    /// Get global configuration (if initialized)
470    pub fn global() -> Option<&'static Self> {
471        GLOBAL_CONFIG.get()
472    }
473
474    /// Get global configuration or initialize it
475    pub fn get_or_init() -> Result<&'static Self, TaskQueueError> {
476        match GLOBAL_CONFIG.get() {
477            Some(config) => Ok(config),
478            None => Self::init_global(),
479        }
480    }
481}
482
483/// Configuration builder for fluent API
484#[derive(Default)]
485pub struct ConfigBuilder {
486    config: TaskQueueConfig,
487}
488
489impl ConfigBuilder {
490    pub fn new() -> Self {
491        Self::default()
492    }
493
494    pub fn redis_url(mut self, url: impl Into<String>) -> Self {
495        self.config.redis.url = url.into();
496        self
497    }
498
499    pub fn workers(mut self, count: usize) -> Self {
500        self.config.workers.initial_count = count;
501        self
502    }
503
504    pub fn enable_auto_register(mut self, enabled: bool) -> Self {
505        self.config.auto_register.enabled = enabled;
506        self
507    }
508
509    pub fn enable_scheduler(mut self, enabled: bool) -> Self {
510        self.config.scheduler.enabled = enabled;
511        self
512    }
513
514    pub fn autoscaler_config(mut self, config: AutoScalerConfig) -> Self {
515        self.config.autoscaler = config;
516        self
517    }
518
519    pub fn build(self) -> TaskQueueConfig {
520        self.config
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527    use std::env;
528
529    #[test]
530    fn test_redis_config_default() {
531        let config = RedisConfig::default();
532
533        // Should use environment variable or default
534        assert!(!config.url.is_empty());
535        assert!(config.url.starts_with("redis://"));
536        assert!(config.pool_size.is_none());
537        assert!(config.connection_timeout.is_none());
538        assert!(config.command_timeout.is_none());
539    }
540
541    #[test]
542    fn test_worker_config_default() {
543        let config = WorkerConfig::default();
544
545        assert!(config.initial_count > 0);
546        assert!(config.max_concurrent_tasks.is_none());
547        assert!(config.heartbeat_interval.is_none());
548        assert!(config.shutdown_grace_period.is_none());
549    }
550
551    #[test]
552    fn test_auto_register_config_default() {
553        let config = AutoRegisterConfig::default();
554        // Default should be false unless environment variable is set
555        assert!(!config.enabled || env::var("AUTO_REGISTER_TASKS").is_ok());
556    }
557
558    #[test]
559    fn test_scheduler_config_default() {
560        let config = SchedulerConfig::default();
561        // Default should be false unless environment variable is set
562        assert!(!config.enabled || env::var("ENABLE_SCHEDULER").is_ok());
563        assert!(config.tick_interval.is_none());
564        assert!(config.max_tasks_per_tick.is_none());
565    }
566
567    #[cfg(feature = "actix-integration")]
568    #[test]
569    fn test_actix_config_default() {
570        let config = ActixConfig::default();
571
572        assert!(!config.route_prefix.is_empty());
573        assert!(config.route_prefix.starts_with('/'));
574        // Other defaults may vary based on environment variables
575    }
576
577    #[test]
578    fn test_task_queue_config_default() {
579        let config = TaskQueueConfig::default();
580
581        assert!(!config.redis.url.is_empty());
582        assert!(config.workers.initial_count > 0);
583        // Other fields should have sensible defaults
584    }
585
586    #[test]
587    fn test_config_validation_valid() {
588        let config = TaskQueueConfig {
589            redis: RedisConfig {
590                url: "redis://localhost:6379".to_string(),
591                pool_size: Some(10),
592                connection_timeout: Some(30),
593                command_timeout: Some(60),
594            },
595            workers: WorkerConfig {
596                initial_count: 4,
597                max_concurrent_tasks: Some(10),
598                heartbeat_interval: Some(30),
599                shutdown_grace_period: Some(60),
600            },
601            autoscaler: AutoScalerConfig::default(),
602            auto_register: AutoRegisterConfig { enabled: true },
603            scheduler: SchedulerConfig {
604                enabled: true,
605                tick_interval: Some(60),
606                max_tasks_per_tick: Some(100),
607            },
608            #[cfg(feature = "actix-integration")]
609            actix: ActixConfig {
610                auto_configure_routes: true,
611                route_prefix: "/api/tasks".to_string(),
612                enable_metrics: true,
613                enable_health_check: true,
614            },
615            #[cfg(feature = "axum-integration")]
616            axum: AxumConfig {
617                auto_configure_routes: true,
618                route_prefix: "/api/tasks".to_string(),
619                enable_metrics: true,
620                enable_health_check: true
621            },
622        };
623
624        assert!(config.validate().is_ok());
625    }
626
627    #[test]
628    fn test_config_validation_empty_redis_url() {
629        let mut config = TaskQueueConfig::default();
630        config.redis.url = "".to_string();
631
632        let result = config.validate();
633        assert!(result.is_err());
634        assert!(result
635            .unwrap_err()
636            .to_string()
637            .contains("Redis URL cannot be empty"));
638    }
639
640    #[test]
641    fn test_config_validation_invalid_redis_url() {
642        let mut config = TaskQueueConfig::default();
643        config.redis.url = "http://localhost:6379".to_string();
644
645        let result = config.validate();
646        assert!(result.is_err());
647        assert!(result
648            .unwrap_err()
649            .to_string()
650            .contains("Redis URL must start with redis://"));
651    }
652
653    #[test]
654    fn test_config_validation_zero_workers() {
655        let mut config = TaskQueueConfig::default();
656        config.workers.initial_count = 0;
657
658        let result = config.validate();
659        assert!(result.is_err());
660        assert!(result
661            .unwrap_err()
662            .to_string()
663            .contains("Initial worker count must be greater than 0"));
664    }
665
666    #[test]
667    fn test_config_validation_too_many_workers() {
668        let mut config = TaskQueueConfig::default();
669        config.workers.initial_count = 1001;
670
671        let result = config.validate();
672        assert!(result.is_err());
673        assert!(result
674            .unwrap_err()
675            .to_string()
676            .contains("Initial worker count cannot exceed 1000"));
677    }
678
679    #[test]
680    fn test_config_validation_invalid_pool_size() {
681        let mut config = TaskQueueConfig::default();
682        config.redis.pool_size = Some(0);
683
684        let result = config.validate();
685        assert!(result.is_err());
686        assert!(result
687            .unwrap_err()
688            .to_string()
689            .contains("Redis pool size must be between 1 and 1000"));
690    }
691
692    #[test]
693    fn test_config_validation_invalid_timeouts() {
694        let mut config = TaskQueueConfig::default();
695        config.redis.connection_timeout = Some(0);
696
697        let result = config.validate();
698        assert!(result.is_err());
699        assert!(result
700            .unwrap_err()
701            .to_string()
702            .contains("Connection timeout must be between 1 and 300"));
703
704        config.redis.connection_timeout = Some(30);
705        config.redis.command_timeout = Some(301);
706
707        let result = config.validate();
708        assert!(result.is_err());
709        assert!(result
710            .unwrap_err()
711            .to_string()
712            .contains("Command timeout must be between 1 and 300"));
713    }
714
715    #[test]
716    fn test_config_validation_invalid_worker_settings() {
717        let mut config = TaskQueueConfig::default();
718        config.workers.max_concurrent_tasks = Some(0);
719
720        let result = config.validate();
721        assert!(result.is_err());
722        assert!(result
723            .unwrap_err()
724            .to_string()
725            .contains("Max concurrent tasks per worker must be between 1 and 1000"));
726
727        config.workers.max_concurrent_tasks = Some(10);
728        config.workers.heartbeat_interval = Some(0);
729
730        let result = config.validate();
731        assert!(result.is_err());
732        assert!(result
733            .unwrap_err()
734            .to_string()
735            .contains("Heartbeat interval must be between 1 and 3600"));
736
737        config.workers.heartbeat_interval = Some(30);
738        config.workers.shutdown_grace_period = Some(301);
739
740        let result = config.validate();
741        assert!(result.is_err());
742        assert!(result
743            .unwrap_err()
744            .to_string()
745            .contains("Shutdown grace period cannot exceed 300"));
746    }
747
748    #[test]
749    fn test_config_validation_invalid_scheduler_settings() {
750        let mut config = TaskQueueConfig::default();
751        config.scheduler.tick_interval = Some(0);
752
753        let result = config.validate();
754        assert!(result.is_err());
755        assert!(result
756            .unwrap_err()
757            .to_string()
758            .contains("Scheduler tick interval must be between 1 and 3600"));
759
760        config.scheduler.tick_interval = Some(60);
761        config.scheduler.max_tasks_per_tick = Some(0);
762
763        let result = config.validate();
764        assert!(result.is_err());
765        assert!(result
766            .unwrap_err()
767            .to_string()
768            .contains("Max tasks per tick must be between 1 and 10000"));
769    }
770
771    #[cfg(feature = "actix-integration")]
772    #[test]
773    fn test_config_validation_invalid_actix_settings() {
774        let mut config = TaskQueueConfig::default();
775        config.actix.route_prefix = "".to_string();
776
777        let result = config.validate();
778        assert!(result.is_err());
779        assert!(result
780            .unwrap_err()
781            .to_string()
782            .contains("Actix route prefix cannot be empty"));
783
784        config.actix.route_prefix = "api/tasks".to_string(); // Missing leading slash
785
786        let result = config.validate();
787        assert!(result.is_err());
788        assert!(result
789            .unwrap_err()
790            .to_string()
791            .contains("Actix route prefix must start with '/'"));
792    }
793
794    #[cfg(feature = "axum-integration")]
795    #[test]
796    fn test_config_validation_invalid_axum_settings() {
797        let mut config = TaskQueueConfig::default();
798        config.axum.route_prefix = "".to_string();
799
800        let result = config.validate();
801        assert!(result.is_err());
802        assert!(result
803            .unwrap_err()
804            .to_string()
805            .contains("Axum route prefix cannot be empty"));
806
807        config.axum.route_prefix = "api/tasks".to_string(); // Missing leading slash
808
809        let result = config.validate();
810        assert!(result.is_err());
811        assert!(result
812            .unwrap_err()
813            .to_string()
814            .contains("Axum route prefix must start with '/'"));
815    }
816
817    #[cfg(feature = "axum-integration")]
818    #[test]
819    fn test_axum_config_default() {
820        let config = AxumConfig::default();
821
822        assert!(!config.route_prefix.is_empty());
823        assert!(config.route_prefix.starts_with('/'));
824        // Other defaults may vary based on environment variables
825    }
826
827    #[test]
828    fn test_config_builder() {
829        let config = ConfigBuilder::new()
830            .redis_url("redis://test:6379")
831            .workers(8)
832            .enable_auto_register(true)
833            .enable_scheduler(true)
834            .autoscaler_config(AutoScalerConfig {
835                min_workers: 2,
836                max_workers: 16,
837                scale_up_count: 4,
838                scale_down_count: 2,
839                scaling_triggers: crate::autoscaler::ScalingTriggers {
840                    queue_pressure_threshold: 1.0,
841                    worker_utilization_threshold: 0.85,
842                    task_complexity_threshold: 1.5,
843                    error_rate_threshold: 0.05,
844                    memory_pressure_threshold: 512.0,
845                },
846                enable_adaptive_thresholds: true,
847                learning_rate: 0.1,
848                adaptation_window_minutes: 30,
849                scale_up_cooldown_seconds: 60,
850                scale_down_cooldown_seconds: 300,
851                consecutive_signals_required: 2,
852                target_sla: crate::autoscaler::SLATargets {
853                    max_p95_latency_ms: 5000.0,
854                    min_success_rate: 0.95,
855                    max_queue_wait_time_ms: 10000.0,
856                    target_worker_utilization: 0.70,
857                },
858            })
859            .build();
860
861        assert_eq!(config.redis.url, "redis://test:6379");
862        assert_eq!(config.workers.initial_count, 8);
863        assert!(config.auto_register.enabled);
864        assert!(config.scheduler.enabled);
865        assert_eq!(config.autoscaler.min_workers, 2);
866        assert_eq!(config.autoscaler.max_workers, 16);
867    }
868
869    #[test]
870    fn test_config_serialization() {
871        let config = TaskQueueConfig::default();
872
873        // Test JSON serialization
874        let json = serde_json::to_string(&config).expect("Failed to serialize to JSON");
875        let deserialized: TaskQueueConfig =
876            serde_json::from_str(&json).expect("Failed to deserialize from JSON");
877
878        assert_eq!(config.redis.url, deserialized.redis.url);
879        assert_eq!(
880            config.workers.initial_count,
881            deserialized.workers.initial_count
882        );
883        assert_eq!(
884            config.auto_register.enabled,
885            deserialized.auto_register.enabled
886        );
887        assert_eq!(config.scheduler.enabled, deserialized.scheduler.enabled);
888    }
889
890    #[test]
891    fn test_config_from_env() {
892        // Set environment variables
893        env::set_var("REDIS_POOL_SIZE", "15");
894        env::set_var("REDIS_CONNECTION_TIMEOUT", "45");
895
896        let config = TaskQueueConfig::from_env().expect("Failed to load config from env");
897
898        assert_eq!(config.redis.pool_size, Some(15));
899        assert_eq!(config.redis.connection_timeout, Some(45));
900
901        // Clean up
902        env::remove_var("REDIS_POOL_SIZE");
903        env::remove_var("REDIS_CONNECTION_TIMEOUT");
904    }
905
906    #[test]
907    fn test_config_from_env_invalid_values() {
908        // Set invalid environment variable
909        env::set_var("REDIS_POOL_SIZE", "invalid");
910
911        let result = TaskQueueConfig::from_env();
912        assert!(result.is_err());
913        assert!(result
914            .unwrap_err()
915            .to_string()
916            .contains("Invalid REDIS_POOL_SIZE"));
917
918        // Clean up
919        env::remove_var("REDIS_POOL_SIZE");
920    }
921
922    #[test]
923    fn test_config_clone() {
924        let original = TaskQueueConfig::default();
925        let cloned = original.clone();
926
927        assert_eq!(original.redis.url, cloned.redis.url);
928        assert_eq!(original.workers.initial_count, cloned.workers.initial_count);
929        assert_eq!(original.auto_register.enabled, cloned.auto_register.enabled);
930        assert_eq!(original.scheduler.enabled, cloned.scheduler.enabled);
931    }
932
933    #[test]
934    fn test_config_debug() {
935        let config = TaskQueueConfig::default();
936        let debug_str = format!("{:?}", config);
937
938        assert!(debug_str.contains("TaskQueueConfig"));
939        assert!(debug_str.contains("redis"));
940        assert!(debug_str.contains("workers"));
941        assert!(debug_str.contains("autoscaler"));
942    }
943
944    #[test]
945    fn test_individual_config_structs_clone() {
946        let redis_config = RedisConfig::default();
947        let cloned_redis = redis_config.clone();
948        assert_eq!(redis_config.url, cloned_redis.url);
949
950        let worker_config = WorkerConfig::default();
951        let cloned_worker = worker_config.clone();
952        assert_eq!(worker_config.initial_count, cloned_worker.initial_count);
953
954        let auto_register_config = AutoRegisterConfig::default();
955        let cloned_auto_register = auto_register_config.clone();
956        assert_eq!(auto_register_config.enabled, cloned_auto_register.enabled);
957
958        let scheduler_config = SchedulerConfig::default();
959        let cloned_scheduler = scheduler_config.clone();
960        assert_eq!(scheduler_config.enabled, cloned_scheduler.enabled);
961    }
962
963    #[test]
964    fn test_config_builder_default() {
965        let builder = ConfigBuilder::new();
966        let config = builder.build();
967
968        // Should have the same values as TaskQueueConfig::default()
969        let default_config = TaskQueueConfig::default();
970        assert_eq!(config.redis.url, default_config.redis.url);
971        assert_eq!(
972            config.workers.initial_count,
973            default_config.workers.initial_count
974        );
975    }
976
977    #[test]
978    fn test_config_builder_method_chaining() {
979        let config = ConfigBuilder::default()
980            .redis_url("redis://chained:6379")
981            .workers(5)
982            .enable_auto_register(false)
983            .enable_scheduler(false)
984            .build();
985
986        assert_eq!(config.redis.url, "redis://chained:6379");
987        assert_eq!(config.workers.initial_count, 5);
988        assert!(!config.auto_register.enabled);
989        assert!(!config.scheduler.enabled);
990    }
991
992    #[test]
993    fn test_redis_url_validation() {
994        let test_cases = vec![
995            ("redis://localhost:6379", true),
996            ("rediss://localhost:6379", true),
997            ("redis://user:pass@localhost:6379/0", true),
998            ("redis://localhost", true),
999            ("http://localhost:6379", false),
1000            ("localhost:6379", false),
1001            ("", false),
1002        ];
1003
1004        for (url, should_be_valid) in test_cases {
1005            let mut config = TaskQueueConfig::default();
1006            config.redis.url = url.to_string();
1007
1008            let result = config.validate();
1009            if should_be_valid {
1010                assert!(result.is_ok(), "URL '{}' should be valid", url);
1011            } else {
1012                assert!(result.is_err(), "URL '{}' should be invalid", url);
1013            }
1014        }
1015    }
1016}