grafos_fence/
epoch.rs

1use core::fmt;
2
3/// A monotonic epoch counter used for fencing stale operations.
4///
5/// Epochs are totally ordered unsigned 64-bit integers. A higher epoch
6/// supersedes all lower epochs — any operation stamped with a lower epoch
7/// than the current one is considered stale.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct FenceEpoch(u64);
11
12impl FenceEpoch {
13    /// Create a new epoch with the given value.
14    pub fn new(value: u64) -> Self {
15        Self(value)
16    }
17
18    /// The initial epoch (0).
19    pub fn zero() -> Self {
20        Self(0)
21    }
22
23    /// Returns `true` if this epoch is strictly behind `current`.
24    pub fn is_stale(&self, current: &FenceEpoch) -> bool {
25        self.0 < current.0
26    }
27
28    /// Returns `true` if this epoch equals `current`.
29    pub fn is_current(&self, current: &FenceEpoch) -> bool {
30        self.0 == current.0
31    }
32
33    /// Returns `true` if this epoch is strictly ahead of `other`.
34    pub fn is_newer(&self, other: &FenceEpoch) -> bool {
35        self.0 > other.0
36    }
37
38    /// Create a fence epoch seeded from a lease's generation counter.
39    ///
40    /// This bridges graph-level generation tracking (e.g.
41    /// [`grafos_core::LeaseRef::generation`]) with the fencing primitives
42    /// in this crate, allowing stale-write rejection based on lease
43    /// generation numbers.
44    pub fn from_generation(generation: u64) -> Self {
45        FenceEpoch(generation)
46    }
47
48    /// Returns a new epoch one greater than this one.
49    pub fn bump(&self) -> FenceEpoch {
50        FenceEpoch(self.0 + 1)
51    }
52
53    /// Unwrap the inner `u64` value.
54    pub fn value(&self) -> u64 {
55        self.0
56    }
57}
58
59impl fmt::Display for FenceEpoch {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "epoch({})", self.0)
62    }
63}
64
65/// Free function: returns an epoch one greater than `old`.
66pub fn bump_epoch(old: FenceEpoch) -> FenceEpoch {
67    old.bump()
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn zero_is_zero() {
76        assert_eq!(FenceEpoch::zero().value(), 0);
77    }
78
79    #[test]
80    fn stale_current_newer() {
81        let e0 = FenceEpoch::new(0);
82        let e1 = FenceEpoch::new(1);
83        let e2 = FenceEpoch::new(2);
84
85        assert!(e0.is_stale(&e1));
86        assert!(!e1.is_stale(&e1));
87        assert!(!e2.is_stale(&e1));
88
89        assert!(!e0.is_current(&e1));
90        assert!(e1.is_current(&e1));
91        assert!(!e2.is_current(&e1));
92
93        assert!(!e0.is_newer(&e1));
94        assert!(!e1.is_newer(&e1));
95        assert!(e2.is_newer(&e1));
96    }
97
98    #[test]
99    fn bump_increments() {
100        let e = FenceEpoch::new(41);
101        let bumped = e.bump();
102        assert_eq!(bumped.value(), 42);
103    }
104
105    #[test]
106    fn bump_epoch_free_fn() {
107        let e = FenceEpoch::new(9);
108        assert_eq!(bump_epoch(e).value(), 10);
109    }
110
111    #[test]
112    fn ordering() {
113        let mut epochs: Vec<FenceEpoch> =
114            vec![FenceEpoch::new(3), FenceEpoch::new(1), FenceEpoch::new(2)];
115        epochs.sort();
116        assert_eq!(
117            epochs.iter().map(|e| e.value()).collect::<Vec<_>>(),
118            vec![1, 2, 3]
119        );
120    }
121
122    #[test]
123    fn display() {
124        assert_eq!(format!("{}", FenceEpoch::new(7)), "epoch(7)");
125    }
126
127    #[test]
128    fn from_generation_roundtrip() {
129        let from_gen = FenceEpoch::from_generation(42);
130        let direct = FenceEpoch::new(42);
131        assert!(from_gen.is_current(&direct));
132        assert_eq!(from_gen.value(), direct.value());
133    }
134
135    #[test]
136    fn from_generation_stale_check() {
137        let e1 = FenceEpoch::from_generation(1);
138        let e2 = FenceEpoch::from_generation(2);
139        assert!(e1.is_stale(&e2));
140        assert!(!e2.is_stale(&e1));
141    }
142
143    #[test]
144    fn from_generation_with_fence_guard() {
145        use crate::FenceGuard;
146
147        let mut guard = FenceGuard::new(FenceEpoch::zero());
148
149        // Advance to epoch 1 (equivalent to from_generation(1))
150        let e1 = guard.advance();
151        assert_eq!(e1, FenceEpoch::from_generation(1));
152
153        // Write at from_generation(1) succeeds
154        assert!(guard.check(FenceEpoch::from_generation(1)).is_ok());
155
156        // Advance to epoch 2 (equivalent to from_generation(2))
157        let e2 = guard.advance();
158        assert_eq!(e2, FenceEpoch::from_generation(2));
159
160        // Stale write at epoch 1 is rejected
161        assert!(guard.check(FenceEpoch::from_generation(1)).is_err());
162
163        // Current write at epoch 2 succeeds
164        assert!(guard.check(FenceEpoch::from_generation(2)).is_ok());
165    }
166}