Skip to main content

flag_bearer_queue/
lib.rs

1#![no_std]
2#![warn(
3    unsafe_op_in_unsafe_fn,
4    clippy::missing_safety_doc,
5    clippy::multiple_unsafe_ops_per_block,
6    clippy::undocumented_unsafe_blocks
7)]
8
9#[cfg(test)]
10extern crate std;
11
12use core::{hint::unreachable_unchecked, task::Waker};
13
14use closeable::{Closeable, IsCloseable};
15use flag_bearer_core::SemaphoreState;
16use pin_list::PinList;
17
18pub mod acquire;
19pub mod closeable;
20
21mod loom;
22
23/// A queue that manages the acquisition of permits from a [`SemaphoreState`], or queues tasks
24/// if no permits are available.
25// don't question the weird bounds here...
26pub struct SemaphoreQueue<
27    S: SemaphoreState<Params = Params, Permit = Permit> + ?Sized,
28    C: IsCloseable,
29    Params = <S as SemaphoreState>::Params,
30    Permit = <S as SemaphoreState>::Permit,
31> {
32    #[allow(clippy::type_complexity)]
33    queue: Result<PinList<PinQueue<Params, Permit, C>>, C::Closed<()>>,
34    /// Set if a panic ever escaped user code (`SemaphoreState::acquire` or a
35    /// `with_state` closure) while we held the state, which may have left `state`
36    /// half-updated. We can't tell a clean panic from a corrupting one, so — like
37    /// [`std::sync::Mutex`] — we assume the worst until `clear_poison`.
38    /// `state` must stay the last field so it can be `?Sized`.
39    poisoned: bool,
40    state: S,
41}
42
43/// Sets the poison flag if dropped. Drop only runs if we unwind out of the
44/// guarded user call; [`core::mem::forget`] it on the success path so a clean
45/// call leaves the flag untouched (and never *clears* a prior poison).
46pub(crate) struct PoisonOnUnwind<'a>(pub(crate) &'a mut bool);
47
48impl Drop for PoisonOnUnwind<'_> {
49    fn drop(&mut self) {
50        *self.0 = true;
51    }
52}
53
54impl<S: SemaphoreState + core::fmt::Debug, C: IsCloseable> core::fmt::Debug
55    for SemaphoreQueue<S, C>
56{
57    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58        let mut d = f.debug_struct("SemaphoreQueue");
59        d.field("state", &self.state);
60        d.finish_non_exhaustive()
61    }
62}
63
64type PinQueue<Params, Permit, C> = dyn pin_list::Types<
65        Id = pin_list::id::DebugChecked,
66        Protected = (
67            // Some(params) -> Pending
68            // None -> the leader's params have been taken: transiently while
69            //         check() acquires, or left behind if that acquire panicked
70            //         (in which case the queue is also poisoned).
71            Option<Params>,
72            Waker,
73        ),
74        Removed = Result<
75            // Ok(permit) -> Ready
76            Permit,
77            // Err(Some(params)) -> Closed
78            // Err(None) -> Closed, Invalid state
79            <C as closeable::private::Sealed>::Closed<Option<Params>>,
80        >,
81        Unprotected = (),
82    >;
83
84impl<S: SemaphoreState, C: IsCloseable> SemaphoreQueue<S, C> {
85    /// Construct a new semaphore queue, with the given [`SemaphoreState`].
86    pub fn new(state: S) -> Self {
87        Self {
88            state,
89            poisoned: false,
90            // Safety: during acquire, we ensure that nodes in this queue
91            // will never attempt to use a different queue to read the nodes.
92            queue: Ok(PinList::new(unsafe { pin_list::id::DebugChecked::new() })),
93        }
94    }
95}
96
97impl<S: SemaphoreState + ?Sized, C: IsCloseable> SemaphoreQueue<S, C> {
98    /// Access the state with mutable access.
99    ///
100    /// This gives direct access to the state, be careful not to
101    /// break any of your own state invariants. You can use this
102    /// to peek at the current state, or to modify it, eg to add or
103    /// remove permits from the semaphore.
104    pub fn with_state<T>(&mut self, f: impl FnOnce(&mut S) -> T) -> T {
105        // A panic in `f` may leave `state` half-updated, so poison if it unwinds.
106        let guard = PoisonOnUnwind(&mut self.poisoned);
107        let res = f(&mut self.state);
108        core::mem::forget(guard);
109
110        self.check();
111        res
112    }
113
114    #[inline]
115    fn check(&mut self) {
116        if self.poisoned {
117            return;
118        }
119        let Ok(queue) = &mut self.queue else { return };
120        let mut leader = queue.cursor_front_mut();
121        while let Some(p) = leader.protected_mut() {
122            let Some(params) = p.0.take() else {
123                // This node's params were lost to a panic in `acquire`. While
124                // poisoned, the check above returns early, so we only reach here
125                // after `clear_poison`. The waiter can never be satisfied (and
126                // leaves when its future is dropped), so skip it and keep serving
127                // the rest of the queue.
128                leader.move_next();
129                continue;
130            };
131            // A panic in `acquire` loses `params` and may leave `state`
132            // half-updated, so poison if it unwinds.
133            let guard = PoisonOnUnwind(&mut self.poisoned);
134            let result = self.state.acquire(params);
135            core::mem::forget(guard);
136
137            match result {
138                Ok(permit) => match leader.remove_current(Ok(permit)) {
139                    Ok((_, waker)) => waker.wake(),
140                    // Safety: with protected_mut, we have just made sure it is in the list
141                    Err(_) => unsafe { unreachable_unchecked() },
142                },
143                Err(params) => {
144                    p.0 = Some(params);
145                    break;
146                }
147            }
148        }
149    }
150
151    /// Check if the queue is closed
152    pub fn is_closed(&self) -> bool {
153        self.queue.is_err()
154    }
155
156    /// Check if the queue has been poisoned.
157    ///
158    /// A queue becomes poisoned if a panic unwinds out of [`SemaphoreState::acquire`]
159    /// or a [`with_state`](Self::with_state) closure, which may have left the state
160    /// half-updated. Poisoning persists until [`clear_poison`](Self::clear_poison).
161    /// A poisoned queue stops granting permits; new acquire attempts surface the
162    /// poison rather than build on a corrupt state.
163    pub fn is_poisoned(&self) -> bool {
164        self.poisoned
165    }
166
167    /// Clear the poison flag, letting the queue grant permits again.
168    ///
169    /// The caller is responsible for ensuring the state is consistent first (e.g.
170    /// inspect/repair it with [`with_state`](Self::with_state)); like
171    /// [`std::sync::Mutex::clear_poison`], this does not itself touch the state.
172    ///
173    /// A blocking acquire whose `acquire` impl panicked lost its params and can
174    /// never complete; it is skipped until its future is dropped.
175    pub fn clear_poison(&mut self) {
176        self.poisoned = false;
177    }
178}
179
180impl<S: SemaphoreState + ?Sized> SemaphoreQueue<S, Closeable> {
181    /// Close the semaphore queue.
182    ///
183    /// All tasks currently waiting to acquire a token will immediately stop.
184    /// No new acquire attempts will succeed.
185    pub fn close(&mut self) {
186        let Ok(queue) = &mut self.queue else {
187            return;
188        };
189
190        let mut cursor = queue.cursor_front_mut();
191        while cursor.remove_current_with_or(
192            |(params, waker)| {
193                waker.wake();
194
195                Err(params)
196            },
197            || Err(None),
198        ) {}
199
200        debug_assert!(queue.is_empty());
201
202        // It's important that we only mark the queue as closed when we have ensured that
203        // all linked nodes are removed.
204        // If we did this early, we could panic and not dequeue every node.
205        self.queue = Err(());
206    }
207}
208
209#[cfg(all(test, loom))]
210mod loom_tests {
211    use crate::{SemaphoreQueue, closeable::Closeable};
212
213    #[derive(Debug)]
214    struct NeverSucceeds;
215
216    impl crate::SemaphoreState for NeverSucceeds {
217        type Params = ();
218        type Permit = ();
219
220        fn acquire(&mut self, _params: Self::Params) -> Result<Self::Permit, Self::Params> {
221            Err(())
222        }
223
224        fn release(&mut self, _permit: Self::Permit) {}
225    }
226
227    #[test]
228    fn concurrent_closed() {
229        loom::model(|| {
230            use std::sync::Arc;
231
232            let s = Arc::new(crate::loom::Mutex::<parking_lot::RawMutex, _>::new(
233                SemaphoreQueue::<NeverSucceeds, Closeable>::new(NeverSucceeds),
234            ));
235
236            let s2 = s.clone();
237            let handle = loom::thread::spawn(move || {
238                loom::future::block_on(async move {
239                    SemaphoreQueue::acquire(&s2, (), crate::acquire::FairOrder::Fifo)
240                        .await
241                        .unwrap_err()
242                })
243            });
244
245            s.lock().close();
246
247            handle.join().unwrap();
248        });
249    }
250}