mockforge_core/
chaos_utilities.rs

1//! Chaos testing utilities with orchestration and randomness
2//!
3//! This module provides high-level chaos testing utilities that randomly inject
4//! errors, delays, and other failures. It builds on the existing latency and
5//! failure injection systems to provide easy-to-use chaos testing capabilities.
6
7use crate::failure_injection::{FailureConfig, FailureInjector};
8use crate::latency::{FaultConfig, LatencyInjector, LatencyProfile};
9use rand::Rng;
10use serde::{Deserialize, Serialize};
11use std::sync::Arc;
12use tokio::sync::RwLock;
13
14/// Chaos mode configuration
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ChaosConfig {
17    /// Enable chaos mode
18    pub enabled: bool,
19    /// Error injection rate (0.0 to 1.0)
20    pub error_rate: f64,
21    /// Delay injection rate (0.0 to 1.0)
22    pub delay_rate: f64,
23    /// Minimum delay in milliseconds
24    pub min_delay_ms: u64,
25    /// Maximum delay in milliseconds
26    pub max_delay_ms: u64,
27    /// Status codes to randomly inject
28    pub status_codes: Vec<u16>,
29    /// Whether to inject random timeouts
30    pub inject_timeouts: bool,
31    /// Timeout duration in milliseconds
32    pub timeout_ms: u64,
33}
34
35impl Default for ChaosConfig {
36    fn default() -> Self {
37        Self {
38            enabled: false,
39            error_rate: 0.1, // 10% error rate
40            delay_rate: 0.3, // 30% delay rate
41            min_delay_ms: 100,
42            max_delay_ms: 2000,
43            status_codes: vec![500, 502, 503, 504],
44            inject_timeouts: false,
45            timeout_ms: 5000,
46        }
47    }
48}
49
50impl ChaosConfig {
51    /// Create a new chaos config with custom error and delay rates
52    pub fn new(error_rate: f64, delay_rate: f64) -> Self {
53        Self {
54            enabled: true,
55            error_rate: error_rate.clamp(0.0, 1.0),
56            delay_rate: delay_rate.clamp(0.0, 1.0),
57            ..Default::default()
58        }
59    }
60
61    /// Configure the delay range
62    pub fn with_delay_range(mut self, min_ms: u64, max_ms: u64) -> Self {
63        self.min_delay_ms = min_ms;
64        self.max_delay_ms = max_ms;
65        self
66    }
67
68    /// Configure status codes to inject
69    pub fn with_status_codes(mut self, codes: Vec<u16>) -> Self {
70        self.status_codes = codes;
71        self
72    }
73
74    /// Enable timeout injection
75    pub fn with_timeouts(mut self, timeout_ms: u64) -> Self {
76        self.inject_timeouts = true;
77        self.timeout_ms = timeout_ms;
78        self
79    }
80}
81
82/// Chaos engine that orchestrates random failure injection
83#[derive(Debug, Clone)]
84pub struct ChaosEngine {
85    config: Arc<RwLock<ChaosConfig>>,
86    latency_injector: Arc<RwLock<LatencyInjector>>,
87    failure_injector: Arc<RwLock<FailureInjector>>,
88}
89
90impl ChaosEngine {
91    /// Create a new chaos engine
92    pub fn new(config: ChaosConfig) -> Self {
93        // Create latency injector from config
94        let latency_profile = LatencyProfile::new(
95            (config.min_delay_ms + config.max_delay_ms) / 2,
96            (config.max_delay_ms - config.min_delay_ms) / 2,
97        );
98
99        let fault_config = FaultConfig {
100            failure_rate: config.error_rate,
101            status_codes: config.status_codes.clone(),
102            error_responses: Default::default(),
103        };
104
105        let latency_injector = LatencyInjector::new(latency_profile, fault_config);
106
107        // Create failure injector from config
108        let failure_config = FailureConfig {
109            global_error_rate: config.error_rate,
110            default_status_codes: config.status_codes.clone(),
111            tag_configs: Default::default(),
112            include_tags: Vec::new(),
113            exclude_tags: Vec::new(),
114        };
115
116        let failure_injector = FailureInjector::new(Some(failure_config), config.enabled);
117
118        Self {
119            config: Arc::new(RwLock::new(config)),
120            latency_injector: Arc::new(RwLock::new(latency_injector)),
121            failure_injector: Arc::new(RwLock::new(failure_injector)),
122        }
123    }
124
125    /// Check if chaos mode is enabled
126    pub async fn is_enabled(&self) -> bool {
127        self.config.read().await.enabled
128    }
129
130    /// Enable or disable chaos mode
131    pub async fn set_enabled(&self, enabled: bool) {
132        let mut config = self.config.write().await;
133        config.enabled = enabled;
134
135        // Update injectors
136        let mut latency = self.latency_injector.write().await;
137        latency.set_enabled(enabled);
138
139        let mut failure = self.failure_injector.write().await;
140        failure.set_enabled(enabled);
141    }
142
143    /// Update chaos configuration
144    pub async fn update_config(&self, new_config: ChaosConfig) {
145        let mut config = self.config.write().await;
146        *config = new_config.clone();
147
148        // Update injectors with new config
149        let latency_profile = LatencyProfile::new(
150            (new_config.min_delay_ms + new_config.max_delay_ms) / 2,
151            (new_config.max_delay_ms - new_config.min_delay_ms) / 2,
152        );
153
154        let fault_config = FaultConfig {
155            failure_rate: new_config.error_rate,
156            status_codes: new_config.status_codes.clone(),
157            error_responses: Default::default(),
158        };
159
160        let mut latency = self.latency_injector.write().await;
161        *latency = LatencyInjector::new(latency_profile, fault_config);
162        latency.set_enabled(new_config.enabled);
163
164        let failure_config = FailureConfig {
165            global_error_rate: new_config.error_rate,
166            default_status_codes: new_config.status_codes.clone(),
167            tag_configs: Default::default(),
168            include_tags: Vec::new(),
169            exclude_tags: Vec::new(),
170        };
171
172        let mut failure = self.failure_injector.write().await;
173        failure.update_config(Some(failure_config));
174        failure.set_enabled(new_config.enabled);
175    }
176
177    /// Process a request with random chaos injection
178    /// Returns Some((status_code, error_message)) if an error should be injected
179    pub async fn process_request(&self, _tags: &[String]) -> ChaosResult {
180        let config = self.config.read().await;
181
182        if !config.enabled {
183            return ChaosResult::Success;
184        }
185
186        let mut rng = rand::rng();
187
188        // First, randomly decide if we should inject an error
189        if rng.random_bool(config.error_rate) {
190            let status_code = if config.status_codes.is_empty() {
191                500
192            } else {
193                let index = rng.random_range(0..config.status_codes.len());
194                config.status_codes[index]
195            };
196
197            return ChaosResult::Error {
198                status_code,
199                message: format!("Chaos-injected error (rate: {:.1}%)", config.error_rate * 100.0),
200            };
201        }
202
203        // Then, randomly decide if we should inject a delay
204        if rng.random_bool(config.delay_rate) {
205            let delay_ms = rng.random_range(config.min_delay_ms..=config.max_delay_ms);
206            return ChaosResult::Delay { delay_ms };
207        }
208
209        // Finally, check for timeout injection
210        if config.inject_timeouts && rng.random_bool(0.05) {
211            // 5% chance of timeout
212            return ChaosResult::Timeout {
213                timeout_ms: config.timeout_ms,
214            };
215        }
216
217        ChaosResult::Success
218    }
219
220    /// Inject latency for a request (respects delay_rate)
221    pub async fn inject_latency(&self, tags: &[String]) -> crate::Result<()> {
222        let config = self.config.read().await;
223
224        if !config.enabled {
225            return Ok(());
226        }
227
228        let mut rng = rand::rng();
229
230        // Only inject latency based on delay_rate
231        if rng.random_bool(config.delay_rate) {
232            let latency = self.latency_injector.read().await;
233            latency.inject_latency(tags).await?;
234        }
235
236        Ok(())
237    }
238
239    /// Check if an error should be injected (respects error_rate)
240    pub async fn should_inject_error(&self, tags: &[String]) -> bool {
241        let config = self.config.read().await;
242
243        if !config.enabled {
244            return false;
245        }
246
247        let failure = self.failure_injector.read().await;
248        failure.should_inject_failure(tags)
249    }
250
251    /// Get a random error response
252    pub async fn get_error_response(&self) -> Option<(u16, String)> {
253        let config = self.config.read().await;
254
255        if !config.enabled {
256            return None;
257        }
258
259        let mut rng = rand::rng();
260        let status_code = if config.status_codes.is_empty() {
261            500
262        } else {
263            let index = rng.random_range(0..config.status_codes.len());
264            config.status_codes[index]
265        };
266
267        Some((
268            status_code,
269            format!("Chaos-injected error (rate: {:.1}%)", config.error_rate * 100.0),
270        ))
271    }
272
273    /// Get current chaos configuration
274    pub async fn get_config(&self) -> ChaosConfig {
275        self.config.read().await.clone()
276    }
277
278    /// Get chaos statistics
279    pub async fn get_statistics(&self) -> ChaosStatistics {
280        let config = self.config.read().await;
281        ChaosStatistics {
282            enabled: config.enabled,
283            error_rate: config.error_rate,
284            delay_rate: config.delay_rate,
285            min_delay_ms: config.min_delay_ms,
286            max_delay_ms: config.max_delay_ms,
287            inject_timeouts: config.inject_timeouts,
288        }
289    }
290}
291
292impl Default for ChaosEngine {
293    fn default() -> Self {
294        Self::new(ChaosConfig::default())
295    }
296}
297
298/// Result of chaos engineering evaluation, indicating what effect to apply
299#[derive(Debug, Clone)]
300pub enum ChaosResult {
301    /// No chaos effect - request should proceed normally
302    Success,
303    /// Inject an error response
304    Error {
305        /// HTTP status code for the error
306        status_code: u16,
307        /// Error message to include
308        message: String,
309    },
310    /// Inject a delay before processing
311    Delay {
312        /// Delay duration in milliseconds
313        delay_ms: u64,
314    },
315    /// Inject a timeout
316    Timeout {
317        /// Timeout duration in milliseconds
318        timeout_ms: u64,
319    },
320}
321
322/// Statistics for chaos engineering engine
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct ChaosStatistics {
325    /// Whether chaos engineering is enabled
326    pub enabled: bool,
327    /// Error injection rate (0.0 to 1.0)
328    pub error_rate: f64,
329    /// Delay injection rate (0.0 to 1.0)
330    pub delay_rate: f64,
331    /// Minimum delay in milliseconds
332    pub min_delay_ms: u64,
333    /// Maximum delay in milliseconds
334    pub max_delay_ms: u64,
335    /// Whether to inject timeouts
336    pub inject_timeouts: bool,
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_chaos_config_default() {
345        let config = ChaosConfig::default();
346        assert!(!config.enabled);
347        assert_eq!(config.error_rate, 0.1);
348        assert_eq!(config.delay_rate, 0.3);
349        assert!(!config.inject_timeouts);
350    }
351
352    #[test]
353    fn test_chaos_config_new() {
354        let config = ChaosConfig::new(0.2, 0.5);
355        assert!(config.enabled);
356        assert_eq!(config.error_rate, 0.2);
357        assert_eq!(config.delay_rate, 0.5);
358    }
359
360    #[test]
361    fn test_chaos_config_builder() {
362        let config = ChaosConfig::new(0.1, 0.2)
363            .with_delay_range(50, 500)
364            .with_status_codes(vec![500, 503])
365            .with_timeouts(3000);
366
367        assert_eq!(config.min_delay_ms, 50);
368        assert_eq!(config.max_delay_ms, 500);
369        assert_eq!(config.status_codes, vec![500, 503]);
370        assert!(config.inject_timeouts);
371        assert_eq!(config.timeout_ms, 3000);
372    }
373
374    #[tokio::test]
375    async fn test_chaos_engine_creation() {
376        let config = ChaosConfig::new(0.5, 0.5);
377        let engine = ChaosEngine::new(config);
378
379        assert!(engine.is_enabled().await);
380    }
381
382    #[tokio::test]
383    async fn test_chaos_engine_enable_disable() {
384        let config = ChaosConfig::new(0.5, 0.5);
385        let engine = ChaosEngine::new(config);
386
387        assert!(engine.is_enabled().await);
388
389        engine.set_enabled(false).await;
390        assert!(!engine.is_enabled().await);
391
392        engine.set_enabled(true).await;
393        assert!(engine.is_enabled().await);
394    }
395
396    #[tokio::test]
397    async fn test_chaos_engine_disabled_returns_success() {
398        let mut config = ChaosConfig::new(1.0, 1.0); // 100% rates
399        config.enabled = false;
400        let engine = ChaosEngine::new(config);
401
402        let result = engine.process_request(&[]).await;
403        assert!(matches!(result, ChaosResult::Success));
404    }
405
406    #[tokio::test]
407    async fn test_chaos_engine_high_error_rate() {
408        let config = ChaosConfig::new(1.0, 0.0) // 100% error rate, 0% delay rate
409            .with_status_codes(vec![503]);
410        let engine = ChaosEngine::new(config);
411
412        let mut error_count = 0;
413        for _ in 0..10 {
414            let result = engine.process_request(&[]).await;
415            if let ChaosResult::Error { status_code, .. } = result {
416                assert_eq!(status_code, 503);
417                error_count += 1;
418            }
419        }
420
421        // With 100% error rate, all requests should fail
422        assert_eq!(error_count, 10);
423    }
424
425    #[tokio::test]
426    async fn test_chaos_engine_high_delay_rate() {
427        let config = ChaosConfig::new(0.0, 1.0) // 0% error rate, 100% delay rate
428            .with_delay_range(100, 200);
429        let engine = ChaosEngine::new(config);
430
431        let mut delay_count = 0;
432        for _ in 0..10 {
433            let result = engine.process_request(&[]).await;
434            if let ChaosResult::Delay { delay_ms } = result {
435                assert!((100..=200).contains(&delay_ms));
436                delay_count += 1;
437            }
438        }
439
440        // With 100% delay rate, all requests should be delayed
441        assert_eq!(delay_count, 10);
442    }
443
444    #[tokio::test]
445    async fn test_chaos_engine_update_config() {
446        let config = ChaosConfig::new(0.5, 0.5);
447        let engine = ChaosEngine::new(config);
448
449        let new_config = ChaosConfig::new(0.2, 0.8).with_delay_range(50, 100);
450
451        engine.update_config(new_config).await;
452
453        let updated = engine.get_config().await;
454        assert_eq!(updated.error_rate, 0.2);
455        assert_eq!(updated.delay_rate, 0.8);
456        assert_eq!(updated.min_delay_ms, 50);
457        assert_eq!(updated.max_delay_ms, 100);
458    }
459
460    #[tokio::test]
461    async fn test_chaos_engine_statistics() {
462        let config = ChaosConfig::new(0.3, 0.4).with_delay_range(100, 500).with_timeouts(2000);
463
464        let engine = ChaosEngine::new(config);
465        let stats = engine.get_statistics().await;
466
467        assert!(stats.enabled);
468        assert_eq!(stats.error_rate, 0.3);
469        assert_eq!(stats.delay_rate, 0.4);
470        assert_eq!(stats.min_delay_ms, 100);
471        assert_eq!(stats.max_delay_ms, 500);
472        assert!(stats.inject_timeouts);
473    }
474
475    #[tokio::test]
476    async fn test_chaos_result_variants() {
477        let success = ChaosResult::Success;
478        assert!(matches!(success, ChaosResult::Success));
479
480        let error = ChaosResult::Error {
481            status_code: 500,
482            message: "Error".to_string(),
483        };
484        if let ChaosResult::Error { status_code, .. } = error {
485            assert_eq!(status_code, 500);
486        } else {
487            panic!("Expected Error variant");
488        }
489
490        let delay = ChaosResult::Delay { delay_ms: 100 };
491        if let ChaosResult::Delay { delay_ms } = delay {
492            assert_eq!(delay_ms, 100);
493        } else {
494            panic!("Expected Delay variant");
495        }
496    }
497}