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}