foundation_core/synca/waitgroup.rs
1//! WaitGroup - Thread completion tracking primitive.
2//!
3//! WHY: Provides a counting barrier for tracking N outstanding work items.
4//!
5//! WHAT: `WaitGroup` blocks until count reaches 0. `WaitGroupGuard` is a
6//! RAII guard that calls `done()` on drop, ensuring cleanup even on panic.
7//!
8//! HOW: A `Mutex<usize>` holds the count and a `Condvar` parks waiters. The
9//! count is read and the thread parks ATOMICALLY under the same lock, so a
10//! `done()` that drives the count to 0 between a waiter's check and its park
11//! cannot be lost (no lost-wakeup). An earlier implementation used an atomic
12//! count plus a separate `LockSignal`, which had exactly that race: the
13//! signal could fire in the gap between the count check and the park, and
14//! `lock_and_wait()`'s internal `try_lock()` then clobbered the signalled
15//! state — leaving the waiter parked forever. Surfaced as intermittent
16//! "test running for over 60 seconds" hangs under contention.
17
18use std::sync::{Arc, Condvar, Mutex};
19
20/// Tracks N outstanding work items. `wait()` blocks until count reaches 0.
21///
22/// Each worker thread gets a `WaitGroupGuard` that calls `done()` on drop,
23/// ensuring cleanup even if the thread panics.
24///
25/// # Examples
26///
27/// ```
28/// use foundation_core::synca::WaitGroup;
29/// use std::sync::Arc;
30///
31/// let wg = WaitGroup::new();
32/// wg.add(3); // 3 workers
33///
34/// for i in 0..3 {
35/// let wg = wg.clone();
36/// std::thread::spawn(move || {
37/// let _guard = wg.guard(); // done() called on drop
38/// // do work
39/// });
40/// }
41///
42/// wg.wait(); // block until all workers done
43/// ```
44#[derive(Clone)]
45pub struct WaitGroup {
46 /// (count, condvar). The count lives INSIDE the mutex so that reading it
47 /// and parking on the condvar happen atomically under the same lock.
48 inner: Arc<(Mutex<usize>, Condvar)>,
49}
50
51impl WaitGroup {
52 /// Create a new `WaitGroup` with count 0.
53 #[must_use]
54 pub fn new() -> Self {
55 Self {
56 inner: Arc::new((Mutex::new(0), Condvar::new())),
57 }
58 }
59
60 /// Increment the counter by n.
61 pub fn add(&self, n: usize) {
62 let (lock, _) = &*self.inner;
63 let mut count = lock.lock().unwrap_or_else(|p| p.into_inner());
64 *count += n;
65 }
66
67 /// Decrement the counter. If it reaches 0, wake all waiters.
68 pub fn done(&self) {
69 let (lock, cond) = &*self.inner;
70 let mut count = lock.lock().unwrap_or_else(|p| p.into_inner());
71 debug_assert!(*count > 0, "WaitGroup::done() called more times than add()");
72 *count -= 1;
73 if *count == 0 {
74 // Notify under the lock — a waiter checking the predicate cannot be
75 // between its check and its park, because both happen under this lock.
76 cond.notify_all();
77 }
78 }
79
80 /// Block until count reaches 0.
81 pub fn wait(&self) {
82 let (lock, cond) = &*self.inner;
83 let mut count = lock.lock().unwrap_or_else(|p| p.into_inner());
84 while *count != 0 {
85 count = cond.wait(count).unwrap_or_else(|p| p.into_inner());
86 }
87 }
88
89 /// Block until count reaches 0 or `timeout` elapses.
90 ///
91 /// Returns `true` if the count reached 0 within the deadline, `false` if it
92 /// timed out with work still outstanding. Used for bounded drains (e.g. a
93 /// graceful-shutdown grace window) where the caller force-closes whatever
94 /// remains past the deadline. The deadline is tracked absolutely so
95 /// spurious condvar wakeups shorten the remaining wait rather than resetting
96 /// it.
97 #[must_use]
98 pub fn wait_timeout(&self, timeout: std::time::Duration) -> bool {
99 let (lock, cond) = &*self.inner;
100 let deadline = std::time::Instant::now() + timeout;
101 let mut count = lock.lock().unwrap_or_else(|p| p.into_inner());
102 while *count != 0 {
103 let Some(remaining) = deadline.checked_duration_since(std::time::Instant::now()) else {
104 return false;
105 };
106 let (next, res) = cond
107 .wait_timeout(count, remaining)
108 .unwrap_or_else(|p| p.into_inner());
109 count = next;
110 if res.timed_out() && *count != 0 {
111 return false;
112 }
113 }
114 true
115 }
116
117 /// Create a RAII guard that calls `done()` on drop.
118 #[must_use]
119 pub fn guard(&self) -> WaitGroupGuard {
120 WaitGroupGuard(self.clone())
121 }
122}
123
124impl Default for WaitGroup {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130/// Calls `WaitGroup::done()` on drop — ensures threads that panic still decrement.
131pub struct WaitGroupGuard(WaitGroup);
132
133impl Drop for WaitGroupGuard {
134 fn drop(&mut self) {
135 self.0.done();
136 }
137}