Skip to main content

mongreldb_sim/
clock.rs

1//! Virtual time in microseconds with per-node skew schedules (spec
2//! section 9.5, FND-005).
3//!
4//! The simulated clock never reads the wall clock. It moves only when
5//! the scenario runner advances it — normally when every task is idle
6//! ("advance-on-idle"), jumping straight to the next timer or message
7//! delivery. Per-node [`SkewSchedule`]s model unsynchronized hardware
8//! clocks without affecting the global event order.
9
10use crate::network::NodeId;
11use std::collections::BTreeMap;
12
13/// Virtual time unit: microseconds since scenario start.
14pub type Micros = u64;
15
16/// Clock misuse.
17#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
18pub enum ClockError {
19    /// Attempted to move virtual time backwards.
20    #[error("cannot move virtual clock backwards from {now} to {target}")]
21    Backwards {
22        /// Current virtual time.
23        now: Micros,
24        /// Requested target time.
25        target: Micros,
26    },
27}
28
29/// A piecewise-constant offset schedule for one node's hardware clock.
30///
31/// Each segment `(start, skew)` applies `skew` micros from `start`
32/// until the next segment begins. Times before the first segment have
33/// zero skew.
34#[derive(Debug, Clone, Default, PartialEq, Eq)]
35pub struct SkewSchedule {
36    segments: Vec<(Micros, i64)>,
37}
38
39impl SkewSchedule {
40    /// A fixed offset applied from t=0.
41    pub fn constant(skew_micros: i64) -> Self {
42        Self {
43            segments: vec![(0, skew_micros)],
44        }
45    }
46
47    /// A schedule from `(start, skew)` segments. Panics unless segment
48    /// starts are strictly increasing.
49    pub fn piecewise(segments: Vec<(Micros, i64)>) -> Self {
50        assert!(
51            segments.windows(2).all(|w| w[0].0 < w[1].0),
52            "skew schedule segments must have strictly increasing start times"
53        );
54        Self { segments }
55    }
56
57    /// The offset in effect at `at`.
58    pub fn skew_at(&self, at: Micros) -> i64 {
59        let mut skew = 0;
60        for &(start, value) in &self.segments {
61            if start > at {
62                break;
63            }
64            skew = value;
65        }
66        skew
67    }
68}
69
70/// The global virtual clock plus per-node skew schedules.
71#[derive(Debug, Default)]
72pub struct Clock {
73    now: Micros,
74    skews: BTreeMap<NodeId, SkewSchedule>,
75}
76
77impl Clock {
78    /// A clock at t=0 with no skew.
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Current global virtual time.
84    pub fn now(&self) -> Micros {
85        self.now
86    }
87
88    /// Moves the clock to `target`. Moving backwards is an error.
89    pub fn advance_to(&mut self, target: Micros) -> Result<(), ClockError> {
90        if target < self.now {
91            return Err(ClockError::Backwards {
92                now: self.now,
93                target,
94            });
95        }
96        self.now = target;
97        Ok(())
98    }
99
100    /// Moves the clock forward by `delta`, returning the new time.
101    pub fn advance_by(&mut self, delta: Micros) -> Micros {
102        self.now += delta;
103        self.now
104    }
105
106    /// Installs a skew schedule for a node.
107    pub fn set_skew(&mut self, node: NodeId, schedule: SkewSchedule) {
108        self.skews.insert(node, schedule);
109    }
110
111    /// Node-local time: global now plus the node's current skew.
112    pub fn node_now(&self, node: NodeId) -> i64 {
113        let skew = self.skews.get(&node).map_or(0, |s| s.skew_at(self.now));
114        self.now as i64 + skew
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn clock_only_moves_forward() {
124        let mut clock = Clock::new();
125        assert_eq!(clock.now(), 0);
126        assert_eq!(clock.advance_by(100), 100);
127        clock.advance_to(250).unwrap();
128        clock.advance_to(250).unwrap();
129        assert_eq!(
130            clock.advance_to(100),
131            Err(ClockError::Backwards {
132                now: 250,
133                target: 100
134            })
135        );
136    }
137
138    #[test]
139    fn piecewise_skew_switches_at_segment_starts() {
140        let schedule = SkewSchedule::piecewise(vec![(100, -50), (500, 200)]);
141        assert_eq!(schedule.skew_at(0), 0);
142        assert_eq!(schedule.skew_at(99), 0);
143        assert_eq!(schedule.skew_at(100), -50);
144        assert_eq!(schedule.skew_at(499), -50);
145        assert_eq!(schedule.skew_at(500), 200);
146    }
147
148    #[test]
149    #[should_panic(expected = "strictly increasing")]
150    fn piecewise_rejects_unsorted_segments() {
151        let _ = SkewSchedule::piecewise(vec![(100, 1), (50, 2)]);
152    }
153
154    #[test]
155    fn node_now_applies_constant_skew() {
156        let node = NodeId(7);
157        let mut clock = Clock::new();
158        clock.set_skew(node, SkewSchedule::constant(250));
159        assert_eq!(clock.node_now(node), 250);
160        clock.advance_by(100);
161        assert_eq!(clock.node_now(node), 350);
162        assert_eq!(clock.node_now(NodeId(8)), 100);
163    }
164}