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}