1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::Duration;
4
5pub const DEFAULT_PROXY_SOURCES: &[&str] = &[
8 "https://letllmrun.codeberg.page/scatter-proxy/socks5.txt",
10];
11
12#[derive(Clone)]
14pub struct ScatterProxyConfig {
15 pub sources: Vec<String>,
18 pub source_refresh_interval: Duration,
20 pub rate_limit: RateLimitConfig,
22 pub proxy_timeout: Duration,
24 pub max_concurrent_per_request: usize,
26 pub max_inflight: usize,
28 pub task_pool_capacity: usize,
30 pub health_window: usize,
32 pub cooldown_base: Duration,
34 pub cooldown_max: Duration,
36 pub cooldown_consecutive_fails: usize,
38 pub eviction_min_samples: usize,
40 pub state_file: Option<PathBuf>,
42 pub state_save_interval: Duration,
44 pub metrics_log_interval: Duration,
46 pub prefer_remote_dns: bool,
48 pub name: Option<String>,
56}
57
58impl Default for ScatterProxyConfig {
59 fn default() -> Self {
60 Self {
61 sources: Vec::new(),
62 source_refresh_interval: Duration::from_secs(600),
63 rate_limit: RateLimitConfig::default(),
64 proxy_timeout: Duration::from_secs(8),
65 max_concurrent_per_request: 3,
66 max_inflight: 100,
67 task_pool_capacity: 1000,
68 health_window: 30,
69 cooldown_base: Duration::from_secs(30),
70 cooldown_max: Duration::from_secs(300),
71 cooldown_consecutive_fails: 3,
72 eviction_min_samples: 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 name: None,
78 }
79 }
80}
81
82#[derive(Clone)]
84pub struct RateLimitConfig {
85 pub default_interval: Duration,
87 pub host_overrides: HashMap<String, Duration>,
89}
90
91impl Default for RateLimitConfig {
92 fn default() -> Self {
93 Self {
94 default_interval: Duration::from_millis(500),
95 host_overrides: HashMap::new(),
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_scatter_proxy_config_defaults() {
106 let cfg = ScatterProxyConfig::default();
107
108 assert!(cfg.sources.is_empty());
109 assert_eq!(cfg.source_refresh_interval, Duration::from_secs(600));
110 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
111 assert_eq!(cfg.max_concurrent_per_request, 3);
112 assert_eq!(cfg.max_inflight, 100);
113 assert_eq!(cfg.task_pool_capacity, 1000);
114 assert_eq!(cfg.health_window, 30);
115 assert_eq!(cfg.cooldown_base, Duration::from_secs(30));
116 assert_eq!(cfg.cooldown_max, Duration::from_secs(300));
117 assert_eq!(cfg.cooldown_consecutive_fails, 3);
118 assert_eq!(cfg.eviction_min_samples, 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
196 .iter()
197 .any(|source| source.contains("codeberg.page/scatter-proxy/socks5.txt")));
198 for source in DEFAULT_PROXY_SOURCES {
199 assert!(source.starts_with("https://"));
200 }
201 }
202}