Skip to main content

structfs_handles/
gate.rs

1//! Parking primitives: `Gate` and `CancelToken`.
2//!
3//! Every hand-rolled handle store repeats the same subtle `Notify` dance:
4//! the notified future must be created and enabled *before* the state
5//! check, or a notification landing between the check and the await is
6//! lost and the reader parks forever. `Gate` owns that ordering so store
7//! authors never write it again.
8
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11
12use tokio::sync::Notify;
13
14/// Park until a predicate holds.
15///
16/// `wait_until` re-runs `check` each time the gate is notified and resolves
17/// with the first `Some` it produces. The enable-before-check ordering is
18/// internal, so a `notify` racing the check is never lost.
19#[derive(Default)]
20pub struct Gate {
21    notify: Notify,
22}
23
24impl Gate {
25    /// Create a gate.
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Wake all parked waiters so they re-run their predicates.
31    ///
32    /// Call after every state change a waiter might be watching.
33    pub fn notify(&self) {
34        self.notify.notify_waiters();
35    }
36
37    /// Park until `check` returns `Some`.
38    pub async fn wait_until<T>(&self, mut check: impl FnMut() -> Option<T>) -> T {
39        loop {
40            let notified = self.notify.notified();
41            tokio::pin!(notified);
42            // Enable the future BEFORE checking, so a notify between the
43            // check and the await still wakes us.
44            notified.as_mut().enable();
45            if let Some(value) = check() {
46                return value;
47            }
48            notified.await;
49        }
50    }
51
52    /// Park until `check` returns `Some` or the token is cancelled.
53    pub async fn wait_until_cancellable<T>(
54        &self,
55        token: &CancelToken,
56        mut check: impl FnMut() -> Option<T>,
57    ) -> Result<T, Cancelled> {
58        loop {
59            let notified = self.notify.notified();
60            let cancelled = token.inner.notify.notified();
61            tokio::pin!(notified);
62            tokio::pin!(cancelled);
63            notified.as_mut().enable();
64            cancelled.as_mut().enable();
65
66            if token.is_cancelled() {
67                return Err(Cancelled);
68            }
69            if let Some(value) = check() {
70                return Ok(value);
71            }
72            tokio::select! {
73                _ = notified => {}
74                _ = cancelled => {}
75            }
76        }
77    }
78}
79
80/// The wait was cancelled via its [`CancelToken`].
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct Cancelled;
83
84impl Cancelled {
85    /// Convert into a store error with context.
86    pub fn into_error(self, context: &str) -> structfs_core_store::Error {
87        structfs_core_store::Error::cancelled(context.to_string())
88    }
89}
90
91#[derive(Default)]
92struct CancelInner {
93    flag: AtomicBool,
94    notify: Notify,
95}
96
97/// A cloneable cancellation token.
98///
99/// Cancelling wakes every parked [`Gate::wait_until_cancellable`] carrying
100/// the token. By the handle-store protocol rule, cancellation fails parked
101/// *reads*; writes are not cancelled, so teardown writes can still land.
102#[derive(Clone, Default)]
103pub struct CancelToken {
104    inner: Arc<CancelInner>,
105}
106
107impl CancelToken {
108    /// Create an un-cancelled token.
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    /// Cancel: wake all waiters. Idempotent.
114    pub fn cancel(&self) {
115        self.inner.flag.store(true, Ordering::SeqCst);
116        self.inner.notify.notify_waiters();
117    }
118
119    /// Whether the token has been cancelled.
120    pub fn is_cancelled(&self) -> bool {
121        self.inner.flag.load(Ordering::SeqCst)
122    }
123
124    /// Park until the token is cancelled.
125    pub async fn cancelled(&self) {
126        loop {
127            let notified = self.inner.notify.notified();
128            tokio::pin!(notified);
129            notified.as_mut().enable();
130            if self.is_cancelled() {
131                return;
132            }
133            notified.await;
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::sync::Mutex;
142
143    #[tokio::test]
144    async fn wait_resolves_when_predicate_holds() {
145        let gate = Arc::new(Gate::new());
146        let slot: Arc<Mutex<Option<i32>>> = Arc::new(Mutex::new(None));
147
148        let waiter = {
149            let gate = gate.clone();
150            let slot = slot.clone();
151            tokio::spawn(async move { gate.wait_until(|| *slot.lock().unwrap()).await })
152        };
153
154        tokio::task::yield_now().await;
155        *slot.lock().unwrap() = Some(7);
156        gate.notify();
157
158        assert_eq!(waiter.await.unwrap(), 7);
159    }
160
161    #[tokio::test]
162    async fn notify_racing_check_is_not_lost() {
163        // Hammer the race: a notifier flips state and notifies while the
164        // waiter is between its check and its await. With enable-before-
165        // check this always resolves; without it, it can hang.
166        for _ in 0..100 {
167            let gate = Arc::new(Gate::new());
168            let flag = Arc::new(AtomicBool::new(false));
169
170            let waiter = {
171                let gate = gate.clone();
172                let flag = flag.clone();
173                tokio::spawn(async move {
174                    gate.wait_until(|| flag.load(Ordering::SeqCst).then_some(()))
175                        .await
176                })
177            };
178            let notifier = {
179                let gate = gate.clone();
180                let flag = flag.clone();
181                tokio::spawn(async move {
182                    flag.store(true, Ordering::SeqCst);
183                    gate.notify();
184                })
185            };
186
187            tokio::time::timeout(std::time::Duration::from_secs(5), waiter)
188                .await
189                .expect("lost wakeup")
190                .unwrap();
191            notifier.await.unwrap();
192        }
193    }
194
195    #[tokio::test]
196    async fn cancellation_wakes_parked_wait() {
197        let gate = Arc::new(Gate::new());
198        let token = CancelToken::new();
199
200        let waiter = {
201            let gate = gate.clone();
202            let token = token.clone();
203            tokio::spawn(async move { gate.wait_until_cancellable(&token, || None::<()>).await })
204        };
205
206        tokio::task::yield_now().await;
207        token.cancel();
208        assert_eq!(waiter.await.unwrap(), Err(Cancelled));
209    }
210
211    #[tokio::test]
212    async fn cancel_before_wait_resolves_immediately() {
213        let gate = Gate::new();
214        let token = CancelToken::new();
215        token.cancel();
216        assert_eq!(
217            gate.wait_until_cancellable(&token, || None::<()>).await,
218            Err(Cancelled)
219        );
220    }
221
222    #[tokio::test]
223    async fn predicate_wins_over_no_cancel() {
224        let gate = Gate::new();
225        let token = CancelToken::new();
226        assert_eq!(gate.wait_until_cancellable(&token, || Some(1)).await, Ok(1));
227    }
228
229    #[tokio::test]
230    async fn cancelled_future_resolves() {
231        let token = CancelToken::new();
232        let t2 = token.clone();
233        let waiter = tokio::spawn(async move { t2.cancelled().await });
234        tokio::task::yield_now().await;
235        token.cancel();
236        waiter.await.unwrap();
237    }
238}