Skip to main content

scatter_proxy/
error.rs

1use std::fmt;
2use std::time::Duration;
3
4/// Errors produced by the ScatterProxy scheduler.
5#[derive(Debug)]
6pub enum ScatterProxyError {
7    /// The target host's circuit breaker is open — no proxies are being sent to it.
8    CircuitOpen { host: String },
9    /// The task was scheduled the maximum number of times without a successful response.
10    MaxAttemptsExhausted {
11        host: String,
12        attempts: usize,
13        last_error: String,
14    },
15    /// The overall task timeout elapsed before a successful response was obtained.
16    Timeout { host: String, elapsed: Duration },
17    /// The task pool is at capacity and cannot accept new submissions.
18    PoolFull { capacity: usize },
19    /// An error occurred during initialization (e.g. failed to fetch proxy sources).
20    Init(String),
21}
22
23impl fmt::Display for ScatterProxyError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            ScatterProxyError::CircuitOpen { host } => {
27                write!(f, "circuit breaker open for host '{host}'")
28            }
29            ScatterProxyError::MaxAttemptsExhausted {
30                host,
31                attempts,
32                last_error,
33            } => {
34                write!(
35                    f,
36                    "max attempts exhausted for host '{host}' after {attempts} attempt(s): {last_error}"
37                )
38            }
39            ScatterProxyError::Timeout { host, elapsed } => {
40                write!(
41                    f,
42                    "task timeout for host '{host}' after {:.1}s",
43                    elapsed.as_secs_f64()
44                )
45            }
46            ScatterProxyError::PoolFull { capacity } => {
47                write!(f, "task pool is full (capacity: {capacity})")
48            }
49            ScatterProxyError::Init(reason) => {
50                write!(f, "initialization error: {reason}")
51            }
52        }
53    }
54}
55
56impl std::error::Error for ScatterProxyError {}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn display_circuit_open() {
64        let err = ScatterProxyError::CircuitOpen {
65            host: "example.com".into(),
66        };
67        let msg = err.to_string();
68        assert!(msg.contains("circuit breaker open"));
69        assert!(msg.contains("example.com"));
70    }
71
72    #[test]
73    fn display_max_attempts_exhausted() {
74        let err = ScatterProxyError::MaxAttemptsExhausted {
75            host: "api.example.com".into(),
76            attempts: 5,
77            last_error: "connection refused".into(),
78        };
79        let msg = err.to_string();
80        assert!(msg.contains("api.example.com"));
81        assert!(msg.contains("5 attempt(s)"));
82        assert!(msg.contains("connection refused"));
83    }
84
85    #[test]
86    fn display_timeout() {
87        let err = ScatterProxyError::Timeout {
88            host: "slow.example.com".into(),
89            elapsed: Duration::from_millis(8500),
90        };
91        let msg = err.to_string();
92        assert!(msg.contains("slow.example.com"));
93        assert!(msg.contains("8.5s"));
94    }
95
96    #[test]
97    fn display_pool_full() {
98        let err = ScatterProxyError::PoolFull { capacity: 1000 };
99        let msg = err.to_string();
100        assert!(msg.contains("task pool is full"));
101        assert!(msg.contains("1000"));
102    }
103
104    #[test]
105    fn display_init() {
106        let err = ScatterProxyError::Init("failed to fetch proxy list".into());
107        let msg = err.to_string();
108        assert!(msg.contains("initialization error"));
109        assert!(msg.contains("failed to fetch proxy list"));
110    }
111
112    #[test]
113    fn error_trait_is_implemented() {
114        let err: Box<dyn std::error::Error> = Box::new(ScatterProxyError::Init("test".into()));
115        // source() should return None for our simple error variants.
116        assert!(err.source().is_none());
117    }
118
119    #[test]
120    fn debug_format_includes_variant_name() {
121        let err = ScatterProxyError::CircuitOpen { host: "h".into() };
122        let dbg = format!("{err:?}");
123        assert!(dbg.contains("CircuitOpen"));
124    }
125
126    #[test]
127    fn timeout_sub_second_formatting() {
128        let err = ScatterProxyError::Timeout {
129            host: "h".into(),
130            elapsed: Duration::from_millis(200),
131        };
132        assert!(err.to_string().contains("0.2s"));
133    }
134
135    #[test]
136    fn timeout_exact_seconds_formatting() {
137        let err = ScatterProxyError::Timeout {
138            host: "h".into(),
139            elapsed: Duration::from_secs(60),
140        };
141        assert!(err.to_string().contains("60.0s"));
142    }
143
144    #[test]
145    fn max_attempts_with_one_attempt() {
146        let err = ScatterProxyError::MaxAttemptsExhausted {
147            host: "h".into(),
148            attempts: 1,
149            last_error: "err".into(),
150        };
151        assert!(err.to_string().contains("1 attempt(s)"));
152    }
153}