1use crate::autoscaler::AutoScalerConfig;
10use crate::TaskQueueError;
11use serde::{Deserialize, Serialize};
12use std::sync::OnceLock;
13
14static GLOBAL_CONFIG: OnceLock<TaskQueueConfig> = OnceLock::new();
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19pub struct TaskQueueConfig {
20 pub redis: RedisConfig,
22
23 pub workers: WorkerConfig,
25
26 pub autoscaler: AutoScalerConfig,
28
29 pub auto_register: AutoRegisterConfig,
31
32 pub scheduler: SchedulerConfig,
34
35 #[cfg(feature = "actix-integration")]
37 pub actix: ActixConfig,
38
39 #[cfg(feature = "axum-integration")]
41 pub axum: AxumConfig,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct RedisConfig {
47 pub url: String,
49
50 pub pool_size: Option<u32>,
52
53 pub connection_timeout: Option<u64>,
55
56 pub command_timeout: Option<u64>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct WorkerConfig {
63 pub initial_count: usize,
65
66 pub max_concurrent_tasks: Option<usize>,
68
69 pub heartbeat_interval: Option<u64>,
71
72 pub shutdown_grace_period: Option<u64>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct AutoRegisterConfig {
79 pub enabled: bool,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct SchedulerConfig {
86 pub enabled: bool,
88
89 pub tick_interval: Option<u64>,
91
92 pub max_tasks_per_tick: Option<usize>,
94}
95
96#[cfg(feature = "actix-integration")]
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ActixConfig {
100 pub auto_configure_routes: bool,
102
103 pub route_prefix: String,
105
106 pub enable_metrics: bool,
108
109 pub enable_health_check: bool,
111}
112
113#[cfg(feature = "axum-integration")]
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct AxumConfig {
117 pub auto_configure_routes: bool,
119
120 pub route_prefix: String,
122
123 pub enable_metrics: bool,
125
126 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 pub fn validate(&self) -> Result<(), TaskQueueError> {
219 if self.redis.url.is_empty() {
221 return Err(TaskQueueError::Configuration(
222 "Redis URL cannot be empty".to_string(),
223 ));
224 }
225
226 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 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 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 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 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 self.autoscaler.validate()?;
315
316 #[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 #[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 pub fn from_env() -> Result<Self, TaskQueueError> {
353 let mut config = Self::default();
354
355 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 config.validate()?;
370
371 Ok(config)
372 }
373
374 #[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 config.validate()?;
394
395 Ok(config)
396 }
397
398 #[cfg(feature = "config")]
400 pub fn load() -> Result<Self, TaskQueueError> {
401 use config::{Config, Environment, File};
402
403 let mut builder = Config::builder()
404 .add_source(Config::try_from(&Self::default()).map_err(|e| {
406 TaskQueueError::Configuration(format!("Failed to create default config: {}", e))
407 })?);
408
409 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 builder = builder.add_source(
426 Environment::with_prefix("TASK_QUEUE")
427 .separator("_")
428 .try_parsing(true),
429 );
430
431 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 config.validate()?;
448
449 Ok(config)
450 }
451
452 #[cfg(not(feature = "config"))]
454 pub fn load() -> Result<Self, TaskQueueError> {
455 Self::from_env()
456 }
457
458 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 pub fn global() -> Option<&'static Self> {
471 GLOBAL_CONFIG.get()
472 }
473
474 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#[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 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 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 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 }
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 }
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(); 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(); 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 }
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 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 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 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 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 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 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}