Skip to main content

mabi_chaos/protocol/
timeout.rs

1//! Protocol timeout fault injection.
2
3use std::any::Any;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use rand::prelude::*;
8use serde::{Deserialize, Serialize};
9
10use crate::context::FaultContext;
11use crate::error::ChaosResult;
12use crate::fault::{
13    BaseFault, Fault, FaultBehavior, FaultCategory, FaultMetadata, FaultSeverity, FaultStatistics,
14};
15
16// =============================================================================
17// Timeout Pattern
18// =============================================================================
19
20/// Pattern for timeout behavior.
21#[derive(Debug, Clone, Serialize, Deserialize, Default)]
22#[serde(rename_all = "snake_case")]
23pub enum TimeoutPattern {
24    /// No response at all.
25    #[default]
26    NoResponse,
27
28    /// Partial response then timeout.
29    PartialResponse,
30
31    /// Response after timeout period.
32    DelayedResponse,
33
34    /// Intermittent (sometimes works, sometimes times out).
35    Intermittent { success_rate: f64 },
36}
37
38// =============================================================================
39// Timeout Configuration
40// =============================================================================
41
42/// Configuration for timeout fault.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct TimeoutConfig {
45    /// Timeout duration in milliseconds.
46    pub timeout_ms: u64,
47
48    /// Timeout pattern.
49    pub pattern: TimeoutPattern,
50
51    /// Error message for timeout.
52    #[serde(default = "default_timeout_message")]
53    pub error_message: String,
54
55    /// Whether to actually wait for timeout duration.
56    #[serde(default = "default_true")]
57    pub wait_duration: bool,
58}
59
60fn default_timeout_message() -> String {
61    "Request timed out".to_string()
62}
63
64fn default_true() -> bool {
65    true
66}
67
68impl Default for TimeoutConfig {
69    fn default() -> Self {
70        Self {
71            timeout_ms: 5000,
72            pattern: TimeoutPattern::NoResponse,
73            error_message: default_timeout_message(),
74            wait_duration: true,
75        }
76    }
77}
78
79impl TimeoutConfig {
80    /// Create a simple timeout config.
81    pub fn simple(timeout_ms: u64) -> Self {
82        Self {
83            timeout_ms,
84            ..Default::default()
85        }
86    }
87
88    /// Create an intermittent timeout config.
89    pub fn intermittent(timeout_ms: u64, success_rate: f64) -> Self {
90        Self {
91            timeout_ms,
92            pattern: TimeoutPattern::Intermittent {
93                success_rate: success_rate.clamp(0.0, 1.0),
94            },
95            ..Default::default()
96        }
97    }
98
99    /// Create a delayed response config.
100    pub fn delayed(timeout_ms: u64) -> Self {
101        Self {
102            timeout_ms,
103            pattern: TimeoutPattern::DelayedResponse,
104            ..Default::default()
105        }
106    }
107
108    /// Don't actually wait for timeout.
109    pub fn no_wait(mut self) -> Self {
110        self.wait_duration = false;
111        self
112    }
113
114    /// Set error message.
115    pub fn with_message(mut self, message: impl Into<String>) -> Self {
116        self.error_message = message.into();
117        self
118    }
119}
120
121// =============================================================================
122// Timeout Fault
123// =============================================================================
124
125/// Protocol timeout fault implementation.
126///
127/// Simulates request timeouts at the protocol level.
128///
129/// # Example
130///
131/// ```rust,ignore
132/// use mabi_chaos::protocol::TimeoutFault;
133///
134/// // Simple 5 second timeout
135/// let fault = TimeoutFault::builder()
136///     .id("timeout-001")
137///     .timeout_ms(5000)
138///     .build();
139///
140/// // Intermittent timeouts (70% success rate)
141/// let fault = TimeoutFault::builder()
142///     .id("timeout-002")
143///     .intermittent(3000, 0.7)
144///     .build();
145/// ```
146#[derive(Debug, Clone)]
147pub struct TimeoutFault {
148    base: BaseFault,
149    config: TimeoutConfig,
150    rng: StdRng,
151}
152
153impl TimeoutFault {
154    /// Create a new timeout fault.
155    pub fn new(id: impl Into<String>, config: TimeoutConfig) -> Self {
156        let id = id.into();
157        let metadata = FaultMetadata::new(&id, "Protocol Timeout", FaultCategory::Protocol)
158            .with_description("Simulates request timeouts")
159            .with_severity(FaultSeverity::High);
160
161        Self {
162            base: BaseFault::new(metadata),
163            config,
164            rng: StdRng::from_entropy(),
165        }
166    }
167
168    /// Create a builder.
169    pub fn builder() -> TimeoutFaultBuilder {
170        TimeoutFaultBuilder::default()
171    }
172
173    /// Check if should timeout based on pattern.
174    fn should_timeout(&mut self) -> bool {
175        match &self.config.pattern {
176            TimeoutPattern::NoResponse | TimeoutPattern::PartialResponse => true,
177            TimeoutPattern::DelayedResponse => true,
178            TimeoutPattern::Intermittent { success_rate } => {
179                self.rng.gen::<f64>() > *success_rate
180            }
181        }
182    }
183
184    /// Get configuration.
185    pub fn config(&self) -> &TimeoutConfig {
186        &self.config
187    }
188}
189
190#[async_trait]
191impl Fault for TimeoutFault {
192    fn metadata(&self) -> &FaultMetadata {
193        &self.base.metadata
194    }
195
196    fn enable(&mut self) {
197        self.base.enable();
198    }
199
200    fn disable(&mut self) {
201        self.base.disable();
202    }
203
204    async fn should_activate(&self, ctx: &FaultContext) -> ChaosResult<bool> {
205        if !self.base.matches_target(ctx.target.identifier()) {
206            return Ok(false);
207        }
208        Ok(true)
209    }
210
211    async fn apply(&self, ctx: &mut FaultContext) -> ChaosResult<FaultBehavior> {
212        let should_timeout = {
213            let mut this = self.clone();
214            if !this.base.check_probability() {
215                return Ok(FaultBehavior::Continue);
216            }
217            this.should_timeout()
218        };
219
220        if !should_timeout {
221            return Ok(FaultBehavior::Continue);
222        }
223
224        ctx.record_applied_fault(self.id(), "timeout");
225
226        tracing::debug!(
227            fault_id = %self.id(),
228            target = %ctx.target.device_id,
229            timeout_ms = %self.config.timeout_ms,
230            pattern = ?self.config.pattern,
231            "Simulating timeout"
232        );
233
234        // Wait if configured
235        if self.config.wait_duration {
236            let wait_time = match &self.config.pattern {
237                TimeoutPattern::PartialResponse => self.config.timeout_ms / 2,
238                TimeoutPattern::DelayedResponse => self.config.timeout_ms,
239                _ => self.config.timeout_ms,
240            };
241            ctx.add_delay(wait_time);
242            tokio::time::sleep(Duration::from_millis(wait_time)).await;
243        }
244
245        match &self.config.pattern {
246            TimeoutPattern::DelayedResponse => {
247                // Allow the response after delay
248                Ok(FaultBehavior::Delay {
249                    duration_ms: self.config.timeout_ms,
250                })
251            }
252            _ => {
253                Ok(FaultBehavior::Abort {
254                    error: self.config.error_message.clone(),
255                })
256            }
257        }
258    }
259
260    fn reset(&mut self) {
261        self.base.reset();
262    }
263
264    fn statistics(&self) -> FaultStatistics {
265        self.base.statistics.clone()
266    }
267
268    fn clone_box(&self) -> Box<dyn Fault> {
269        Box::new(self.clone())
270    }
271
272    fn as_any(&self) -> &dyn Any {
273        self
274    }
275
276    fn as_any_mut(&mut self) -> &mut dyn Any {
277        self
278    }
279}
280
281// =============================================================================
282// Builder
283// =============================================================================
284
285/// Builder for timeout fault.
286#[derive(Debug, Default)]
287pub struct TimeoutFaultBuilder {
288    id: Option<String>,
289    name: Option<String>,
290    config: TimeoutConfig,
291    probability: f64,
292    targets: Vec<String>,
293    severity: FaultSeverity,
294}
295
296impl TimeoutFaultBuilder {
297    /// Set fault ID.
298    pub fn id(mut self, id: impl Into<String>) -> Self {
299        self.id = Some(id.into());
300        self
301    }
302
303    /// Set fault name.
304    pub fn name(mut self, name: impl Into<String>) -> Self {
305        self.name = Some(name.into());
306        self
307    }
308
309    /// Set timeout duration.
310    pub fn timeout_ms(mut self, ms: u64) -> Self {
311        self.config.timeout_ms = ms;
312        self
313    }
314
315    /// Set pattern.
316    pub fn pattern(mut self, pattern: TimeoutPattern) -> Self {
317        self.config.pattern = pattern;
318        self
319    }
320
321    /// Set intermittent pattern.
322    pub fn intermittent(mut self, timeout_ms: u64, success_rate: f64) -> Self {
323        self.config.timeout_ms = timeout_ms;
324        self.config.pattern = TimeoutPattern::Intermittent {
325            success_rate: success_rate.clamp(0.0, 1.0),
326        };
327        self
328    }
329
330    /// Set delayed response pattern.
331    pub fn delayed(mut self) -> Self {
332        self.config.pattern = TimeoutPattern::DelayedResponse;
333        self
334    }
335
336    /// Don't wait for timeout.
337    pub fn no_wait(mut self) -> Self {
338        self.config.wait_duration = false;
339        self
340    }
341
342    /// Set error message.
343    pub fn message(mut self, message: impl Into<String>) -> Self {
344        self.config.error_message = message.into();
345        self
346    }
347
348    /// Set probability.
349    pub fn probability(mut self, p: f64) -> Self {
350        self.probability = p.clamp(0.0, 1.0);
351        self
352    }
353
354    /// Add target pattern.
355    pub fn target(mut self, target: impl Into<String>) -> Self {
356        self.targets.push(target.into());
357        self
358    }
359
360    /// Set severity.
361    pub fn severity(mut self, severity: FaultSeverity) -> Self {
362        self.severity = severity;
363        self
364    }
365
366    /// Build the fault.
367    pub fn build(self) -> TimeoutFault {
368        let id = self.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
369        let mut fault = TimeoutFault::new(&id, self.config);
370
371        if let Some(name) = self.name {
372            fault.base.metadata.name = name;
373        }
374
375        fault.base.metadata.probability = if self.probability == 0.0 {
376            1.0
377        } else {
378            self.probability
379        };
380        fault.base.metadata.targets = self.targets;
381        fault.base.metadata.severity = self.severity;
382
383        fault
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_always_timeout() {
393        let config = TimeoutConfig::simple(100);
394        let mut fault = TimeoutFault::new("test", config);
395
396        assert!(fault.should_timeout());
397    }
398
399    #[test]
400    fn test_intermittent_timeout() {
401        let config = TimeoutConfig::intermittent(100, 0.5);
402        let mut fault = TimeoutFault::new("test", config);
403
404        let mut timeouts = 0;
405        let iterations = 1000;
406
407        for _ in 0..iterations {
408            if fault.should_timeout() {
409                timeouts += 1;
410            }
411        }
412
413        // Should be approximately 50% timeouts
414        let ratio = timeouts as f64 / iterations as f64;
415        assert!(ratio > 0.4 && ratio < 0.6, "ratio was {}", ratio);
416    }
417
418    #[test]
419    fn test_builder() {
420        let fault = TimeoutFault::builder()
421            .id("timeout-001")
422            .timeout_ms(3000)
423            .intermittent(3000, 0.8)
424            .no_wait()
425            .message("Custom timeout message")
426            .target("device-*")
427            .build();
428
429        assert_eq!(fault.id(), "timeout-001");
430        assert_eq!(fault.config.timeout_ms, 3000);
431        assert!(!fault.config.wait_duration);
432    }
433}