1use crate::clock::{Micros, SkewSchedule};
17use crate::disk::VirtualDisk;
18use crate::network::{DeliveryOutcome, DropReason, LinkConfig, Network, NetworkStats, NodeId};
19use crate::rng::Seed;
20use crate::runtime::{Runtime, TaskBody, TaskId};
21use serde::{Deserialize, Serialize};
22use std::any::Any;
23use std::collections::{BTreeMap, BTreeSet};
24use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
25use std::path::{Path, PathBuf};
26use std::sync::atomic::{AtomicU32, Ordering};
27use std::{env, fs, io};
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub enum Event {
33 TaskSpawned {
35 task: TaskId,
37 node: NodeId,
39 name: String,
41 },
42 TaskDone {
44 task: TaskId,
46 node: NodeId,
48 name: String,
50 },
51 MessageSent {
53 from: NodeId,
55 to: NodeId,
57 seq: u64,
59 deliver_at: Micros,
61 },
62 MessageDropped {
64 from: NodeId,
66 to: NodeId,
68 seq: u64,
70 reason: DropReason,
72 },
73 MessageDuplicated {
75 from: NodeId,
77 to: NodeId,
79 seq: u64,
81 },
82 MessageReordered {
84 from: NodeId,
86 to: NodeId,
88 seq: u64,
90 },
91 MessageDelivered {
93 from: NodeId,
95 to: NodeId,
97 seq: u64,
99 },
100 DiskWrite {
102 node: NodeId,
104 path: String,
106 len: usize,
108 },
109 DiskTornWrite {
111 node: NodeId,
113 path: String,
115 requested: usize,
117 written: usize,
119 },
120 DiskWriteFailed {
122 node: NodeId,
124 path: String,
126 },
127 DiskFsync {
129 node: NodeId,
131 path: String,
133 durable_len: usize,
135 },
136 DiskFsyncFailed {
138 node: NodeId,
140 path: String,
142 },
143 Crash {
145 node: NodeId,
147 },
148 Restart {
150 node: NodeId,
152 },
153 Partition {
155 group_a: Vec<NodeId>,
157 group_b: Vec<NodeId>,
159 },
160 Healed,
162 SkewUpdated {
164 node: NodeId,
166 },
167 ClockAdvanced {
169 from: Micros,
171 to: Micros,
173 },
174 Custom {
176 node: NodeId,
178 message: String,
180 },
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
185pub enum ScenarioError {
186 #[error("deadlock detected at t={at_micros}: {waiting} task(s) waiting, none runnable")]
189 DeadlockDetected {
190 at_micros: Micros,
192 waiting: usize,
194 },
195 #[error("step limit {limit} exceeded (possible livelock)")]
197 StepLimitExceeded {
198 limit: u64,
200 },
201 #[error("task panicked: {0}")]
204 TaskPanicked(String),
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct RunOutcome {
210 pub seed: Seed,
212 pub steps: u64,
214 pub sim_time_micros: Micros,
216}
217
218type ProcessFactory = (String, Box<dyn FnMut() -> TaskBody>);
221
222pub struct Scenario {
225 seed: Seed,
226 runtime: Runtime,
227 network: Network,
228 disks: BTreeMap<NodeId, VirtualDisk>,
229 factories: BTreeMap<NodeId, ProcessFactory>,
230 crashed: BTreeSet<NodeId>,
231 events: Vec<Event>,
232}
233
234impl Scenario {
235 pub fn new(seed: Seed) -> Self {
237 Self {
238 seed,
239 runtime: Runtime::new(seed),
240 network: Network::new(LinkConfig::default()),
241 disks: BTreeMap::new(),
242 factories: BTreeMap::new(),
243 crashed: BTreeSet::new(),
244 events: Vec::new(),
245 }
246 }
247
248 pub fn seed(&self) -> Seed {
250 self.seed
251 }
252
253 pub fn now(&self) -> Micros {
255 self.runtime.clock().now()
256 }
257
258 pub fn events(&self) -> &[Event] {
260 &self.events
261 }
262
263 pub fn network_stats(&self) -> NetworkStats {
265 self.network.stats()
266 }
267
268 pub fn disk(&self, node: NodeId) -> Option<&VirtualDisk> {
270 self.disks.get(&node)
271 }
272
273 pub fn disk_mut(&mut self, node: NodeId) -> &mut VirtualDisk {
275 self.disks.entry(node).or_default()
276 }
277
278 pub fn add_node(
283 &mut self,
284 node: NodeId,
285 name: &str,
286 mut factory: impl FnMut() -> TaskBody + 'static,
287 ) {
288 self.disks.entry(node).or_default();
289 let body = factory();
290 let task = self.runtime.spawn(node, name, body);
291 self.events.push(Event::TaskSpawned {
292 task,
293 node,
294 name: name.to_string(),
295 });
296 self.factories
297 .insert(node, (name.to_string(), Box::new(factory)));
298 }
299
300 pub fn set_link_config(&mut self, from: NodeId, to: NodeId, config: LinkConfig) {
302 self.network.set_link_config(from, to, config);
303 }
304
305 pub fn set_skew(&mut self, node: NodeId, schedule: SkewSchedule) {
307 self.runtime.clock_mut().set_skew(node, schedule);
308 self.events.push(Event::SkewUpdated { node });
309 }
310
311 pub fn partition(
313 &mut self,
314 group_a: impl IntoIterator<Item = NodeId>,
315 group_b: impl IntoIterator<Item = NodeId>,
316 ) {
317 let group_a: Vec<NodeId> = group_a.into_iter().collect();
318 let group_b: Vec<NodeId> = group_b.into_iter().collect();
319 self.network
320 .partition(group_a.iter().copied(), group_b.iter().copied());
321 self.events.push(Event::Partition { group_a, group_b });
322 }
323
324 pub fn heal(&mut self) {
326 self.network.heal();
327 self.events.push(Event::Healed);
328 }
329
330 pub fn crash_node(&mut self, node: NodeId) {
334 if !self.crashed.insert(node) {
335 return;
336 }
337 self.runtime.remove_tasks_of(node);
338 self.network.clear_inbox(node);
339 if let Some(disk) = self.disks.get_mut(&node) {
340 disk.crash();
341 }
342 self.events.push(Event::Crash { node });
343 }
344
345 pub fn restart_node(&mut self, node: NodeId) {
348 if !self.crashed.remove(&node) {
349 return;
350 }
351 self.disks.entry(node).or_default();
352 let (name, factory) = self
353 .factories
354 .get_mut(&node)
355 .expect("no process registered for node");
356 let name = name.clone();
357 let body = factory();
358 let task = self.runtime.spawn(node, &name, body);
359 self.events.push(Event::Restart { node });
360 self.events.push(Event::TaskSpawned { task, node, name });
361 }
362
363 pub fn run(&mut self, max_steps: u64) -> Result<RunOutcome, ScenarioError> {
368 self.run_with(max_steps, |_| {})
369 }
370
371 pub fn run_with(
376 &mut self,
377 max_steps: u64,
378 mut chaos: impl FnMut(&mut Scenario),
379 ) -> Result<RunOutcome, ScenarioError> {
380 let outcome = catch_unwind(AssertUnwindSafe(|| self.run_inner(max_steps, &mut chaos)));
381 match outcome {
382 Ok(Ok(report)) => Ok(report),
383 Ok(Err(error)) => {
384 self.persist_failure(&error);
385 Err(error)
386 }
387 Err(payload) => {
388 self.persist_failure(&ScenarioError::TaskPanicked(panic_message(
389 payload.as_ref(),
390 )));
391 resume_unwind(payload)
392 }
393 }
394 }
395
396 fn run_inner(
397 &mut self,
398 max_steps: u64,
399 chaos: &mut dyn FnMut(&mut Scenario),
400 ) -> Result<RunOutcome, ScenarioError> {
401 let mut steps = 0u64;
402 loop {
403 chaos(self);
404 self.runtime.wake_due();
405
406 if let Some(task) = self.runtime.pick_ready() {
407 steps += 1;
408 if steps > max_steps {
409 return Err(ScenarioError::StepLimitExceeded { limit: max_steps });
410 }
411 let node = self.runtime.task_node(task);
412 let disk = self.disks.entry(node).or_default();
413 self.runtime
414 .step(task, &mut self.network, disk, &mut self.events);
415 continue;
416 }
417
418 let next = self
419 .network
420 .next_delivery()
421 .into_iter()
422 .chain(self.runtime.next_wake())
423 .min();
424 match next {
425 Some(target) => {
426 let from = self.now();
427 self.runtime
428 .clock_mut()
429 .advance_to(target)
430 .expect("event times are monotonic");
431 self.events.push(Event::ClockAdvanced { from, to: target });
432 self.deliver_due();
433 }
434 None => {
435 if self.runtime.all_done() {
436 return Ok(RunOutcome {
437 seed: self.seed,
438 steps,
439 sim_time_micros: self.now(),
440 });
441 }
442 return Err(ScenarioError::DeadlockDetected {
443 at_micros: self.now(),
444 waiting: self.runtime.unfinished_count(),
445 });
446 }
447 }
448 }
449 }
450
451 fn deliver_due(&mut self) {
452 let now = self.now();
453 let deliveries = self.network.deliver_due(now, &self.crashed);
454 let mut woke = BTreeSet::new();
455 for delivery in deliveries {
456 match delivery.outcome {
457 DeliveryOutcome::Delivered => {
458 self.events.push(Event::MessageDelivered {
459 from: delivery.from,
460 to: delivery.to,
461 seq: delivery.seq,
462 });
463 woke.insert(delivery.to);
464 }
465 DeliveryOutcome::Discarded(reason) => {
466 self.events.push(Event::MessageDropped {
467 from: delivery.from,
468 to: delivery.to,
469 seq: delivery.seq,
470 reason,
471 });
472 }
473 }
474 }
475 for node in woke {
476 self.runtime.wake_receivers(node);
477 }
478 }
479
480 fn persist_failure(&self, error: &ScenarioError) {
481 let dir = failure_dir();
482 if let Err(io_error) = persist_failure_report(&dir, self.seed, error, &self.events) {
483 eprintln!(
484 "mongreldb-sim: could not persist failure artifact to {}: {io_error}",
485 dir.display()
486 );
487 }
488 }
489}
490
491fn panic_message(payload: &dyn Any) -> String {
492 if let Some(message) = payload.downcast_ref::<&str>() {
493 (*message).to_string()
494 } else if let Some(message) = payload.downcast_ref::<String>() {
495 message.clone()
496 } else {
497 "unknown panic payload".to_string()
498 }
499}
500
501pub fn failure_dir() -> PathBuf {
504 env::var("MONGRELDB_SIM_FAILURES")
505 .map_or_else(|_| PathBuf::from("target/sim-failures"), PathBuf::from)
506}
507
508static FAILURE_COUNTER: AtomicU32 = AtomicU32::new(0);
509
510pub fn persist_failure_report(
513 dir: &Path,
514 seed: Seed,
515 error: &ScenarioError,
516 events: &[Event],
517) -> io::Result<PathBuf> {
518 fs::create_dir_all(dir)?;
519 let ordinal = FAILURE_COUNTER.fetch_add(1, Ordering::Relaxed);
520 let path = dir.join(format!("sim-failure-{}-{ordinal}.json", seed.get()));
521 let report = serde_json::json!({
522 "seed": seed.get(),
523 "error": error.to_string(),
524 "event_count": events.len(),
525 "events": events,
526 });
527 fs::write(&path, serde_json::to_string_pretty(&report)?)?;
528 Ok(path)
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use crate::runtime::TaskState;
535
536 const A: NodeId = NodeId(1);
537
538 #[test]
539 fn deadlock_is_detected() {
540 let mut scenario = Scenario::new(Seed::new(1));
541 scenario.add_node(A, "waiter", || Box::new(|_| TaskState::WaitForMessage));
542 let error = scenario.run_inner(1_000, &mut |_| {}).unwrap_err();
543 assert!(matches!(
544 error,
545 ScenarioError::DeadlockDetected { waiting: 1, .. }
546 ));
547 }
548
549 #[test]
550 fn step_limit_guards_livelock() {
551 let mut scenario = Scenario::new(Seed::new(2));
552 scenario.add_node(A, "spinner", || Box::new(|_| TaskState::Yield));
553 let error = scenario.run_inner(100, &mut |_| {}).unwrap_err();
554 assert_eq!(error, ScenarioError::StepLimitExceeded { limit: 100 });
555 }
556
557 #[test]
558 fn happy_path_run_records_events() {
559 let mut scenario = Scenario::new(Seed::new(3));
560 scenario.add_node(A, "lonely", || {
561 Box::new(|ctx| {
562 ctx.log("hello");
563 TaskState::Done
564 })
565 });
566 let outcome = scenario.run(1_000).unwrap();
567 assert_eq!(outcome.seed, Seed::new(3));
568 assert_eq!(outcome.steps, 1);
569 assert!(scenario
570 .events()
571 .iter()
572 .any(|event| matches!(event, Event::Custom { message, .. } if message == "hello")));
573 }
574
575 #[test]
576 fn persist_failure_report_writes_seed_file() {
577 let dir =
578 env::temp_dir().join(format!("mongreldb-sim-persist-test-{}", std::process::id()));
579 let _ = fs::remove_dir_all(&dir);
580 let error = ScenarioError::StepLimitExceeded { limit: 5 };
581 let events = vec![Event::Healed];
582 let path = persist_failure_report(&dir, Seed::new(77), &error, &events).unwrap();
583
584 let written: serde_json::Value =
585 serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
586 assert_eq!(written["seed"], 77);
587 assert!(written["error"].as_str().unwrap().contains("step limit"));
588 assert_eq!(written["events"], serde_json::json!(["Healed"]));
589 let _ = fs::remove_dir_all(&dir);
590 }
591}