mockforge_core/
chaos_utilities.rs

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