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