1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::Duration;
4
5pub struct ScatterProxyConfig {
7 pub sources: Vec<String>,
9 pub source_refresh_interval: Duration,
11 pub rate_limit: RateLimitConfig,
13 pub proxy_timeout: Duration,
15 pub task_timeout: Duration,
17 pub max_concurrent_per_request: usize,
19 pub max_inflight: usize,
21 pub max_attempts: usize,
23 pub task_pool_capacity: usize,
25 pub health_window: usize,
27 pub cooldown_base: Duration,
29 pub cooldown_max: Duration,
31 pub cooldown_consecutive_fails: usize,
33 pub eviction_min_samples: usize,
35 pub circuit_breaker_threshold: usize,
37 pub circuit_breaker_probe_interval: Duration,
39 pub state_file: Option<PathBuf>,
41 pub state_save_interval: Duration,
43 pub metrics_log_interval: Duration,
45 pub prefer_remote_dns: bool,
47}
48
49impl Default for ScatterProxyConfig {
50 fn default() -> Self {
51 Self {
52 sources: Vec::new(),
53 source_refresh_interval: Duration::from_secs(600),
54 rate_limit: RateLimitConfig::default(),
55 proxy_timeout: Duration::from_secs(8),
56 task_timeout: Duration::from_secs(60),
57 max_concurrent_per_request: 3,
58 max_inflight: 100,
59 max_attempts: 5,
60 task_pool_capacity: 1000,
61 health_window: 30,
62 cooldown_base: Duration::from_secs(30),
63 cooldown_max: Duration::from_secs(300),
64 cooldown_consecutive_fails: 3,
65 eviction_min_samples: 30,
66 circuit_breaker_threshold: 10,
67 circuit_breaker_probe_interval: Duration::from_secs(30),
68 state_file: None,
69 state_save_interval: Duration::from_secs(300),
70 metrics_log_interval: Duration::from_secs(30),
71 prefer_remote_dns: true,
72 }
73 }
74}
75
76pub struct RateLimitConfig {
78 pub default_interval: Duration,
80 pub host_overrides: HashMap<String, Duration>,
82}
83
84impl Default for RateLimitConfig {
85 fn default() -> Self {
86 Self {
87 default_interval: Duration::from_millis(500),
88 host_overrides: HashMap::new(),
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_scatter_proxy_config_defaults() {
99 let cfg = ScatterProxyConfig::default();
100
101 assert!(cfg.sources.is_empty());
102 assert_eq!(cfg.source_refresh_interval, Duration::from_secs(600));
103 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
104 assert_eq!(cfg.task_timeout, Duration::from_secs(60));
105 assert_eq!(cfg.max_concurrent_per_request, 3);
106 assert_eq!(cfg.max_inflight, 100);
107 assert_eq!(cfg.max_attempts, 5);
108 assert_eq!(cfg.task_pool_capacity, 1000);
109 assert_eq!(cfg.health_window, 30);
110 assert_eq!(cfg.cooldown_base, Duration::from_secs(30));
111 assert_eq!(cfg.cooldown_max, Duration::from_secs(300));
112 assert_eq!(cfg.cooldown_consecutive_fails, 3);
113 assert_eq!(cfg.eviction_min_samples, 30);
114 assert_eq!(cfg.circuit_breaker_threshold, 10);
115 assert_eq!(cfg.circuit_breaker_probe_interval, Duration::from_secs(30));
116 assert!(cfg.state_file.is_none());
117 assert_eq!(cfg.state_save_interval, Duration::from_secs(300));
118 assert_eq!(cfg.metrics_log_interval, Duration::from_secs(30));
119 assert!(cfg.prefer_remote_dns);
120 }
121
122 #[test]
123 fn test_rate_limit_config_defaults() {
124 let rl = RateLimitConfig::default();
125
126 assert_eq!(rl.default_interval, Duration::from_millis(500));
127 assert!(rl.host_overrides.is_empty());
128 }
129
130 #[test]
131 fn test_rate_limit_config_nested_in_scatter_proxy_config() {
132 let cfg = ScatterProxyConfig::default();
133
134 assert_eq!(cfg.rate_limit.default_interval, Duration::from_millis(500));
135 assert!(cfg.rate_limit.host_overrides.is_empty());
136 }
137
138 #[test]
139 fn test_config_can_be_customised() {
140 let cfg = ScatterProxyConfig {
141 sources: vec!["https://example.com/proxies.txt".into()],
142 max_concurrent_per_request: 5,
143 max_inflight: 200,
144 max_attempts: 10,
145 state_file: Some(PathBuf::from("/tmp/scatter.json")),
146 prefer_remote_dns: false,
147 rate_limit: RateLimitConfig {
148 default_interval: Duration::from_millis(250),
149 host_overrides: {
150 let mut m = HashMap::new();
151 m.insert("slow.example.com".into(), Duration::from_secs(2));
152 m
153 },
154 },
155 ..ScatterProxyConfig::default()
156 };
157
158 assert_eq!(cfg.sources.len(), 1);
159 assert_eq!(cfg.max_concurrent_per_request, 5);
160 assert_eq!(cfg.max_inflight, 200);
161 assert_eq!(cfg.max_attempts, 10);
162 assert_eq!(cfg.state_file, Some(PathBuf::from("/tmp/scatter.json")));
163 assert!(!cfg.prefer_remote_dns);
164 assert_eq!(cfg.rate_limit.default_interval, Duration::from_millis(250));
165 assert_eq!(
166 cfg.rate_limit.host_overrides.get("slow.example.com"),
167 Some(&Duration::from_secs(2))
168 );
169 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
171 assert_eq!(cfg.task_timeout, Duration::from_secs(60));
172 assert_eq!(cfg.health_window, 30);
173 }
174
175 #[test]
176 fn test_cooldown_max_gte_cooldown_base() {
177 let cfg = ScatterProxyConfig::default();
178 assert!(cfg.cooldown_max >= cfg.cooldown_base);
179 }
180
181 #[test]
182 fn test_source_refresh_interval_is_10_minutes() {
183 let cfg = ScatterProxyConfig::default();
184 assert_eq!(cfg.source_refresh_interval.as_secs(), 10 * 60);
185 }
186
187 #[test]
188 fn test_state_save_interval_is_5_minutes() {
189 let cfg = ScatterProxyConfig::default();
190 assert_eq!(cfg.state_save_interval.as_secs(), 5 * 60);
191 }
192}