Skip to main content

scatter_proxy/
config.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::Duration;
4
5/// Default free SOCKS5 proxy sources used when no custom sources are configured.
6/// These are fetched from the scatter-proxy GitHub Pages and popular community lists.
7pub const DEFAULT_PROXY_SOURCES: &[&str] = &[
8    // scatter-proxy's own curated list (GitHub Pages + jsDelivr CDN)
9    "https://cdn.jsdelivr.net/gh/letllmrun/scatter-proxy@main/docs/socks5.txt",
10    // Community-maintained lists
11    "https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt",
12    "https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/socks5.txt",
13];
14
15/// Main configuration for ScatterProxy.
16pub struct ScatterProxyConfig {
17    /// URLs of proxy sources (line-delimited `ip:port` or `socks5://ip:port`).
18    pub sources: Vec<String>,
19    /// How often to re-fetch proxy sources (default: 10 min).
20    pub source_refresh_interval: Duration,
21    /// Per-(proxy, host) rate-limiting configuration.
22    pub rate_limit: RateLimitConfig,
23    /// Timeout for a single proxy connection attempt (default: 8s).
24    pub proxy_timeout: Duration,
25    /// Overall timeout for a task from submission to final failure (default: 60s).
26    pub task_timeout: Duration,
27    /// Number of concurrent proxy paths raced per request (default: 3).
28    pub max_concurrent_per_request: usize,
29    /// Global in-flight concurrency limit (default: 100).
30    pub max_inflight: usize,
31    /// Maximum scheduling attempts per task before giving up (default: 5).
32    pub max_attempts: usize,
33    /// Maximum number of pending tasks in the pool (default: 1000).
34    pub task_pool_capacity: usize,
35    /// Sliding window size for health tracking (default: 30).
36    pub health_window: usize,
37    /// Base cooldown duration after consecutive failures (default: 30s).
38    pub cooldown_base: Duration,
39    /// Maximum cooldown duration (default: 300s).
40    pub cooldown_max: Duration,
41    /// Number of consecutive failures before entering cooldown (default: 3).
42    pub cooldown_consecutive_fails: usize,
43    /// Minimum samples required before a proxy can be evicted (default: 30).
44    pub eviction_min_samples: usize,
45    /// Number of target errors before tripping the host circuit breaker (default: 10).
46    pub circuit_breaker_threshold: usize,
47    /// Interval between probe requests when a circuit breaker is open (default: 30s).
48    pub circuit_breaker_probe_interval: Duration,
49    /// Optional file path for persisting proxy state as JSON.
50    pub state_file: Option<PathBuf>,
51    /// How often to save state to disk (default: 5 min).
52    pub state_save_interval: Duration,
53    /// How often to log the metrics summary line (default: 30s).
54    pub metrics_log_interval: Duration,
55    /// Whether to prefer remote DNS resolution through the SOCKS5 proxy (default: true).
56    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
86/// Per-(proxy, host) rate-limiting configuration.
87pub struct RateLimitConfig {
88    /// Default minimum interval between requests through the same proxy to the same host (default: 500ms).
89    pub default_interval: Duration,
90    /// Per-host overrides for the minimum interval.
91    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        // fields that should still have defaults
180        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}