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>,
20 pub source_refresh_interval: Duration,
22 pub rate_limit: RateLimitConfig,
24 pub proxy_timeout: Duration,
26 pub max_concurrent_per_request: usize,
28 pub max_inflight: usize,
30 pub task_pool_capacity: usize,
32 pub health_window: usize,
34 pub cooldown_base: Duration,
36 pub cooldown_max: Duration,
38 pub cooldown_consecutive_fails: usize,
40 pub eviction_min_samples: usize,
42 pub circuit_breaker_threshold: usize,
44 pub circuit_breaker_probe_interval: Duration,
46 pub state_file: Option<PathBuf>,
48 pub state_save_interval: Duration,
50 pub metrics_log_interval: Duration,
52 pub prefer_remote_dns: bool,
54}
55
56impl Default for ScatterProxyConfig {
57 fn default() -> Self {
58 Self {
59 sources: Vec::new(),
60 source_refresh_interval: Duration::from_secs(600),
61 rate_limit: RateLimitConfig::default(),
62 proxy_timeout: Duration::from_secs(8),
63 max_concurrent_per_request: 3,
64 max_inflight: 100,
65 task_pool_capacity: 1000,
66 health_window: 30,
67 cooldown_base: Duration::from_secs(30),
68 cooldown_max: Duration::from_secs(300),
69 cooldown_consecutive_fails: 3,
70 eviction_min_samples: 30,
71 circuit_breaker_threshold: 10,
72 circuit_breaker_probe_interval: Duration::from_secs(30),
73 state_file: None,
74 state_save_interval: Duration::from_secs(300),
75 metrics_log_interval: Duration::from_secs(30),
76 prefer_remote_dns: true,
77 }
78 }
79}
80
81pub struct RateLimitConfig {
83 pub default_interval: Duration,
85 pub host_overrides: HashMap<String, Duration>,
87}
88
89impl Default for RateLimitConfig {
90 fn default() -> Self {
91 Self {
92 default_interval: Duration::from_millis(500),
93 host_overrides: HashMap::new(),
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_scatter_proxy_config_defaults() {
104 let cfg = ScatterProxyConfig::default();
105
106 assert!(cfg.sources.is_empty());
107 assert_eq!(cfg.source_refresh_interval, Duration::from_secs(600));
108 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
109 assert_eq!(cfg.max_concurrent_per_request, 3);
110 assert_eq!(cfg.max_inflight, 100);
111 assert_eq!(cfg.task_pool_capacity, 1000);
112 assert_eq!(cfg.health_window, 30);
113 assert_eq!(cfg.cooldown_base, Duration::from_secs(30));
114 assert_eq!(cfg.cooldown_max, Duration::from_secs(300));
115 assert_eq!(cfg.cooldown_consecutive_fails, 3);
116 assert_eq!(cfg.eviction_min_samples, 30);
117 assert_eq!(cfg.circuit_breaker_threshold, 10);
118 assert_eq!(cfg.circuit_breaker_probe_interval, Duration::from_secs(30));
119 assert!(cfg.state_file.is_none());
120 assert_eq!(cfg.state_save_interval, Duration::from_secs(300));
121 assert_eq!(cfg.metrics_log_interval, Duration::from_secs(30));
122 assert!(cfg.prefer_remote_dns);
123 }
124
125 #[test]
126 fn test_rate_limit_config_defaults() {
127 let rl = RateLimitConfig::default();
128
129 assert_eq!(rl.default_interval, Duration::from_millis(500));
130 assert!(rl.host_overrides.is_empty());
131 }
132
133 #[test]
134 fn test_rate_limit_config_nested_in_scatter_proxy_config() {
135 let cfg = ScatterProxyConfig::default();
136
137 assert_eq!(cfg.rate_limit.default_interval, Duration::from_millis(500));
138 assert!(cfg.rate_limit.host_overrides.is_empty());
139 }
140
141 #[test]
142 fn test_config_can_be_customised() {
143 let cfg = ScatterProxyConfig {
144 sources: vec!["https://example.com/proxies.txt".into()],
145 max_concurrent_per_request: 5,
146 max_inflight: 200,
147 state_file: Some(PathBuf::from("/tmp/scatter.json")),
148 prefer_remote_dns: false,
149 rate_limit: RateLimitConfig {
150 default_interval: Duration::from_millis(250),
151 host_overrides: {
152 let mut m = HashMap::new();
153 m.insert("slow.example.com".into(), Duration::from_secs(2));
154 m
155 },
156 },
157 ..ScatterProxyConfig::default()
158 };
159
160 assert_eq!(cfg.sources.len(), 1);
161 assert_eq!(cfg.max_concurrent_per_request, 5);
162 assert_eq!(cfg.max_inflight, 200);
163 assert_eq!(cfg.state_file, Some(PathBuf::from("/tmp/scatter.json")));
164 assert!(!cfg.prefer_remote_dns);
165 assert_eq!(cfg.rate_limit.default_interval, Duration::from_millis(250));
166 assert_eq!(
167 cfg.rate_limit.host_overrides.get("slow.example.com"),
168 Some(&Duration::from_secs(2))
169 );
170 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
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
193 #[test]
194 fn test_default_proxy_sources_not_empty() {
195 assert!(!DEFAULT_PROXY_SOURCES.is_empty());
196 for source in DEFAULT_PROXY_SOURCES {
197 assert!(source.starts_with("https://"));
198 }
199 }
200}