1use 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
22#[serde(rename_all = "snake_case")]
23pub enum TimeoutPattern {
24 #[default]
26 NoResponse,
27
28 PartialResponse,
30
31 DelayedResponse,
33
34 Intermittent { success_rate: f64 },
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct TimeoutConfig {
45 pub timeout_ms: u64,
47
48 pub pattern: TimeoutPattern,
50
51 #[serde(default = "default_timeout_message")]
53 pub error_message: String,
54
55 #[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 pub fn simple(timeout_ms: u64) -> Self {
82 Self {
83 timeout_ms,
84 ..Default::default()
85 }
86 }
87
88 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 pub fn delayed(timeout_ms: u64) -> Self {
101 Self {
102 timeout_ms,
103 pattern: TimeoutPattern::DelayedResponse,
104 ..Default::default()
105 }
106 }
107
108 pub fn no_wait(mut self) -> Self {
110 self.wait_duration = false;
111 self
112 }
113
114 pub fn with_message(mut self, message: impl Into<String>) -> Self {
116 self.error_message = message.into();
117 self
118 }
119}
120
121#[derive(Debug, Clone)]
147pub struct TimeoutFault {
148 base: BaseFault,
149 config: TimeoutConfig,
150 rng: StdRng,
151}
152
153impl TimeoutFault {
154 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 pub fn builder() -> TimeoutFaultBuilder {
170 TimeoutFaultBuilder::default()
171 }
172
173 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 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 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 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#[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 pub fn id(mut self, id: impl Into<String>) -> Self {
299 self.id = Some(id.into());
300 self
301 }
302
303 pub fn name(mut self, name: impl Into<String>) -> Self {
305 self.name = Some(name.into());
306 self
307 }
308
309 pub fn timeout_ms(mut self, ms: u64) -> Self {
311 self.config.timeout_ms = ms;
312 self
313 }
314
315 pub fn pattern(mut self, pattern: TimeoutPattern) -> Self {
317 self.config.pattern = pattern;
318 self
319 }
320
321 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 pub fn delayed(mut self) -> Self {
332 self.config.pattern = TimeoutPattern::DelayedResponse;
333 self
334 }
335
336 pub fn no_wait(mut self) -> Self {
338 self.config.wait_duration = false;
339 self
340 }
341
342 pub fn message(mut self, message: impl Into<String>) -> Self {
344 self.config.error_message = message.into();
345 self
346 }
347
348 pub fn probability(mut self, p: f64) -> Self {
350 self.probability = p.clamp(0.0, 1.0);
351 self
352 }
353
354 pub fn target(mut self, target: impl Into<String>) -> Self {
356 self.targets.push(target.into());
357 self
358 }
359
360 pub fn severity(mut self, severity: FaultSeverity) -> Self {
362 self.severity = severity;
363 self
364 }
365
366 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 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}