1use std::path::{Path, PathBuf};
56use std::sync::atomic::Ordering;
57use std::sync::Arc;
58use std::thread;
59use std::time::{Duration, Instant};
60
61use crate::heartbeat::{HeartbeatTable, EMPTY_PID};
62use crate::shared_atomic::{SharedAtomicError, SharedAtomicU64};
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum BarrierError {
66 Atomic(SharedAtomicError),
67 EpochTooFarBehind,
68 Timeout,
69 NoLivePeers,
70}
71
72impl From<SharedAtomicError> for BarrierError {
73 fn from(e: SharedAtomicError) -> Self { Self::Atomic(e) }
74}
75
76fn state_path(base: &Path) -> PathBuf {
77 let mut p = base.to_path_buf();
78 let stem = p.file_name().unwrap().to_string_lossy().to_string();
79 p.set_file_name(format!("{stem}.state.bin"));
80 p
81}
82
83#[inline]
84fn pack(epoch: u32, arrived: u32) -> u64 {
85 ((epoch as u64) << 32) | (arrived as u64)
86}
87#[inline]
88fn unpack(state: u64) -> (u32, u32) {
89 ((state >> 32) as u32, state as u32)
90}
91
92pub const DEFAULT_BARRIER_GRACE_EPOCHS: u64 = 3;
95
96pub struct EpochBarrier {
97 state: Arc<SharedAtomicU64>,
98 heartbeat: Arc<HeartbeatTable>,
99 grace_epochs: u64,
100 header_sidecar: subetha_core::HandshakeHeader,
101 ring_sidecar: Box<subetha_core::ObservationRing>,
102}
103
104impl subetha_sidecar::AdaptiveInstance for EpochBarrier {
105 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
106 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
107 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
108 Box::new(subetha_sidecar::NoMigrationPolicy)
109 }
110}
111
112impl EpochBarrier {
113 pub fn create(
117 base_path: impl AsRef<Path>,
118 heartbeat: Arc<HeartbeatTable>,
119 grace_epochs: u64,
120 ) -> Result<Self, BarrierError> {
121 let base = base_path.as_ref();
122 let state = Arc::new(SharedAtomicU64::create(state_path(base), 0)?);
123 Ok(Self {
124 state, heartbeat, grace_epochs,
125 header_sidecar: subetha_core::HandshakeHeader::new(),
126 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
127 })
128 }
129
130 pub fn open(
132 base_path: impl AsRef<Path>,
133 heartbeat: Arc<HeartbeatTable>,
134 grace_epochs: u64,
135 ) -> Result<Self, BarrierError> {
136 let base = base_path.as_ref();
137 let state = Arc::new(SharedAtomicU64::open(state_path(base))?);
138 Ok(Self {
139 state, heartbeat, grace_epochs,
140 header_sidecar: subetha_core::HandshakeHeader::new(),
141 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
142 })
143 }
144
145 pub fn live_peer_count(&self) -> u32 {
148 let global = self.heartbeat.global_epoch();
149 let cap = self.heartbeat.capacity();
150 let mut count = 0u32;
151 for i in 0..cap {
152 if let Some(snap) = self.heartbeat.snapshot(i) {
153 if snap.pid == EMPTY_PID { continue; }
154 if global.saturating_sub(snap.last_seen_epoch) <= self.grace_epochs {
155 count += 1;
156 }
157 }
158 }
159 count
160 }
161
162 pub fn wait(&self, my_epoch: u32) -> Result<(), BarrierError> {
165 let r = self.wait_inner(my_epoch, None, None);
166 self.ring_sidecar.push_op(
167 crate::sidecar_ops::liveness::OP_WAIT,
168 if r.is_err() { 1 } else { 0 },
169 );
170 r
171 }
172
173 pub fn wait_quorum(&self, my_epoch: u32, quorum: u32) -> Result<(), BarrierError> {
176 let r = self.wait_inner(my_epoch, Some(quorum), None);
177 self.ring_sidecar.push_op(
178 crate::sidecar_ops::liveness::OP_WAIT,
179 if r.is_err() { 1 } else { 0 },
180 );
181 r
182 }
183
184 pub fn wait_timeout(
187 &self, my_epoch: u32, timeout: Duration,
188 ) -> Result<(), BarrierError> {
189 self.wait_inner(my_epoch, None, Some(Instant::now() + timeout))
190 }
191
192 pub fn wait_quorum_timeout(
194 &self, my_epoch: u32, quorum: u32, timeout: Duration,
195 ) -> Result<(), BarrierError> {
196 self.wait_inner(my_epoch, Some(quorum), Some(Instant::now() + timeout))
197 }
198
199 fn wait_inner(
200 &self,
201 my_epoch: u32,
202 quorum: Option<u32>,
203 deadline: Option<Instant>,
204 ) -> Result<(), BarrierError> {
205 loop {
208 if let Some(d) = deadline
209 && Instant::now() >= d { return Err(BarrierError::Timeout); }
210 let state = self.state.load(Ordering::Acquire);
211 let (cur_epoch, cur_arrived) = unpack(state);
212 if cur_epoch > my_epoch {
213 return Ok(());
214 }
215 if cur_epoch < my_epoch {
216 thread::yield_now();
217 continue;
218 }
219 let new = pack(cur_epoch, cur_arrived.saturating_add(1));
220 if self.state.compare_exchange(
221 state, new, Ordering::AcqRel, Ordering::Acquire,
222 ).is_ok() {
223 break;
224 }
225 }
226
227 let mut spins = 0u32;
230 loop {
231 if let Some(d) = deadline
232 && Instant::now() >= d { return Err(BarrierError::Timeout); }
233 let state = self.state.load(Ordering::Acquire);
234 let (cur_epoch, cur_arrived) = unpack(state);
235 if cur_epoch > my_epoch {
236 return Ok(());
237 }
238 let threshold = match quorum {
239 Some(q) => q,
240 None => {
241 let live = self.live_peer_count();
242 if live == 0 { return Err(BarrierError::NoLivePeers); }
243 live
244 }
245 };
246 if cur_arrived >= threshold {
247 let new = pack(cur_epoch.saturating_add(1), 0);
248 if self.state.compare_exchange(
249 state, new, Ordering::AcqRel, Ordering::Acquire,
250 ).is_ok() {
251 return Ok(());
252 }
253 continue;
254 }
255 spins += 1;
256 if spins < 32 {
257 std::hint::spin_loop();
258 } else if spins < 256 {
259 thread::yield_now();
260 } else {
261 thread::sleep(Duration::from_micros(50));
262 }
263 }
264 }
265
266 pub fn current_epoch(&self) -> u32 {
268 unpack(self.state.load(Ordering::Acquire)).0
269 }
270
271 pub fn arrived_count(&self) -> u32 {
273 unpack(self.state.load(Ordering::Acquire)).1
274 }
275
276 pub fn snapshot(&self) -> (u32, u32) {
278 unpack(self.state.load(Ordering::Acquire))
279 }
280
281 pub fn flush(&self) -> Result<(), BarrierError> {
282 self.state.flush()?;
283 Ok(())
284 }
285
286 pub fn flush_async(&self) -> Result<(), BarrierError> {
290 self.state.flush_async()?;
291 Ok(())
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::heartbeat::HeartbeatTable;
299 use std::sync::Barrier as StdBarrier;
300 use std::sync::atomic::{AtomicU32, Ordering as O};
301
302 fn tmp_base(name: &str) -> PathBuf {
303 let mut p = std::env::temp_dir();
304 let pid = std::process::id();
305 p.push(format!("subetha-barrier-{name}-{pid}"));
306 p
307 }
308
309 fn tmp_hb(name: &str) -> PathBuf {
310 let mut p = std::env::temp_dir();
311 let pid = std::process::id();
312 p.push(format!("subetha-barrier-hb-{name}-{pid}.bin"));
313 p
314 }
315
316 fn cleanup(base: &Path, hb: &Path) {
317 std::fs::remove_file(state_path(base)).ok();
318 std::fs::remove_file(hb).ok();
319 }
320
321 fn make(name: &str, n_slots: usize, grace: u64) -> (PathBuf, PathBuf, Arc<HeartbeatTable>, EpochBarrier) {
322 let base = tmp_base(name);
323 let hb_path = tmp_hb(name);
324 let hb = Arc::new(HeartbeatTable::create(&hb_path, n_slots).unwrap());
325 let barrier = EpochBarrier::create(&base, hb.clone(), grace).unwrap();
326 (base, hb_path, hb, barrier)
327 }
328
329 #[test]
330 fn create_initial_state_is_zero() {
331 let (base, hb_path, _hb, barrier) = make("init", 4, 3);
332 assert_eq!(barrier.current_epoch(), 0);
333 assert_eq!(barrier.arrived_count(), 0);
334 cleanup(&base, &hb_path);
335 }
336
337 #[test]
338 fn single_peer_releases_immediately() {
339 let (base, hb_path, hb, barrier) = make("solo", 4, 3);
340 let s = hb.register(1001).unwrap();
341 hb.beat(s);
342 assert_eq!(barrier.live_peer_count(), 1);
343 barrier.wait(0).unwrap();
344 assert_eq!(barrier.current_epoch(), 1);
345 cleanup(&base, &hb_path);
346 }
347
348 #[test]
349 fn three_peers_all_arrive_releases() {
350 let (base, hb_path, hb, barrier) = make("three", 4, 10);
351 let slots: Vec<usize> = (0..3).map(|i| {
352 let s = hb.register(1000 + i as u32).unwrap();
353 hb.beat(s);
354 s
355 }).collect();
356 let _slots = slots;
357 let barrier = Arc::new(barrier);
358 let arrived = Arc::new(AtomicU32::new(0));
359 let sync = Arc::new(StdBarrier::new(3));
360 let mut handles = vec![];
361 for _ in 0..3 {
362 let b = barrier.clone();
363 let a = arrived.clone();
364 let s = sync.clone();
365 handles.push(thread::spawn(move || {
366 s.wait();
367 b.wait(0).unwrap();
368 a.fetch_add(1, O::AcqRel);
369 }));
370 }
371 for h in handles { h.join().unwrap(); }
372 assert_eq!(arrived.load(O::Acquire), 3);
373 assert_eq!(barrier.current_epoch(), 1);
374 cleanup(&base, &hb_path);
375 }
376
377 #[test]
378 fn early_arriver_waits_for_late_arriver() {
379 let (base, hb_path, hb, barrier) = make("late", 4, 10);
380 for i in 0..2 {
381 let s = hb.register(2000 + i).unwrap();
382 hb.beat(s);
383 }
384 let barrier = Arc::new(barrier);
385
386 let b1 = barrier.clone();
387 let early = thread::spawn(move || {
388 let start = Instant::now();
389 b1.wait(0).unwrap();
390 start.elapsed()
391 });
392 thread::sleep(Duration::from_millis(20));
393 let b2 = barrier.clone();
394 let late = thread::spawn(move || {
395 b2.wait(0).unwrap();
396 });
397 let elapsed = early.join().unwrap();
398 late.join().unwrap();
399 assert!(elapsed >= Duration::from_millis(15),
400 "early arriver should have waited ~20ms, got {elapsed:?}");
401 cleanup(&base, &hb_path);
402 }
403
404 #[test]
405 fn quorum_releases_at_threshold_below_total() {
406 let (base, hb_path, hb, barrier) = make("quorum", 8, 10);
407 for i in 0..5 {
408 let s = hb.register(3000 + i).unwrap();
409 hb.beat(s);
410 }
411 let barrier = Arc::new(barrier);
412
413 let sync = Arc::new(StdBarrier::new(3));
414 let mut handles = vec![];
415 for _ in 0..3 {
416 let b = barrier.clone();
417 let s = sync.clone();
418 handles.push(thread::spawn(move || {
419 s.wait();
420 b.wait_quorum(0, 3).unwrap();
421 }));
422 }
423 for h in handles { h.join().unwrap(); }
424 assert_eq!(barrier.current_epoch(), 1);
425 cleanup(&base, &hb_path);
426 }
427
428 #[test]
429 fn wait_timeout_returns_timeout_when_not_enough_arrive() {
430 let (base, hb_path, hb, barrier) = make("timeout", 4, 10);
431 for i in 0..2 {
432 let s = hb.register(4000 + i).unwrap();
433 hb.beat(s);
434 }
435 let start = Instant::now();
436 let r = barrier.wait_timeout(0, Duration::from_millis(30));
437 let elapsed = start.elapsed();
438 assert_eq!(r.err(), Some(BarrierError::Timeout));
439 assert!(elapsed >= Duration::from_millis(25));
440 cleanup(&base, &hb_path);
441 }
442
443 #[test]
444 fn epoch_passed_returns_immediately() {
445 let (base, hb_path, hb, barrier) = make("passed", 4, 10);
446 let _val = hb.register(5000).unwrap();
447 hb.beat(0);
448 barrier.wait(0).unwrap();
449 assert_eq!(barrier.current_epoch(), 1);
450 let start = Instant::now();
451 barrier.wait(0).unwrap();
452 assert!(start.elapsed() < Duration::from_millis(5));
453 cleanup(&base, &hb_path);
454 }
455
456 #[test]
457 fn multiple_epochs_in_sequence() {
458 let (base, hb_path, hb, barrier) = make("seq", 4, 10);
459 for i in 0..2 {
460 let s = hb.register(6000 + i).unwrap();
461 hb.beat(s);
462 }
463 let barrier = Arc::new(barrier);
464 let mut handles = vec![];
465 for _ in 0..2 {
466 let b = barrier.clone();
467 handles.push(thread::spawn(move || {
468 for e in 0..5u32 { b.wait(e).unwrap(); }
469 }));
470 }
471 for h in handles { h.join().unwrap(); }
472 assert_eq!(barrier.current_epoch(), 5);
473 cleanup(&base, &hb_path);
474 }
475
476 #[test]
477 fn dead_peer_does_not_block_barrier() {
478 let (base, hb_path, hb, barrier) = make("dead-peer", 4, 1);
479 let live_slot = hb.register(7000).unwrap();
480 let _dead_slot = hb.register(7001).unwrap();
481 hb.beat(live_slot);
482 for _ in 0..5 { hb.tick_global_epoch(); }
483 hb.beat(live_slot);
484 assert_eq!(barrier.live_peer_count(), 1,
485 "dead peer should be excluded by grace_epochs");
486 let start = Instant::now();
487 barrier.wait(0).unwrap();
488 assert!(start.elapsed() < Duration::from_millis(50));
489 cleanup(&base, &hb_path);
490 }
491
492 #[test]
493 fn cross_handle_barrier_state_visible() {
494 let (base, hb_path, hb, barrier_a) = make("cross", 4, 10);
495 let _val = hb.register(8000).unwrap();
496 hb.beat(0);
497 let barrier_b = EpochBarrier::open(&base, hb.clone(), 10).unwrap();
498 barrier_a.wait(0).unwrap();
499 assert_eq!(barrier_b.current_epoch(), 1);
500 cleanup(&base, &hb_path);
501 }
502
503 #[test]
504 fn no_live_peers_returns_error() {
505 let (base, hb_path, _hb, barrier) = make("no-peers", 4, 1);
506 let r = barrier.wait(0);
507 assert_eq!(r.err(), Some(BarrierError::NoLivePeers));
508 cleanup(&base, &hb_path);
509 }
510
511 #[test]
512 fn arrived_resets_at_each_epoch() {
513 let (base, hb_path, hb, barrier) = make("reset", 4, 10);
514 for i in 0..2 {
515 let s = hb.register(9000 + i).unwrap();
516 hb.beat(s);
517 }
518 let barrier = Arc::new(barrier);
519 let sync = Arc::new(StdBarrier::new(2));
520 let mut handles = vec![];
521 for _ in 0..2 {
522 let b = barrier.clone();
523 let s = sync.clone();
524 handles.push(thread::spawn(move || {
525 s.wait();
526 b.wait(0).unwrap();
527 b.wait(1).unwrap();
528 b.wait(2).unwrap();
529 }));
530 }
531 for h in handles { h.join().unwrap(); }
532 assert_eq!(barrier.current_epoch(), 3);
533 assert_eq!(barrier.arrived_count(), 0,
534 "arrived must reset to 0 after final release");
535 cleanup(&base, &hb_path);
536 }
537}