scatter-proxy 0.1.0

Async request scheduler for unreliable SOCKS5 proxies — multi-path race for maximum throughput
Documentation
use std::fmt;
use std::time::Duration;

/// Errors produced by the ScatterProxy scheduler.
#[derive(Debug)]
pub enum ScatterProxyError {
    /// The target host's circuit breaker is open — no proxies are being sent to it.
    CircuitOpen { host: String },
    /// The task was scheduled the maximum number of times without a successful response.
    MaxAttemptsExhausted {
        host: String,
        attempts: usize,
        last_error: String,
    },
    /// The overall task timeout elapsed before a successful response was obtained.
    Timeout { host: String, elapsed: Duration },
    /// The task pool is at capacity and cannot accept new submissions.
    PoolFull { capacity: usize },
    /// An error occurred during initialization (e.g. failed to fetch proxy sources).
    Init(String),
}

impl fmt::Display for ScatterProxyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ScatterProxyError::CircuitOpen { host } => {
                write!(f, "circuit breaker open for host '{host}'")
            }
            ScatterProxyError::MaxAttemptsExhausted {
                host,
                attempts,
                last_error,
            } => {
                write!(
                    f,
                    "max attempts exhausted for host '{host}' after {attempts} attempt(s): {last_error}"
                )
            }
            ScatterProxyError::Timeout { host, elapsed } => {
                write!(
                    f,
                    "task timeout for host '{host}' after {:.1}s",
                    elapsed.as_secs_f64()
                )
            }
            ScatterProxyError::PoolFull { capacity } => {
                write!(f, "task pool is full (capacity: {capacity})")
            }
            ScatterProxyError::Init(reason) => {
                write!(f, "initialization error: {reason}")
            }
        }
    }
}

impl std::error::Error for ScatterProxyError {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_circuit_open() {
        let err = ScatterProxyError::CircuitOpen {
            host: "example.com".into(),
        };
        let msg = err.to_string();
        assert!(msg.contains("circuit breaker open"));
        assert!(msg.contains("example.com"));
    }

    #[test]
    fn display_max_attempts_exhausted() {
        let err = ScatterProxyError::MaxAttemptsExhausted {
            host: "api.example.com".into(),
            attempts: 5,
            last_error: "connection refused".into(),
        };
        let msg = err.to_string();
        assert!(msg.contains("api.example.com"));
        assert!(msg.contains("5 attempt(s)"));
        assert!(msg.contains("connection refused"));
    }

    #[test]
    fn display_timeout() {
        let err = ScatterProxyError::Timeout {
            host: "slow.example.com".into(),
            elapsed: Duration::from_millis(8500),
        };
        let msg = err.to_string();
        assert!(msg.contains("slow.example.com"));
        assert!(msg.contains("8.5s"));
    }

    #[test]
    fn display_pool_full() {
        let err = ScatterProxyError::PoolFull { capacity: 1000 };
        let msg = err.to_string();
        assert!(msg.contains("task pool is full"));
        assert!(msg.contains("1000"));
    }

    #[test]
    fn display_init() {
        let err = ScatterProxyError::Init("failed to fetch proxy list".into());
        let msg = err.to_string();
        assert!(msg.contains("initialization error"));
        assert!(msg.contains("failed to fetch proxy list"));
    }

    #[test]
    fn error_trait_is_implemented() {
        let err: Box<dyn std::error::Error> = Box::new(ScatterProxyError::Init("test".into()));
        // source() should return None for our simple error variants.
        assert!(err.source().is_none());
    }

    #[test]
    fn debug_format_includes_variant_name() {
        let err = ScatterProxyError::CircuitOpen { host: "h".into() };
        let dbg = format!("{err:?}");
        assert!(dbg.contains("CircuitOpen"));
    }

    #[test]
    fn timeout_sub_second_formatting() {
        let err = ScatterProxyError::Timeout {
            host: "h".into(),
            elapsed: Duration::from_millis(200),
        };
        assert!(err.to_string().contains("0.2s"));
    }

    #[test]
    fn timeout_exact_seconds_formatting() {
        let err = ScatterProxyError::Timeout {
            host: "h".into(),
            elapsed: Duration::from_secs(60),
        };
        assert!(err.to_string().contains("60.0s"));
    }

    #[test]
    fn max_attempts_with_one_attempt() {
        let err = ScatterProxyError::MaxAttemptsExhausted {
            host: "h".into(),
            attempts: 1,
            last_error: "err".into(),
        };
        assert!(err.to_string().contains("1 attempt(s)"));
    }
}