base_scheduler/cancellation.rs
1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::Cancelled;
5use event_listener::Event;
6use std::future::pending;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex, PoisonError, Weak};
9
10struct Node {
11 cancelled: AtomicBool,
12 event: Event,
13 // Serialize child registration and cancellation across this signal family.
14 tree: Arc<Mutex<()>>,
15 // Children retain ancestors, but ancestors only hold weak child references.
16 parent: Option<Arc<Node>>,
17 // This node's slot in its parent's children.
18 index: usize,
19 children: Mutex<Children>,
20}
21
22// Weak child registrations, addressed by a slot each child records.
23#[derive(Default)]
24struct Children {
25 slots: Vec<Option<Weak<Node>>>,
26 free: Vec<usize>,
27}
28
29impl Children {
30 #[cfg(test)]
31 fn is_empty(&self) -> bool {
32 self.slots.iter().all(Option::is_none)
33 }
34 fn next(&self) -> usize {
35 self.free.last().copied().unwrap_or(self.slots.len())
36 }
37 fn insert(&mut self, child: Weak<Node>) {
38 match self.free.pop() {
39 Some(index) => self.slots[index] = Some(child),
40 None => self.slots.push(Some(child)),
41 }
42 }
43 fn remove(&mut self, index: usize) {
44 // A live child keeps its slot until it drops, and a cancelled parent
45 // takes every slot and registers no further children.
46 if let Some(slot) = self.slots.get_mut(index) {
47 *slot = None;
48 self.free.push(index);
49 }
50 }
51}
52
53impl Node {
54 fn new(tree: Arc<Mutex<()>>, parent: Option<Arc<Node>>, index: usize) -> Self {
55 let event = Event::new();
56 // Allocate shared event state during setup, rather than on the first wait.
57 event.notify(0);
58 Self {
59 cancelled: AtomicBool::new(false),
60 event,
61 tree,
62 parent,
63 index,
64 children: Mutex::new(Children::default()),
65 }
66 }
67}
68
69impl Drop for Node {
70 fn drop(&mut self) {
71 let mut index = self.index;
72 let mut parent = self.parent.take();
73 // Release an exclusively owned ancestor chain without recursive drops.
74 while let Some(node) = parent {
75 // A slot is reused only after its child's destruction removes it, so the index cannot alias a later registration.
76 node.children
77 .lock()
78 .unwrap_or_else(PoisonError::into_inner)
79 .remove(index);
80 index = node.index;
81 match Arc::try_unwrap(node) {
82 Ok(mut node) => {
83 parent = node.parent.take();
84 }
85 Err(_) => break,
86 }
87 }
88 }
89}
90
91/// A runtime-independent, cooperative cancellation signal.
92///
93/// Clones share a persistent cancellation state. Child signals receive ancestor cancellation, but cancelling a child does not cancel its parent or siblings. Dropping a signal does not request cancellation. Signals neither own tasks nor abort futures; callers decide how to respond and separately await completion.
94///
95/// ```
96/// use base_scheduler::{Cancellation, Cancelled};
97///
98/// let system = Cancellation::new();
99/// let server = system.child();
100/// assert!(server.cancel());
101/// assert_eq!(server.check(), Err(Cancelled));
102/// assert!(!system.is_cancelled());
103/// assert!(system.cancel());
104/// ```
105#[derive(Clone)]
106pub struct Cancellation {
107 node: Option<Arc<Node>>,
108}
109
110impl Cancellation {
111 /// Create an independent, uncancelled signal.
112 pub fn new() -> Self {
113 Self {
114 node: Some(Arc::new(Node::new(Arc::new(Mutex::new(())), None, 0))),
115 }
116 }
117
118 /// Create a signal which cannot be cancelled and requires no allocation. `cancel` always returns false and `cancelled` remains pending forever.
119 pub const fn never() -> Self {
120 Self { node: None }
121 }
122
123 /// Whether this signal can never be cancelled. This is true for [`Self::never`] and its clones; its children are independently cancellable. Backends can omit cancellation registration for these signals.
124 pub const fn is_never(&self) -> bool {
125 self.node.is_none()
126 }
127
128 /// Create a signal cancelled by this signal or any of its ancestors. A child of an already-cancelled signal starts cancelled. A child of a never-cancelled signal is an independent, cancellable signal.
129 pub fn child(&self) -> Self {
130 let Some(parent) = &self.node else {
131 return Self::new();
132 };
133 let _tree = parent.tree.lock().unwrap_or_else(PoisonError::into_inner);
134 let mut children = parent
135 .children
136 .lock()
137 .unwrap_or_else(PoisonError::into_inner);
138 let child = Arc::new(Node::new(
139 Arc::clone(&parent.tree),
140 Some(Arc::clone(parent)),
141 children.next(),
142 ));
143 if parent.cancelled.load(Ordering::Acquire) {
144 child.cancelled.store(true, Ordering::Release);
145 } else {
146 children.insert(Arc::downgrade(&child));
147 }
148 Self { node: Some(child) }
149 }
150
151 /// Request cancellation of this signal and all its descendants. Returns true only for the first request on this signal. Repeated requests never escalate. Waiters are woken after the entire family lock is released. Concurrent observers may see propagation in progress, but all descendants are cancelled when this call returns. This does not wait for their work.
152 pub fn cancel(&self) -> bool {
153 let Some(root) = &self.node else {
154 return false;
155 };
156 let tree = root.tree.lock().unwrap_or_else(PoisonError::into_inner);
157 if root.cancelled.load(Ordering::Acquire) {
158 return false;
159 }
160 let mut pending = vec![Arc::clone(root)];
161 let mut cancelled = Vec::new();
162 while let Some(node) = pending.pop() {
163 node.cancelled.store(true, Ordering::Release);
164 pending.extend(
165 std::mem::take(&mut *node.children.lock().unwrap_or_else(PoisonError::into_inner))
166 .slots
167 .into_iter()
168 .flatten()
169 .filter_map(|child| child.upgrade()),
170 );
171 cancelled.push(node);
172 }
173 drop(tree);
174 for node in cancelled {
175 node.event.notify(usize::MAX);
176 }
177 true
178 }
179
180 /// Whether cancellation has been requested.
181 #[inline]
182 pub fn is_cancelled(&self) -> bool {
183 self.node
184 .as_ref()
185 .is_some_and(|node| node.cancelled.load(Ordering::Acquire))
186 }
187
188 /// Check for cancellation at an explicit cooperative cancellation point.
189 #[inline]
190 pub fn check(&self) -> Result<(), Cancelled> {
191 if self.is_cancelled() {
192 Err(Cancelled)
193 } else {
194 Ok(())
195 }
196 }
197
198 /// Wait for cancellation using standard future polling and wakeups. This future is immediately ready if already cancelled. Dropping the wait unregisters its listener without cancelling the signal or other waiters. Listener storage is retained inline in the future, without a separate allocation.
199 pub async fn cancelled(&self) {
200 let Some(node) = &self.node else {
201 return pending().await;
202 };
203 event_listener::listener!(node.event => listener);
204 // Register before checking to preserve a racing cancellation notification.
205 if !node.cancelled.load(Ordering::Acquire) {
206 listener.await;
207 }
208 }
209}
210
211impl Default for Cancellation {
212 fn default() -> Self {
213 Self::new()
214 }
215}
216
217#[cfg(test)]
218mod tests;