1mod challenge;
6mod classifier;
7mod config;
8mod error;
9mod health;
10mod metrics;
11mod mutator;
12mod persist;
13mod proxy;
14mod rate_limit;
15mod router;
16mod scheduler;
17mod score;
18mod task;
19
20pub use challenge::ChallengeResolver;
22pub use classifier::{BodyClassifier, BodyVerdict, DefaultClassifier};
23pub use config::{RateLimitConfig, ScatterProxyConfig, DEFAULT_PROXY_SOURCES};
24pub use error::ScatterProxyError;
25pub use metrics::{PoolMetrics, ProxyHostStats};
26pub use mutator::RequestMutator;
27pub use proxy::{ProxyManager, ProxyState};
28pub use router::ScatterProxyRouter;
29pub use task::{ScatterResponse, TaskHandle};
30
31pub use http::HeaderMap;
33pub use http::StatusCode;
34
35use std::collections::HashMap;
36use std::sync::Arc;
37use std::time::Duration;
38use tokio::sync::{broadcast, Semaphore};
39use tokio::task::JoinHandle;
40use tracing::{debug, info, warn};
41
42#[allow(dead_code)]
44pub struct ScatterProxy {
45 config: Arc<ScatterProxyConfig>,
46 task_pool: Arc<task::TaskPool>,
47 health: Arc<health::HealthTracker>,
48 rate_limiter: Arc<rate_limit::RateLimiter>,
49 proxy_manager: Arc<proxy::ProxyManager>,
50 throughput: Arc<metrics::ThroughputTracker>,
51 semaphore: Arc<Semaphore>,
52 shutdown_tx: broadcast::Sender<()>,
53 _scheduler_handle: JoinHandle<()>,
55 _persist_handle: Option<JoinHandle<()>>,
56 _metrics_handle: JoinHandle<()>,
57 _refresh_handle: JoinHandle<()>,
58}
59
60impl ScatterProxy {
61 pub async fn new(
67 config: ScatterProxyConfig,
68 classifier: impl BodyClassifier,
69 ) -> Result<Self, ScatterProxyError> {
70 Self::new_with_extensions(
71 config,
72 classifier,
73 None::<NoOpResolver>,
74 None::<NoOpMutator>,
75 )
76 .await
77 }
78
79 pub async fn new_with_extensions<C, R, M>(
84 config: ScatterProxyConfig,
85 classifier: C,
86 challenge_resolver: Option<R>,
87 request_mutator: Option<M>,
88 ) -> Result<Self, ScatterProxyError>
89 where
90 C: BodyClassifier,
91 R: ChallengeResolver,
92 M: RequestMutator,
93 {
94 let metrics_name = config.name.clone();
95 let config = Arc::new(config);
96
97 let task_pool = Arc::new(task::TaskPool::new(config.task_pool_capacity));
99 let health = Arc::new(health::HealthTracker::new(config.health_window));
100 let rate_limiter = Arc::new(rate_limit::RateLimiter::new(&config.rate_limit));
101 let proxy_manager = Arc::new(proxy::ProxyManager::new(config.proxy_timeout));
102 let throughput = Arc::new(metrics::ThroughputTracker::new());
103 let semaphore = Arc::new(Semaphore::new(config.max_inflight));
104 let classifier: Arc<dyn BodyClassifier> = Arc::new(classifier);
105 let challenge_resolver: Option<Arc<dyn ChallengeResolver>> =
106 challenge_resolver.map(|r| Arc::new(r) as Arc<dyn ChallengeResolver>);
107 let request_mutator: Option<Arc<dyn RequestMutator>> =
108 request_mutator.map(|m| Arc::new(m) as Arc<dyn RequestMutator>);
109
110 let effective_sources: Vec<String> = if config.sources.is_empty() {
112 info!("no custom sources configured, using default free proxy sources");
113 DEFAULT_PROXY_SOURCES
114 .iter()
115 .map(|s| s.to_string())
116 .collect()
117 } else {
118 config.sources.clone()
119 };
120
121 let proxy_count = proxy_manager
123 .fetch_and_add(&effective_sources, config.prefer_remote_dns)
124 .await?;
125
126 if let Some(ref state_path) = config.state_file {
128 match persist::load_state(state_path).await {
129 Ok(Some(persisted)) => {
130 for (proxy_url, persisted_proxy) in &persisted.proxies {
131 for (host, stats) in &persisted_proxy.hosts {
133 health.restore(proxy_url, host, stats);
134 }
135 match persisted_proxy.state.as_str() {
137 "Active" => {
138 proxy_manager.set_state(proxy_url, proxy::ProxyState::Active)
139 }
140 "Dead" => proxy_manager.set_state(proxy_url, proxy::ProxyState::Dead),
141 "Retired" => {
142 proxy_manager.set_state(proxy_url, proxy::ProxyState::Retired)
143 }
144 _ => {}
145 }
146 if persisted_proxy.request_count > 0 {
148 for _ in 0..persisted_proxy.request_count {
149 proxy_manager.increment_request_count(proxy_url);
150 }
151 }
152 }
153 debug!(
154 proxies = persisted.proxies.len(),
155 "restored persisted state"
156 );
157 }
158 Ok(None) => {
159 debug!("no persisted state file found, starting fresh");
160 }
161 Err(e) => {
162 warn!(error = %e, "failed to load persisted state, starting fresh");
163 }
164 }
165 }
166
167 info!(count = proxy_count, "loaded proxies from sources");
168
169 let (shutdown_tx, _) = broadcast::channel(1);
171
172 let sched = scheduler::Scheduler::new(
176 Arc::clone(&config),
177 Arc::clone(&task_pool),
178 Arc::clone(&health),
179 Arc::clone(&rate_limiter),
180 Arc::clone(&proxy_manager),
181 Arc::clone(&classifier),
182 Arc::clone(&semaphore),
183 Arc::clone(&throughput),
184 challenge_resolver,
185 request_mutator,
186 );
187 let shutdown_rx_sched = shutdown_tx.subscribe();
188 let _scheduler_handle = tokio::spawn(async move {
189 sched.run(shutdown_rx_sched).await;
190 });
191
192 let _persist_handle = if config.state_file.is_some() {
194 let persist_config = Arc::clone(&config);
195 let persist_health = Arc::clone(&health);
196 let persist_proxy_mgr = Arc::clone(&proxy_manager);
197 let mut shutdown_rx_persist = shutdown_tx.subscribe();
198 let interval = config.state_save_interval;
199
200 Some(tokio::spawn(async move {
201 loop {
202 tokio::select! {
203 _ = shutdown_rx_persist.recv() => {
204 debug!("persist task received shutdown signal");
205 break;
206 }
207 _ = tokio::time::sleep(interval) => {
208 if let Some(ref path) = persist_config.state_file {
209 let health_stats = persist_health.get_all_stats();
210 let proxy_states = build_proxy_states(&persist_proxy_mgr);
211 let request_counts = build_request_counts(&persist_proxy_mgr);
212 if let Err(e) = persist::save_state(path, &health_stats, &proxy_states, &request_counts).await {
213 warn!(error = %e, "failed to save state");
214 } else {
215 debug!("persisted state to disk");
216 }
217 }
218 }
219 }
220 }
221 }))
222 } else {
223 None
224 };
225
226 let _metrics_config = Arc::clone(&config);
228 let metrics_task_pool = Arc::clone(&task_pool);
229 let metrics_health = Arc::clone(&health);
230 let metrics_proxy_mgr = Arc::clone(&proxy_manager);
231 let metrics_throughput = Arc::clone(&throughput);
232 let metrics_semaphore = Arc::clone(&semaphore);
233 let mut shutdown_rx_metrics = shutdown_tx.subscribe();
234 let metrics_interval = config.metrics_log_interval;
235 let max_inflight = config.max_inflight;
236
237 let _metrics_handle = tokio::spawn(async move {
238 let prefix = metrics_name
239 .as_deref()
240 .map_or_else(String::new, |n| format!("[{n}] "));
241 loop {
242 tokio::select! {
243 _ = shutdown_rx_metrics.recv() => {
244 debug!("metrics task received shutdown signal");
245 break;
246 }
247 _ = tokio::time::sleep(metrics_interval) => {
248 let tp = metrics_throughput.throughput(Duration::from_secs(10));
249 let total_s = metrics_health.total_success();
250 let total_f = metrics_health.total_fail();
251 let total = total_s + total_f;
252 let success_pct = if total > 0 {
253 (total_s as f64 / total as f64) * 100.0
254 } else {
255 0.0
256 };
257 let (_, healthy, cooldown, dead, retired) = metrics_proxy_mgr.proxy_counts();
258 let pending = metrics_task_pool.pending_count();
259 let delayed = metrics_task_pool.delayed_count();
260 let done = metrics_task_pool.completed_count();
261 let failed = metrics_task_pool.failed_count();
262 let requeued = metrics_task_pool.requeued_count();
263 let zero_available = metrics_task_pool.zero_available_count();
264 let skipped_no_permit = metrics_task_pool.skipped_no_permit_count();
265 let skipped_rate_limit = metrics_task_pool.skipped_rate_limit_count();
266 let skipped_cooldown = metrics_task_pool.skipped_cooldown_count();
267 let dispatches = metrics_task_pool.dispatch_count();
268 let inflight = max_inflight - metrics_semaphore.available_permits();
269 info!(
270 "{prefix}throughput={:.1}/s | success={:.0}% | pool: {} healthy / {} cooldown / {} dead / {} retired | tasks: {} pending {} delayed {} done {} failed requeued={} zero_avail={} dispatch={} skip(no_permit/rate/cooldown)={}/{}/{} | inflight={}",
271 tp,
272 success_pct,
273 healthy,
274 cooldown,
275 dead,
276 retired,
277 pending,
278 delayed,
279 done,
280 failed,
281 requeued,
282 zero_available,
283 dispatches,
284 skipped_no_permit,
285 skipped_rate_limit,
286 skipped_cooldown,
287 inflight
288 );
289 }
290 }
291 }
292 });
293
294 let effective_sources = Arc::new(effective_sources);
296 let refresh_sources = Arc::clone(&effective_sources);
297 let refresh_config = Arc::clone(&config);
298 let refresh_proxy_mgr = Arc::clone(&proxy_manager);
299 let mut shutdown_rx_refresh = shutdown_tx.subscribe();
300 let refresh_interval = config.source_refresh_interval;
301
302 let _refresh_handle = tokio::spawn(async move {
303 loop {
304 tokio::select! {
305 _ = shutdown_rx_refresh.recv() => {
306 debug!("refresh task received shutdown signal");
307 break;
308 }
309 _ = tokio::time::sleep(refresh_interval) => {
310 match refresh_proxy_mgr
311 .fetch_and_add(&refresh_sources, refresh_config.prefer_remote_dns)
312 .await
313 {
314 Ok(count) => {
315 let all = refresh_proxy_mgr.all_proxy_urls();
317 for url in &all {
318 if matches!(refresh_proxy_mgr.get_state(url), ProxyState::Retired) {
319 refresh_proxy_mgr.reset_request_count(url);
320 debug!(proxy = %url, "retired proxy reset via source refresh");
321 }
322 }
323 debug!(new_count = count, "refreshed proxy sources");
324 }
325 Err(e) => {
326 warn!(error = %e, "failed to refresh proxy sources");
327 }
328 }
329 }
330 }
331 }
332 });
333
334 Ok(ScatterProxy {
335 config,
336 task_pool,
337 health,
338 rate_limiter,
339 proxy_manager,
340 throughput,
341 semaphore,
342 shutdown_tx,
343 _scheduler_handle,
344 _persist_handle,
345 _metrics_handle,
346 _refresh_handle,
347 })
348 }
349
350 pub async fn submit(&self, request: reqwest::Request) -> TaskHandle {
357 self.task_pool.submit(request).await
358 }
359
360 pub fn try_submit(&self, request: reqwest::Request) -> Result<TaskHandle, ScatterProxyError> {
363 self.task_pool.try_submit(request)
364 }
365
366 pub async fn submit_batch(&self, requests: Vec<reqwest::Request>) -> Vec<TaskHandle> {
369 self.task_pool.submit_batch(requests).await
370 }
371
372 pub fn try_submit_batch(
375 &self,
376 requests: Vec<reqwest::Request>,
377 ) -> Result<Vec<TaskHandle>, ScatterProxyError> {
378 self.task_pool.try_submit_batch(requests)
379 }
380
381 pub async fn submit_timeout(
388 &self,
389 request: reqwest::Request,
390 timeout: Duration,
391 ) -> Result<TaskHandle, ScatterProxyError> {
392 self.task_pool.submit_timeout(request, timeout).await
393 }
394
395 pub fn metrics(&self) -> PoolMetrics {
397 let (total, healthy, cooldown, dead, retired) = self.proxy_manager.proxy_counts();
398
399 let total_s = self.health.total_success();
400 let total_f = self.health.total_fail();
401 let total_requests = total_s + total_f;
402 let success_rate = if total_requests > 0 {
403 total_s as f64 / total_requests as f64
404 } else {
405 0.0
406 };
407
408 PoolMetrics {
409 total_proxies: total,
410 healthy_proxies: healthy,
411 cooldown_proxies: cooldown,
412 dead_proxies: dead,
413 retired_proxies: retired,
414
415 pending_tasks: self.task_pool.pending_count(),
416 delayed_tasks: self.task_pool.delayed_count(),
417 completed_tasks: self.task_pool.completed_count(),
418 failed_tasks: self.task_pool.failed_count(),
419
420 throughput_1s: self.throughput.throughput(Duration::from_secs(1)),
421 throughput_10s: self.throughput.throughput(Duration::from_secs(10)),
422 throughput_60s: self.throughput.throughput(Duration::from_secs(60)),
423
424 success_rate_1m: success_rate,
425 avg_latency_ms: self.health.avg_latency_ms(),
426
427 inflight: self.config.max_inflight - self.semaphore.available_permits(),
428 requeued_tasks: self.task_pool.requeued_count(),
429 zero_available_events: self.task_pool.zero_available_count(),
430 skipped_no_permit: self.task_pool.skipped_no_permit_count(),
431 skipped_rate_limit: self.task_pool.skipped_rate_limit_count(),
432 skipped_cooldown: self.task_pool.skipped_cooldown_count(),
433 dispatch_count: self.task_pool.dispatch_count(),
434 }
435 }
436
437 pub async fn shutdown(self) {
440 info!("initiating scatter-proxy shutdown");
441
442 let _ = self.shutdown_tx.send(());
444
445 if let Some(ref path) = self.config.state_file {
447 let health_stats = self.health.get_all_stats();
448 let proxy_states = build_proxy_states(&self.proxy_manager);
449 let request_counts = build_request_counts(&self.proxy_manager);
450 if let Err(e) =
451 persist::save_state(path, &health_stats, &proxy_states, &request_counts).await
452 {
453 warn!(error = %e, "failed to save final state during shutdown");
454 } else {
455 debug!("saved final state to disk");
456 }
457 }
458
459 info!("scatter-proxy shutdown complete");
460 }
461}
462
463fn build_proxy_states(proxy_manager: &proxy::ProxyManager) -> HashMap<String, String> {
465 let urls = proxy_manager.all_proxy_urls();
466 let mut states = HashMap::with_capacity(urls.len());
467 for url in urls {
468 let state = proxy_manager.get_state(&url);
469 let label = match state {
470 proxy::ProxyState::Active => "Active",
471 proxy::ProxyState::Dead => "Dead",
472 proxy::ProxyState::Retired => "Retired",
473 proxy::ProxyState::Unknown => "Unknown",
474 };
475 states.insert(url, label.to_string());
476 }
477 states
478}
479
480fn build_request_counts(proxy_manager: &proxy::ProxyManager) -> HashMap<String, u32> {
482 let urls = proxy_manager.all_proxy_urls();
483 let mut counts = HashMap::with_capacity(urls.len());
484 for url in urls {
485 let count = proxy_manager.request_count(&url);
486 if count > 0 {
487 counts.insert(url, count);
488 }
489 }
490 counts
491}
492
493impl ScatterProxy {
496 pub(crate) async fn new_arc(
501 config: ScatterProxyConfig,
502 classifier: Arc<dyn BodyClassifier>,
503 ) -> Result<Self, ScatterProxyError> {
504 Self::new_with_extensions(
505 config,
506 ArcClassifier(classifier),
507 None::<NoOpResolver>,
508 None::<NoOpMutator>,
509 )
510 .await
511 }
512}
513
514struct ArcClassifier(Arc<dyn BodyClassifier>);
516impl classifier::BodyClassifier for ArcClassifier {
517 fn classify(
518 &self,
519 status: http::StatusCode,
520 headers: &http::HeaderMap,
521 body: &[u8],
522 ) -> classifier::BodyVerdict {
523 self.0.classify(status, headers, body)
524 }
525}
526
527struct NoOpResolver;
530impl ChallengeResolver for NoOpResolver {
531 fn resolve<'a>(
532 &'a self,
533 _proxy_url: &'a str,
534 _body: &'a [u8],
535 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Option<String>> + Send + 'a>> {
536 Box::pin(async { None })
537 }
538}
539
540struct NoOpMutator;
541impl RequestMutator for NoOpMutator {
542 fn mutate(&self, _proxy_url: &str, req: reqwest::Request) -> reqwest::Request {
543 req
544 }
545}