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}