Skip to main content

moonpool_sim/chaos/
fault_events.rs

1//! Simulator-emitted fault events for the timeline.
2//!
3//! When faults are injected (network partitions, process reboots, storage corruption, etc.),
4//! the simulation engine records [`SimFaultEvent`]s and the runner drains them into the
5//! captured timeline under the [`SIM_FAULT_EVENT_NAME`] event name, with a `kind` field
6//! identifying the fault variant and the payload flattened into fields.
7//! Invariants can read these to correlate application behavior with infrastructure faults.
8//!
9//! # Usage
10//!
11//! ```ignore
12//! use std::cell::Cell;
13//! use moonpool_sim::{Invariant, SIM_FAULT_EVENT_NAME, TraceQuery};
14//!
15//! struct FaultCounter { cursor: Cell<usize> }
16//!
17//! impl Invariant for FaultCounter {
18//!     fn name(&self) -> &str { "fault_counter" }
19//!     fn observe(&self, q: &dyn TraceQuery, _t: u64) {
20//!         for e in q.since(SIM_FAULT_EVENT_NAME, &self.cursor) {
21//!             if e.str("kind") == Some("process_force_kill") {
22//!                 let ip = e.str("ip");
23//!                 // ...
24//!             }
25//!         }
26//!     }
27//! }
28//! ```
29
30use std::collections::BTreeMap;
31
32use serde::Serialize;
33
34use crate::observability::FieldValue;
35
36/// Well-known event name for simulator-emitted fault events.
37pub const SIM_FAULT_EVENT_NAME: &str = "sim_fault";
38
39/// Fault events automatically recorded by the simulator.
40///
41/// Invariants read these from the timeline via [`crate::TraceQuery`]:
42/// `q.since(SIM_FAULT_EVENT_NAME, &cursor)`, matching on the `kind` field
43/// (see [`SimFaultEvent::kind`]).
44#[derive(Debug, Clone, Serialize)]
45pub enum SimFaultEvent {
46    // -- Process lifecycle --
47    /// Process graceful shutdown initiated.
48    ProcessGracefulShutdown {
49        /// IP address of the process.
50        ip: String,
51        /// Grace period before force-kill, in milliseconds.
52        grace_period_ms: u64,
53    },
54    /// Process force-killed.
55    ProcessForceKill {
56        /// IP address of the process.
57        ip: String,
58    },
59    /// Process restarted after recovery delay.
60    ProcessRestart {
61        /// IP address of the process.
62        ip: String,
63    },
64
65    // -- Network --
66    /// Bidirectional network partition created between two IPs.
67    PartitionCreated {
68        /// Source IP.
69        from: String,
70        /// Destination IP.
71        to: String,
72    },
73    /// Network partition healed between two IPs.
74    PartitionHealed {
75        /// Source IP.
76        from: String,
77        /// Destination IP.
78        to: String,
79    },
80    /// Connection temporarily cut.
81    ConnectionCut {
82        /// The connection that was cut.
83        connection_id: u64,
84        /// Duration of the cut in milliseconds.
85        duration_ms: u64,
86    },
87    /// Temporarily cut connection restored.
88    CutRestored {
89        /// The connection that was restored.
90        connection_id: u64,
91    },
92    /// Half-open connection error triggered.
93    HalfOpenError {
94        /// The connection now returning errors.
95        connection_id: u64,
96    },
97    /// Send partition created (blocks outgoing from an IP).
98    SendPartitionCreated {
99        /// The partitioned IP.
100        ip: String,
101    },
102    /// Receive partition created (blocks incoming to an IP).
103    RecvPartitionCreated {
104        /// The partitioned IP.
105        ip: String,
106    },
107    /// Connection randomly closed by chaos.
108    RandomClose {
109        /// The connection that was closed.
110        connection_id: u64,
111    },
112    /// Peer crash simulated (half-open connection created).
113    PeerCrash {
114        /// The connection now in half-open state.
115        connection_id: u64,
116    },
117    /// Bit flip corruption injected during data delivery.
118    BitFlip {
119        /// The connection carrying corrupted data.
120        connection_id: u64,
121        /// Number of bits flipped.
122        flip_count: usize,
123    },
124
125    // -- Storage --
126    /// Read fault injected (sector marked as faulted).
127    StorageReadFault {
128        /// IP of the process owning the file.
129        ip: String,
130        /// File identifier.
131        file_id: u64,
132    },
133    /// Write fault injected (phantom, misdirected, or corruption).
134    StorageWriteFault {
135        /// IP of the process owning the file.
136        ip: String,
137        /// File identifier.
138        file_id: u64,
139        /// Kind of write fault: "phantom", "misdirected", or "corruption".
140        /// (Named `write_kind` to avoid colliding with the timeline's
141        /// variant-discriminator `kind` field.)
142        write_kind: String,
143    },
144    /// Sync failure injected.
145    StorageSyncFault {
146        /// IP of the process owning the file.
147        ip: String,
148        /// File identifier.
149        file_id: u64,
150    },
151    /// Storage crash simulated for a process.
152    StorageCrash {
153        /// IP of the crashed process.
154        ip: String,
155    },
156    /// All storage wiped for a process (`CrashAndWipe` reboot).
157    StorageWipe {
158        /// IP of the wiped process.
159        ip: String,
160    },
161}
162
163impl SimFaultEvent {
164    /// Stable `snake_case` identifier for this fault variant, stored in the
165    /// timeline event's `kind` field.
166    #[must_use]
167    pub fn kind(&self) -> &'static str {
168        match self {
169            Self::ProcessGracefulShutdown { .. } => "process_graceful_shutdown",
170            Self::ProcessForceKill { .. } => "process_force_kill",
171            Self::ProcessRestart { .. } => "process_restart",
172            Self::PartitionCreated { .. } => "partition_created",
173            Self::PartitionHealed { .. } => "partition_healed",
174            Self::ConnectionCut { .. } => "connection_cut",
175            Self::CutRestored { .. } => "cut_restored",
176            Self::HalfOpenError { .. } => "half_open_error",
177            Self::SendPartitionCreated { .. } => "send_partition_created",
178            Self::RecvPartitionCreated { .. } => "recv_partition_created",
179            Self::RandomClose { .. } => "random_close",
180            Self::PeerCrash { .. } => "peer_crash",
181            Self::BitFlip { .. } => "bit_flip",
182            Self::StorageReadFault { .. } => "storage_read_fault",
183            Self::StorageWriteFault { .. } => "storage_write_fault",
184            Self::StorageSyncFault { .. } => "storage_sync_fault",
185            Self::StorageCrash { .. } => "storage_crash",
186            Self::StorageWipe { .. } => "storage_wipe",
187        }
188    }
189
190    /// Flatten this fault's payload into timeline fields.
191    ///
192    /// Serializes via serde's external tagging (`{"Variant": {fields}}`) and
193    /// maps the inner object's scalars to [`FieldValue`]s. Returns an empty
194    /// map if serialization fails (it cannot for these variants).
195    pub(crate) fn to_fields(&self) -> BTreeMap<String, FieldValue> {
196        let mut fields = BTreeMap::new();
197        let Ok(serde_json::Value::Object(tagged)) = serde_json::to_value(self) else {
198            return fields;
199        };
200        for payload in tagged.into_values() {
201            let serde_json::Value::Object(entries) = payload else {
202                continue;
203            };
204            for (key, value) in entries {
205                let field = match value {
206                    serde_json::Value::Bool(b) => FieldValue::Bool(b),
207                    serde_json::Value::Number(n) => {
208                        if let Some(u) = n.as_u64() {
209                            FieldValue::U64(u)
210                        } else if let Some(i) = n.as_i64() {
211                            FieldValue::I64(i)
212                        } else if let Some(f) = n.as_f64() {
213                            FieldValue::F64(f)
214                        } else {
215                            continue;
216                        }
217                    }
218                    serde_json::Value::String(s) => FieldValue::Str(s),
219                    _ => continue,
220                };
221                fields.insert(key, field);
222            }
223        }
224        fields
225    }
226}