1use std::time::Duration;
38
39use async_trait::async_trait;
40use moonpool_core::TimeProvider;
41
42use crate::SimulationResult;
43use crate::providers::{SimRandomProvider, SimTimeProvider};
44use crate::runner::locality::{DomainLevel, MachineRegistry};
45use crate::runner::process::{AttritionScope, RebootKind};
46use crate::runner::tags::TagRegistry;
47use crate::sim::SimWorld;
48use crate::{assert_reachable, assert_sometimes_each};
49
50pub struct ProcessInfo {
52 pub process_ips: Vec<String>,
54 pub tag_registry: TagRegistry,
56 pub machine_registry: MachineRegistry,
58 pub dead_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
60}
61
62pub struct FaultContext {
68 sim: SimWorld,
69 process_info: ProcessInfo,
70 random: SimRandomProvider,
71 time: SimTimeProvider,
72 chaos_shutdown: tokio_util::sync::CancellationToken,
73}
74
75impl FaultContext {
76 #[must_use]
78 pub fn new(
79 sim: SimWorld,
80 process_info: ProcessInfo,
81 random: SimRandomProvider,
82 time: SimTimeProvider,
83 chaos_shutdown: tokio_util::sync::CancellationToken,
84 ) -> Self {
85 Self {
86 sim,
87 process_info,
88 random,
89 time,
90 chaos_shutdown,
91 }
92 }
93
94 #[must_use]
96 pub fn dead_count(&self) -> usize {
97 self.process_info
98 .dead_count
99 .load(std::sync::atomic::Ordering::Relaxed)
100 }
101
102 pub fn partition(&self, a: &str, b: &str) -> SimulationResult<()> {
110 let a_ip: std::net::IpAddr = a
111 .parse()
112 .map_err(|e| crate::SimulationError::InvalidState(format!("invalid IP '{a}': {e}")))?;
113 let b_ip: std::net::IpAddr = b
114 .parse()
115 .map_err(|e| crate::SimulationError::InvalidState(format!("invalid IP '{b}': {e}")))?;
116 self.sim.partition_pair(a_ip, b_ip, Duration::from_hours(1))
118 }
119
120 pub fn heal_partition(&self, a: &str, b: &str) -> SimulationResult<()> {
126 let a_ip: std::net::IpAddr = a
127 .parse()
128 .map_err(|e| crate::SimulationError::InvalidState(format!("invalid IP '{a}': {e}")))?;
129 let b_ip: std::net::IpAddr = b
130 .parse()
131 .map_err(|e| crate::SimulationError::InvalidState(format!("invalid IP '{b}': {e}")))?;
132 self.sim.restore_partition(a_ip, b_ip)
133 }
134
135 pub fn is_partitioned(&self, a: &str, b: &str) -> SimulationResult<bool> {
141 let a_ip: std::net::IpAddr = a
142 .parse()
143 .map_err(|e| crate::SimulationError::InvalidState(format!("invalid IP '{a}': {e}")))?;
144 let b_ip: std::net::IpAddr = b
145 .parse()
146 .map_err(|e| crate::SimulationError::InvalidState(format!("invalid IP '{b}': {e}")))?;
147 self.sim.is_partitioned(a_ip, b_ip)
148 }
149
150 #[must_use]
152 pub fn random(&self) -> &SimRandomProvider {
153 &self.random
154 }
155
156 #[must_use]
158 pub fn time(&self) -> &SimTimeProvider {
159 &self.time
160 }
161
162 #[must_use]
167 pub fn chaos_shutdown(&self) -> &tokio_util::sync::CancellationToken {
168 &self.chaos_shutdown
169 }
170
171 #[must_use]
173 pub fn process_ips(&self) -> &[String] {
174 &self.process_info.process_ips
175 }
176
177 pub fn reboot(&self, ip: &str, kind: RebootKind) -> SimulationResult<()> {
191 let recovery_range = 1000..10000;
192 let grace_range = 2000..5000;
193 self.reboot_with_delays(ip, kind, &recovery_range, &grace_range)
194 }
195
196 pub fn reboot_with_delays(
206 &self,
207 ip: &str,
208 kind: RebootKind,
209 recovery_delay_range_ms: &std::ops::Range<usize>,
210 grace_period_range_ms: &std::ops::Range<usize>,
211 ) -> SimulationResult<()> {
212 let ip_addr: std::net::IpAddr = ip
213 .parse()
214 .map_err(|e| crate::SimulationError::InvalidState(format!("invalid IP '{ip}': {e}")))?;
215
216 match kind {
217 RebootKind::Graceful => {
218 assert_reachable!("reboot: graceful path");
219 let grace_ms = crate::sim::sim_random_range(grace_period_range_ms.clone()) as u64;
220 let recovery_ms =
221 crate::sim::sim_random_range(recovery_delay_range_ms.clone()) as u64;
222 self.sim.schedule_event(
223 crate::sim::Event::ProcessGracefulShutdown {
224 ip: ip_addr,
225 grace_period_ms: grace_ms,
226 recovery_delay_ms: recovery_ms,
227 },
228 Duration::from_nanos(1),
229 );
230 tracing::info!(
231 "Initiated graceful reboot for process at IP {} (grace={}ms, recovery={}ms)",
232 ip,
233 grace_ms,
234 recovery_ms
235 );
236 }
237 RebootKind::Crash | RebootKind::CrashAndWipe => {
238 assert_reachable!("reboot: crash path");
239 self.sim.abort_all_connections_for_ip(ip_addr);
240 self.sim.simulate_crash_for_process(ip_addr, true);
242 if kind == RebootKind::CrashAndWipe {
244 self.sim.wipe_storage_for_process(ip_addr);
245 }
246 self.process_info
247 .dead_count
248 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
249 let delay_ms = crate::sim::sim_random_range(recovery_delay_range_ms.clone()) as u64;
250 let recovery_delay = Duration::from_millis(delay_ms);
251 self.sim.schedule_process_restart(ip_addr, recovery_delay);
252 tracing::info!(
253 "Crashed process at IP {} (recovery in {:?})",
254 ip,
255 recovery_delay
256 );
257 }
258 }
259
260 Ok(())
261 }
262
263 pub fn reboot_random(&self, kind: RebootKind) -> SimulationResult<Option<String>> {
272 if self.process_info.process_ips.is_empty() {
273 return Ok(None);
274 }
275 let idx = crate::sim::sim_random_range(0..self.process_info.process_ips.len());
276 let ip = self.process_info.process_ips[idx].clone();
277 self.reboot(&ip, kind)?;
278 Ok(Some(ip))
279 }
280
281 pub fn reboot_tagged(
287 &self,
288 key: &str,
289 value: &str,
290 kind: RebootKind,
291 ) -> SimulationResult<Vec<String>> {
292 let matching_ips: Vec<String> = self
293 .process_info
294 .tag_registry
295 .ips_tagged(key, value)
296 .into_iter()
297 .map(|ip| ip.to_string())
298 .collect();
299
300 for ip in &matching_ips {
301 self.reboot(ip, kind)?;
302 }
303
304 Ok(matching_ips)
305 }
306
307 #[must_use]
309 pub fn machine_registry(&self) -> &MachineRegistry {
310 &self.process_info.machine_registry
311 }
312
313 pub fn reboot_machine(
322 &self,
323 machine_id: &str,
324 kind: RebootKind,
325 ) -> SimulationResult<Vec<String>> {
326 self.reboot_domain(DomainLevel::Machine, machine_id, kind)
327 }
328
329 pub fn reboot_domain(
337 &self,
338 level: DomainLevel,
339 id: &str,
340 kind: RebootKind,
341 ) -> SimulationResult<Vec<String>> {
342 let ips: Vec<String> = self
343 .process_info
344 .machine_registry
345 .ips_in_domain(level, id)
346 .into_iter()
347 .map(|ip| ip.to_string())
348 .collect();
349
350 for ip in &ips {
351 self.reboot(ip, kind)?;
352 }
353
354 Ok(ips)
355 }
356}
357
358#[async_trait]
364pub trait FaultInjector: Send + Sync + 'static {
365 fn name(&self) -> &str;
367
368 async fn inject(&mut self, ctx: &FaultContext) -> SimulationResult<()>;
372}
373
374pub(crate) struct AttritionInjector {
380 config: super::process::Attrition,
381}
382
383impl AttritionInjector {
384 pub(crate) fn new(config: super::process::Attrition) -> Self {
386 Self { config }
387 }
388
389 fn choose_kind(&self) -> RebootKind {
391 let rand_val = f64::from(crate::sim::sim_random_range(0..10000)) / 10000.0;
392 let kind = self.config.choose_kind(rand_val);
393 assert_sometimes_each!("attrition_reboot_kind", [("kind", kind as i64)]);
394 kind
395 }
396
397 fn delay_ranges(&self) -> (std::ops::Range<usize>, std::ops::Range<usize>) {
399 (
400 self.config.recovery_delay_ms.clone().unwrap_or(1000..10000),
401 self.config.grace_period_ms.clone().unwrap_or(2000..5000),
402 )
403 }
404
405 fn inject_process(&self, ctx: &FaultContext) -> SimulationResult<()> {
407 if ctx.dead_count() >= self.config.max_dead {
408 assert_reachable!("attrition: max_dead limit enforced");
409 return Ok(());
410 }
411 let kind = self.choose_kind();
412 let (recovery_range, grace_range) = self.delay_ranges();
413 let idx = crate::sim::sim_random_range(0..ctx.process_ips().len());
414 let ip = ctx.process_ips()[idx].clone();
415 assert_sometimes_each!(
416 "attrition_process_targeted",
417 [("process_idx", i64::try_from(idx).unwrap_or(i64::MAX))]
418 );
419 ctx.reboot_with_delays(&ip, kind, &recovery_range, &grace_range)
420 }
421
422 fn inject_domain(
426 &self,
427 ctx: &FaultContext,
428 level: DomainLevel,
429 domains: &[String],
430 ) -> SimulationResult<()> {
431 if domains.is_empty() {
432 return Ok(());
433 }
434 let di = crate::sim::sim_random_range(0..domains.len());
435 let ips: Vec<String> = ctx
436 .machine_registry()
437 .ips_in_domain(level, &domains[di])
438 .into_iter()
439 .map(|ip| ip.to_string())
440 .collect();
441 if ctx.dead_count() + ips.len() > self.config.max_dead {
444 assert_reachable!("attrition: max_dead limit enforced (group)");
445 return Ok(());
446 }
447 let kind = self.choose_kind();
448 let (recovery_range, grace_range) = self.delay_ranges();
449 assert_sometimes_each!(
450 "attrition_domain_targeted",
451 [("group_size", i64::try_from(ips.len()).unwrap_or(i64::MAX))]
452 );
453 for ip in &ips {
454 ctx.reboot_with_delays(ip, kind, &recovery_range, &grace_range)?;
455 }
456 Ok(())
457 }
458}
459
460#[async_trait]
461impl FaultInjector for AttritionInjector {
462 fn name(&self) -> &'static str {
463 "attrition"
464 }
465
466 async fn inject(&mut self, ctx: &FaultContext) -> SimulationResult<()> {
467 while !ctx.chaos_shutdown().is_cancelled() {
468 let delay_ms = crate::sim::sim_random_range(1000..5000);
470 ctx.time()
471 .sleep(Duration::from_millis(
472 u64::try_from(delay_ms).expect("delay_ms is non-negative"),
473 ))
474 .await
475 .map_err(|e| crate::SimulationError::InvalidState(format!("sleep failed: {e}")))?;
476
477 if ctx.chaos_shutdown().is_cancelled() {
478 break;
479 }
480
481 if ctx.process_ips().is_empty() {
482 continue;
483 }
484
485 match self.config.scope {
486 AttritionScope::PerProcess => self.inject_process(ctx)?,
487 AttritionScope::PerMachine => {
488 let machines = ctx.machine_registry().all_machines();
489 self.inject_domain(ctx, DomainLevel::Machine, &machines)?;
490 }
491 AttritionScope::PerZone => {
492 let zones = ctx.machine_registry().all_zones();
493 self.inject_domain(ctx, DomainLevel::Zone, &zones)?;
494 }
495 }
496 }
497 Ok(())
498 }
499}