Skip to main content

mongreldb_sim/
runtime.rs

1//! Deterministic cooperative task scheduling (spec section 9.5,
2//! FND-005).
3//!
4//! Tasks are boxed closure state machines: each poll runs one step and
5//! returns a [`TaskState`] telling the scheduler what to do next. The
6//! executor is deliberately std-only and single-threaded — no tokio, no
7//! worker threads — because a production runtime schedules on OS threads
8//! and reads the real clock, which would make runs irreproducible. When
9//! several tasks are ready, the runtime's seeded [`SimRng`] picks the
10//! next one, so the seed fixes the interleaving.
11//!
12//! [`crate::scenario::Scenario`] builds process crash/restart on top:
13//! crashing a node drops its task bodies (volatile state) while the
14//! node's [`VirtualDisk`] keeps exactly the fsynced prefix.
15
16use crate::clock::{Clock, Micros};
17use crate::disk::{DiskError, VirtualDisk};
18use crate::network::{Message, Network, NodeId, SendOutcome};
19use crate::rng::{Seed, SimRng};
20use crate::scenario::Event;
21use std::collections::BTreeMap;
22
23/// A task handle within a [`Runtime`].
24pub type TaskId = u64;
25
26/// One step of a cooperative state machine. Captured variables are the
27/// task's volatile state: they vanish when the owning process crashes.
28pub type TaskBody = Box<dyn FnMut(&mut NodeContext<'_>) -> TaskState>;
29
30/// What a task wants after a step.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum TaskState {
33    /// Reschedule as soon as the scheduler comes back to it.
34    Yield,
35    /// Wake after the given number of virtual micros.
36    SleepFor(Micros),
37    /// Park until a network message arrives for this node.
38    WaitForMessage,
39    /// The task is finished.
40    Done,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44enum Status {
45    Ready,
46    Sleeping(Micros),
47    WaitingMessage,
48    Done,
49}
50
51struct Task {
52    node: NodeId,
53    name: String,
54    status: Status,
55    rng: SimRng,
56    body: TaskBody,
57}
58
59/// The single-node view handed to a task step: virtual time, a seeded
60/// stream, the network, the node's disk, and the scenario event log.
61pub struct NodeContext<'a> {
62    node: NodeId,
63    clock: &'a Clock,
64    rng: &'a mut SimRng,
65    net: &'a mut Network,
66    disk: &'a mut VirtualDisk,
67    events: &'a mut Vec<Event>,
68}
69
70impl NodeContext<'_> {
71    /// The node this task runs on.
72    pub fn node(&self) -> NodeId {
73        self.node
74    }
75
76    /// Global virtual time.
77    pub fn now(&self) -> Micros {
78        self.clock.now()
79    }
80
81    /// Node-local time with the node's skew schedule applied.
82    pub fn node_now(&self) -> i64 {
83        self.clock.node_now(self.node)
84    }
85
86    /// The task's own seeded stream (forked at spawn).
87    pub fn rng(&mut self) -> &mut SimRng {
88        &mut *self.rng
89    }
90
91    /// Sends a message, recording send/drop/duplicate/reorder events.
92    pub fn send(&mut self, to: NodeId, payload: impl Into<Vec<u8>>) -> SendOutcome {
93        let outcome = self.net.send(
94            self.node,
95            to,
96            payload.into(),
97            self.clock.now(),
98            &mut *self.rng,
99        );
100        match &outcome {
101            SendOutcome::Scheduled {
102                seq,
103                deliver_at,
104                duplicated,
105                reordered,
106            } => {
107                self.events.push(Event::MessageSent {
108                    from: self.node,
109                    to,
110                    seq: *seq,
111                    deliver_at: *deliver_at,
112                });
113                if *duplicated {
114                    self.events.push(Event::MessageDuplicated {
115                        from: self.node,
116                        to,
117                        seq: *seq,
118                    });
119                }
120                if *reordered {
121                    self.events.push(Event::MessageReordered {
122                        from: self.node,
123                        to,
124                        seq: *seq,
125                    });
126                }
127            }
128            SendOutcome::Dropped { seq, reason } => {
129                self.events.push(Event::MessageDropped {
130                    from: self.node,
131                    to,
132                    seq: *seq,
133                    reason: *reason,
134                });
135            }
136        }
137        outcome
138    }
139
140    /// Pops the oldest message from this node's inbox, if any.
141    pub fn try_recv(&mut self) -> Option<Message> {
142        self.net.try_recv(self.node)
143    }
144
145    /// Appends to a file's pending stage, recording the outcome.
146    pub fn disk_append(&mut self, path: &str, bytes: &[u8]) -> Result<usize, DiskError> {
147        match self.disk.append(path, bytes) {
148            Ok(written) => {
149                if written < bytes.len() {
150                    self.events.push(Event::DiskTornWrite {
151                        node: self.node,
152                        path: path.to_string(),
153                        requested: bytes.len(),
154                        written,
155                    });
156                } else {
157                    self.events.push(Event::DiskWrite {
158                        node: self.node,
159                        path: path.to_string(),
160                        len: written,
161                    });
162                }
163                Ok(written)
164            }
165            Err(error) => {
166                self.events.push(Event::DiskWriteFailed {
167                    node: self.node,
168                    path: path.to_string(),
169                });
170                Err(error)
171            }
172        }
173    }
174
175    /// Fsyncs a file, recording the outcome.
176    pub fn disk_fsync(&mut self, path: &str) -> Result<(), DiskError> {
177        match self.disk.fsync(path) {
178            Ok(()) => {
179                self.events.push(Event::DiskFsync {
180                    node: self.node,
181                    path: path.to_string(),
182                    durable_len: self.disk.durable_len(path),
183                });
184                Ok(())
185            }
186            Err(error) => {
187                self.events.push(Event::DiskFsyncFailed {
188                    node: self.node,
189                    path: path.to_string(),
190                });
191                Err(error)
192            }
193        }
194    }
195
196    /// Live view of a file: durable bytes plus pending bytes.
197    pub fn disk_read(&self, path: &str) -> Vec<u8> {
198        self.disk.read(path)
199    }
200
201    /// Crash-recovery view of a file: exactly the fsynced prefix.
202    pub fn disk_read_durable(&self, path: &str) -> Vec<u8> {
203        self.disk.read_durable(path)
204    }
205
206    /// Records an application-level event in the scenario log.
207    pub fn log(&mut self, message: impl Into<String>) {
208        self.events.push(Event::Custom {
209            node: self.node,
210            message: message.into(),
211        });
212    }
213}
214
215/// The cooperative executor: a clock, a seeded stream, and the tasks.
216pub struct Runtime {
217    clock: Clock,
218    rng: SimRng,
219    tasks: BTreeMap<TaskId, Task>,
220    next_task_id: TaskId,
221}
222
223impl Runtime {
224    /// An empty runtime at virtual t=0.
225    pub fn new(seed: Seed) -> Self {
226        Self {
227            clock: Clock::new(),
228            rng: SimRng::from_seed(seed),
229            tasks: BTreeMap::new(),
230            next_task_id: 1,
231        }
232    }
233
234    /// The virtual clock driving timers and node skew.
235    pub fn clock(&self) -> &Clock {
236        &self.clock
237    }
238
239    pub(crate) fn clock_mut(&mut self) -> &mut Clock {
240        &mut self.clock
241    }
242
243    /// Spawns a task on a node. The task receives its own stream forked
244    /// from the runtime seed.
245    pub fn spawn(&mut self, node: NodeId, name: &str, body: TaskBody) -> TaskId {
246        let id = self.next_task_id;
247        self.next_task_id += 1;
248        let rng = self.rng.fork();
249        self.tasks.insert(
250            id,
251            Task {
252                node,
253                name: name.to_string(),
254                status: Status::Ready,
255                rng,
256                body,
257            },
258        );
259        id
260    }
261
262    pub(crate) fn task_node(&self, task: TaskId) -> NodeId {
263        self.tasks[&task].node
264    }
265
266    /// Wakes every task whose sleep deadline has passed.
267    pub(crate) fn wake_due(&mut self) {
268        let now = self.clock.now();
269        for task in self.tasks.values_mut() {
270            if let Status::Sleeping(until) = task.status {
271                if until <= now {
272                    task.status = Status::Ready;
273                }
274            }
275        }
276    }
277
278    /// Wakes tasks of `node` that parked on [`TaskState::WaitForMessage`].
279    pub(crate) fn wake_receivers(&mut self, node: NodeId) {
280        for task in self.tasks.values_mut() {
281            if task.node == node && task.status == Status::WaitingMessage {
282                task.status = Status::Ready;
283            }
284        }
285    }
286
287    /// Picks a ready task using the runtime's seeded stream, so the seed
288    /// fixes the interleaving. Returns `None` when nothing can run.
289    pub(crate) fn pick_ready(&mut self) -> Option<TaskId> {
290        let ready: Vec<TaskId> = self
291            .tasks
292            .iter()
293            .filter(|(_, task)| task.status == Status::Ready)
294            .map(|(&id, _)| id)
295            .collect();
296        if ready.is_empty() {
297            None
298        } else {
299            Some(ready[self.rng.below(ready.len() as u64) as usize])
300        }
301    }
302
303    /// Runs one step of a task and applies the resulting [`TaskState`].
304    pub(crate) fn step(
305        &mut self,
306        task_id: TaskId,
307        net: &mut Network,
308        disk: &mut VirtualDisk,
309        events: &mut Vec<Event>,
310    ) {
311        let now = self.clock.now();
312        let task = self.tasks.get_mut(&task_id).expect("task must exist");
313        let node = task.node;
314        let state = {
315            let mut ctx = NodeContext {
316                node,
317                clock: &self.clock,
318                rng: &mut task.rng,
319                net: &mut *net,
320                disk: &mut *disk,
321                events: &mut *events,
322            };
323            (task.body)(&mut ctx)
324        };
325        match state {
326            TaskState::Yield => task.status = Status::Ready,
327            TaskState::SleepFor(delta) => task.status = Status::Sleeping(now + delta),
328            TaskState::WaitForMessage => {
329                task.status = if net.inbox_len(node) > 0 {
330                    Status::Ready
331                } else {
332                    Status::WaitingMessage
333                };
334            }
335            TaskState::Done => {
336                task.status = Status::Done;
337                events.push(Event::TaskDone {
338                    task: task_id,
339                    node,
340                    name: task.name.clone(),
341                });
342            }
343        }
344    }
345
346    /// Drops every task of a node, discarding its volatile state.
347    /// Returns how many tasks were dropped.
348    pub(crate) fn remove_tasks_of(&mut self, node: NodeId) -> usize {
349        let before = self.tasks.len();
350        self.tasks.retain(|_, task| task.node != node);
351        before - self.tasks.len()
352    }
353
354    /// The earliest sleep deadline among parked tasks, if any.
355    pub(crate) fn next_wake(&self) -> Option<Micros> {
356        self.tasks
357            .values()
358            .filter_map(|task| match task.status {
359                Status::Sleeping(until) => Some(until),
360                _ => None,
361            })
362            .min()
363    }
364
365    /// Whether every spawned task has finished.
366    pub(crate) fn all_done(&self) -> bool {
367        self.tasks.values().all(|task| task.status == Status::Done)
368    }
369
370    /// Number of tasks that have not finished yet.
371    pub(crate) fn unfinished_count(&self) -> usize {
372        self.tasks
373            .values()
374            .filter(|task| task.status != Status::Done)
375            .count()
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use crate::network::LinkConfig;
383
384    const A: NodeId = NodeId(1);
385
386    fn harness(seed: u64) -> (Runtime, Network, VirtualDisk, Vec<Event>) {
387        (
388            Runtime::new(Seed::new(seed)),
389            Network::new(LinkConfig::default()),
390            VirtualDisk::new(),
391            Vec::new(),
392        )
393    }
394
395    #[test]
396    fn tasks_run_as_cooperative_state_machines() {
397        let (mut runtime, mut net, mut disk, mut events) = harness(1);
398        let task = runtime.spawn(A, "counter", {
399            let mut steps = 0;
400            Box::new(move |_ctx| {
401                steps += 1;
402                if steps >= 3 {
403                    TaskState::Done
404                } else {
405                    TaskState::Yield
406                }
407            })
408        });
409
410        runtime.step(task, &mut net, &mut disk, &mut events);
411        runtime.step(task, &mut net, &mut disk, &mut events);
412        assert!(!runtime.all_done());
413        assert_eq!(runtime.unfinished_count(), 1);
414        runtime.step(task, &mut net, &mut disk, &mut events);
415        assert!(runtime.all_done());
416        assert!(events.iter().any(|event| matches!(
417            event,
418            Event::TaskDone { task: done, name, .. } if *done == task && name == "counter"
419        )));
420    }
421
422    #[test]
423    fn sleeping_tasks_wake_at_their_deadline() {
424        let (mut runtime, mut net, mut disk, mut events) = harness(2);
425        let task = runtime.spawn(A, "sleeper", {
426            let mut slept = false;
427            Box::new(move |_ctx| {
428                if slept {
429                    TaskState::Done
430                } else {
431                    slept = true;
432                    TaskState::SleepFor(100)
433                }
434            })
435        });
436
437        runtime.step(task, &mut net, &mut disk, &mut events);
438        assert_eq!(runtime.next_wake(), Some(100));
439        runtime.wake_due();
440        assert_eq!(runtime.pick_ready(), None);
441
442        runtime.clock_mut().advance_to(100).unwrap();
443        runtime.wake_due();
444        assert_eq!(runtime.pick_ready(), Some(task));
445    }
446
447    #[test]
448    fn seeded_ready_pick_is_reproducible() {
449        let pick_sequence = |seed: u64| {
450            let mut runtime = Runtime::new(Seed::new(seed));
451            for _ in 0..5 {
452                runtime.spawn(A, "idle", Box::new(|_| TaskState::Yield));
453            }
454            (0..20).map(|_| runtime.pick_ready()).collect::<Vec<_>>()
455        };
456        assert_eq!(pick_sequence(9), pick_sequence(9));
457        assert_ne!(pick_sequence(9), pick_sequence(10));
458    }
459
460    #[test]
461    fn crash_drops_tasks_and_receiver_wake_works() {
462        let (mut runtime, mut net, mut disk, mut events) = harness(3);
463        let b = NodeId(2);
464        runtime.spawn(b, "waiter", Box::new(|_| TaskState::WaitForMessage));
465        runtime.spawn(b, "waiter2", Box::new(|_| TaskState::WaitForMessage));
466        let parked = runtime.tasks.len();
467        assert_eq!(parked, 2);
468
469        // Step one waiter: it parks because the inbox is empty.
470        let task = runtime.pick_ready().unwrap();
471        runtime.step(task, &mut net, &mut disk, &mut events);
472        runtime.wake_receivers(b);
473        // Messages arrived? No — wake_receivers only helps after delivery;
474        // with an empty inbox the stepped task parks again on next step.
475
476        assert_eq!(runtime.remove_tasks_of(b), 2);
477        assert_eq!(runtime.unfinished_count(), 0);
478    }
479}