Skip to main content

subetha_core/
handshake.rs

1//! Per-instance handshake header: generation + in-flight refcounts.
2//!
3//! The header is two cache lines wide to keep the read-mostly generation
4//! field from false-sharing with the write-hot in-flight counters.
5
6use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
7
8/// Layout invariant: 64-byte aligned, 128 bytes total (two cache lines).
9///
10/// Line 0 is read-mostly (generation + strategy tag).
11/// Line 1 is write-hot (in-flight counters for the two live generations).
12#[repr(C, align(64))]
13pub struct HandshakeHeader {
14    // Cache line 0: read-mostly.
15    /// Current generation. Op entry captures this; migration bumps it.
16    pub generation: AtomicU32,
17    /// PIC strategy tag. Hot-path branch target. One byte semantically;
18    /// stored as u32 for atomic alignment.
19    pub strategy_tag: AtomicU32,
20    _pad0: [u8; 56],
21
22    // Cache line 1: write-hot.
23    /// In-flight op counts, one slot per generation parity.
24    /// Indexed by `generation & 1`. Op entry increments; exit decrements.
25    pub in_flight: [AtomicU64; 2],
26    _pad1: [u8; 48],
27}
28
29impl HandshakeHeader {
30    pub const fn new() -> Self {
31        Self {
32            generation: AtomicU32::new(0),
33            strategy_tag: AtomicU32::new(0),
34            _pad0: [0; 56],
35            in_flight: [AtomicU64::new(0), AtomicU64::new(0)],
36            _pad1: [0; 48],
37        }
38    }
39
40    /// Enter an op. Returns the captured generation, which the caller
41    /// must pass to [`Self::exit_op`] to release the in-flight slot.
42    ///
43    /// Uses the standard RCU/epoch **double-check** pattern: load the
44    /// generation, increment the matching in_flight slot, re-load the
45    /// generation; retry if the generation changed between loads. This
46    /// closes the race where a migration completes (bump + drain + free)
47    /// after the first load but before the increment, which would
48    /// otherwise leave the reader holding an in_flight slot on a freed
49    /// generation.
50    ///
51    /// Adds ~1 cycle (a second Acquire load) on the common no-migration
52    /// path. The Acquire on the in-flight fetch_add also enables the
53    /// pair `state.store(Release) + fence(SeqCst) + in_flight.load(Acquire)`
54    /// for primitives that need to skip wakeups when no waiters exist.
55    #[inline(always)]
56    pub fn enter_op(&self) -> u32 {
57        loop {
58            let current = self.generation.load(Ordering::Acquire);
59            let slot = (current & 1) as usize;
60            self.in_flight[slot].fetch_add(1, Ordering::AcqRel);
61            let recheck = self.generation.load(Ordering::Acquire);
62            if recheck == current {
63                return current;
64            }
65            // Generation changed between our gen load and our in_flight
66            // increment. Our increment is on the wrong slot - undo and
67            // retry on the new generation.
68            self.in_flight[slot].fetch_sub(1, Ordering::AcqRel);
69            core::hint::spin_loop();
70        }
71    }
72
73    /// Read the in-flight count for a given generation.
74    ///
75    /// Used by primitive coordinators to decide whether to skip wakeup
76    /// after a state transition. For the safe skip-wakeup pattern,
77    /// pair this with a `SeqCst` fence after the state store and use
78    /// `Acquire` ordering on this load.
79    #[inline]
80    pub fn in_flight_count(&self, generation: u32) -> u64 {
81        self.in_flight[(generation & 1) as usize].load(Ordering::Acquire)
82    }
83
84    #[inline(always)]
85    pub fn exit_op(&self, captured_gen: u32) {
86        self.in_flight[(captured_gen & 1) as usize].fetch_sub(1, Ordering::Release);
87    }
88
89    /// Read the current strategy tag without entering an op.
90    #[inline(always)]
91    pub fn tag(&self) -> u32 {
92        self.strategy_tag.load(Ordering::Relaxed)
93    }
94
95    /// Bump generation only. Used for data-layout migration without
96    /// changing the strategy tag.
97    ///
98    /// Returns the old generation so the caller can drain its in-flight slot.
99    pub fn bump_generation(&self) -> u32 {
100        let old_value = self.generation.load(Ordering::Acquire);
101        self.generation.store(old_value.wrapping_add(1), Ordering::Release);
102        old_value
103    }
104
105    /// Set the strategy tag in place. PIC-only update; does NOT bump
106    /// generation. Use when the strategy change does not require any
107    /// data-layout migration (e.g., switching wait strategy in a
108    /// once-shot primitive).
109    #[inline]
110    pub fn set_tag(&self, new_tag: u32) {
111        self.strategy_tag.store(new_tag, Ordering::Release);
112    }
113
114    /// Bump generation and atomically swap the strategy tag.
115    ///
116    /// After this returns, new ops will read the new tag; in-flight ops
117    /// on the old generation continue to completion. Returns the old
118    /// generation so the caller can wait on its in-flight counter.
119    pub fn migrate(&self, new_tag: u32) -> u32 {
120        let old_value = self.generation.load(Ordering::Acquire);
121        self.strategy_tag.store(new_tag, Ordering::Relaxed);
122        self.generation.store(old_value.wrapping_add(1), Ordering::Release);
123        old_value
124    }
125
126    /// Wait until the given generation has zero in-flight ops.
127    ///
128    /// Spins. Caller is the migration coordinator and migration is rare,
129    /// so spinning is acceptable here.
130    pub fn drain(&self, generation: u32) {
131        let slot = (generation & 1) as usize;
132        while self.in_flight[slot].load(Ordering::Acquire) != 0 {
133            core::hint::spin_loop();
134        }
135    }
136}
137
138impl Default for HandshakeHeader {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn header_size_is_two_cache_lines() {
150        assert_eq!(core::mem::size_of::<HandshakeHeader>(), 128);
151        assert_eq!(core::mem::align_of::<HandshakeHeader>(), 64);
152    }
153
154    #[test]
155    fn enter_exit_balances() {
156        let h = HandshakeHeader::new();
157        let g = h.enter_op();
158        assert_eq!(g, 0);
159        assert_eq!(h.in_flight[0].load(Ordering::Relaxed), 1);
160        h.exit_op(g);
161        assert_eq!(h.in_flight[0].load(Ordering::Relaxed), 0);
162    }
163
164    #[test]
165    fn migrate_bumps_generation_and_swaps_tag() {
166        let h = HandshakeHeader::new();
167        assert_eq!(h.tag(), 0);
168        let old = h.migrate(7);
169        assert_eq!(old, 0);
170        assert_eq!(h.generation.load(Ordering::Acquire), 1);
171        assert_eq!(h.tag(), 7);
172    }
173
174    #[test]
175    fn bump_generation_does_not_touch_tag() {
176        let h = HandshakeHeader::new();
177        h.set_tag(3);
178        let old = h.bump_generation();
179        assert_eq!(old, 0);
180        assert_eq!(h.generation.load(Ordering::Acquire), 1);
181        assert_eq!(h.tag(), 3, "tag must not change on generation bump");
182    }
183
184    #[test]
185    fn set_tag_does_not_touch_generation() {
186        let h = HandshakeHeader::new();
187        h.set_tag(5);
188        assert_eq!(h.tag(), 5);
189        assert_eq!(h.generation.load(Ordering::Acquire), 0, "generation must not change on tag set");
190    }
191
192    #[test]
193    fn drain_returns_when_in_flight_zero() {
194        let h = HandshakeHeader::new();
195        h.drain(0);
196        let g = h.enter_op();
197        h.exit_op(g);
198        h.drain(0);
199    }
200}