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