almost_enough/stopper.rs
1//! The default cancellation primitive.
2//!
3//! [`Stopper`] is the recommended type for most use cases. It's a simple,
4//! Arc-based cancellation flag with unified clone semantics.
5//!
6//! # Example
7//!
8//! ```rust
9//! use almost_enough::{Stopper, Stop};
10//!
11//! let stop = Stopper::new();
12//! let stop2 = stop.clone(); // Both share the same flag
13//!
14//! assert!(!stop.should_stop());
15//!
16//! stop2.cancel(); // Any clone can cancel
17//! assert!(stop.should_stop());
18//! ```
19//!
20//! # Design
21//!
22//! `Stopper` uses a unified clone model (like tokio's `CancellationToken`):
23//! - Just clone to share - no separate "token" type
24//! - Any clone can call `cancel()`
25//! - Any clone can check `should_stop()`
26//!
27//! This is simpler than source/token split but means you can't prevent
28//! a recipient from cancelling. If you need that, use
29//! [`StopSource`](crate::StopSource)/[`StopRef`](crate::StopRef).
30//!
31//! # Memory Ordering
32//!
33//! `cancel()` is a Release swap and checks are Relaxed loads, so a check
34//! costs one plain load. To read what the cancelling thread wrote before
35//! `cancel()`, call [`Stopper::is_cancelled`], an Acquire load, after the
36//! work reports the cancellation. [`SyncStopper`](crate::SyncStopper) makes every
37//! check an Acquire load instead.
38
39use alloc::sync::Arc;
40use core::sync::atomic::{AtomicBool, Ordering};
41
42use crate::{Stop, StopReason};
43
44/// Inner state for [`Stopper`] — implements [`Stop`] directly so that
45/// `Arc<StopperInner>` can be widened to `Arc<dyn Stop>` without double-wrapping.
46pub(crate) struct StopperInner {
47 cancelled: AtomicBool,
48}
49
50impl Stop for StopperInner {
51 #[inline]
52 #[track_caller]
53 fn check(&self) -> Result<(), StopReason> {
54 if self.cancelled.load(Ordering::Relaxed) {
55 Err(StopReason::Cancelled)
56 } else {
57 Ok(())
58 }
59 }
60
61 #[inline]
62 #[track_caller]
63 fn should_stop(&self) -> bool {
64 self.cancelled.load(Ordering::Relaxed)
65 }
66}
67
68/// A cancellation primitive with unified clone semantics.
69///
70/// This is the recommended default for most use cases. Clone it to share
71/// the cancellation state - any clone can cancel or check status.
72///
73/// Converts to [`StopToken`](crate::StopToken) via `From`/`Into` with zero
74/// overhead — the existing `Arc` is reused, not double-wrapped.
75///
76/// # Example
77///
78/// ```rust
79/// use almost_enough::{Stopper, Stop};
80///
81/// let stop = Stopper::new();
82///
83/// // Pass a clone to another thread
84/// let stop2 = stop.clone();
85/// std::thread::spawn(move || {
86/// while !stop2.should_stop() {
87/// // do work
88/// break;
89/// }
90/// }).join().unwrap();
91///
92/// // Cancel from original
93/// stop.cancel();
94/// ```
95///
96/// # Performance
97///
98/// - Size: 8 bytes (one pointer)
99/// - `check()`: ~1-2ns (single atomic load with Relaxed ordering)
100/// - `clone()`: atomic increment
101/// - `cancel()`: atomic store
102/// - `into() -> StopToken`: zero-cost (Arc pointer widening)
103#[derive(Debug, Clone)]
104pub struct Stopper {
105 pub(crate) inner: Arc<StopperInner>,
106}
107
108impl Stopper {
109 /// Create a new stopper.
110 #[inline]
111 pub fn new() -> Self {
112 Self {
113 inner: Arc::new(StopperInner {
114 cancelled: AtomicBool::new(false),
115 }),
116 }
117 }
118
119 /// Create a stopper that is already cancelled.
120 ///
121 /// Useful for testing or when you want to signal immediate stop.
122 #[inline]
123 pub fn cancelled() -> Self {
124 Self {
125 inner: Arc::new(StopperInner {
126 cancelled: AtomicBool::new(true),
127 }),
128 }
129 }
130
131 /// Signal all clones to stop.
132 ///
133 /// A Release swap: writes made before a `cancel()` are visible to a
134 /// thread whose [`is_cancelled`](Self::is_cancelled) returns `true` after
135 /// it, even when several threads cancel. Calling it again changes nothing
136 /// else.
137 #[inline]
138 pub fn cancel(&self) {
139 // A swap, not a store: a second canceller's plain store would end the
140 // first one's release sequence, and a reader that saw only the second
141 // could miss the first canceller's writes.
142 self.inner.cancelled.swap(true, Ordering::Release);
143 }
144
145 /// Whether cancellation has been requested, as an Acquire load.
146 ///
147 /// When it returns `true`, this thread also sees every write the
148 /// cancelling thread made before [`cancel`](Self::cancel), such as a
149 /// reason code kept in an atomic. Call it after the work reports the
150 /// stop. In a loop, use [`check`](Stop::check) or
151 /// [`should_stop`](Stop::should_stop): one Relaxed load each, which see
152 /// the stop just as soon but promise nothing about other writes.
153 #[inline]
154 pub fn is_cancelled(&self) -> bool {
155 self.inner.cancelled.load(Ordering::Acquire)
156 }
157}
158
159impl Default for Stopper {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165impl Stop for Stopper {
166 #[inline]
167 #[track_caller]
168 fn check(&self) -> Result<(), StopReason> {
169 self.inner.check()
170 }
171
172 #[inline]
173 #[track_caller]
174 fn should_stop(&self) -> bool {
175 self.inner.should_stop()
176 }
177}
178
179impl core::fmt::Debug for StopperInner {
180 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
181 f.debug_struct("StopperInner")
182 .field("cancelled", &self.cancelled.load(Ordering::Relaxed))
183 .finish()
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 /// The writer cancels after a Relaxed store; the reader waits with
192 /// Relaxed checks, then calls `is_cancelled()`. Miri's weak-memory
193 /// emulation fails this if `cancel()` isn't Release or `is_cancelled()`
194 /// isn't Acquire; CI runs it over many seeds.
195 #[cfg(feature = "std")]
196 #[test]
197 fn is_cancelled_after_the_stop_sees_writes_made_before_cancel() {
198 use core::sync::atomic::{AtomicU32, Ordering::Relaxed};
199 for _ in 0..if cfg!(miri) { 30 } else { 1_000 } {
200 let stop = Stopper::new();
201 let data = Arc::new(AtomicU32::new(0));
202 let reader = {
203 let (stop, data) = (stop.clone(), Arc::clone(&data));
204 std::thread::spawn(move || {
205 while !stop.should_stop() {
206 core::hint::spin_loop();
207 }
208 assert!(stop.is_cancelled());
209 data.load(Relaxed)
210 })
211 };
212 data.store(42, Relaxed);
213 stop.cancel();
214 assert_eq!(reader.join().unwrap(), 42);
215 }
216 }
217
218 #[cfg(feature = "std")]
219 #[test]
220 fn is_cancelled_after_a_second_cancel_sees_the_first_cancellers_writes() {
221 // A second canceller (a guard dropping, a watchdog) cancels again
222 // after seeing the first cancel through a Relaxed check.
223 use core::sync::atomic::{AtomicU32, Ordering::Relaxed};
224 for _ in 0..if cfg!(miri) { 30 } else { 1_000 } {
225 let stop = Stopper::new();
226 let data = AtomicU32::new(0);
227 std::thread::scope(|scope| {
228 let reader = scope.spawn(|| {
229 while !stop.should_stop() {
230 core::hint::spin_loop();
231 }
232 assert!(stop.is_cancelled());
233 data.load(Relaxed)
234 });
235 scope.spawn(|| {
236 while !stop.should_stop() {
237 core::hint::spin_loop();
238 }
239 stop.cancel();
240 });
241 data.store(42, Relaxed);
242 stop.cancel();
243 assert_eq!(reader.join().unwrap(), 42);
244 });
245 }
246 }
247
248 #[test]
249 fn stopper_basic() {
250 let stop = Stopper::new();
251 assert!(!stop.is_cancelled());
252 assert!(!stop.should_stop());
253 assert!(stop.check().is_ok());
254
255 stop.cancel();
256
257 assert!(stop.is_cancelled());
258 assert!(stop.should_stop());
259 assert_eq!(stop.check(), Err(StopReason::Cancelled));
260 }
261
262 #[test]
263 fn stopper_cancelled_constructor() {
264 let stop = Stopper::cancelled();
265 assert!(stop.is_cancelled());
266 assert!(stop.should_stop());
267 }
268
269 #[test]
270 fn stopper_clone_shares_state() {
271 let stop1 = Stopper::new();
272 let stop2 = stop1.clone();
273
274 assert!(!stop1.should_stop());
275 assert!(!stop2.should_stop());
276
277 // Either clone can cancel
278 stop2.cancel();
279
280 assert!(stop1.should_stop());
281 assert!(stop2.should_stop());
282 }
283
284 #[test]
285 fn stopper_is_default() {
286 let stop: Stopper = Default::default();
287 assert!(!stop.is_cancelled());
288 }
289
290 #[test]
291 fn stopper_is_send_sync() {
292 fn assert_send_sync<T: Send + Sync>() {}
293 assert_send_sync::<Stopper>();
294 }
295
296 #[test]
297 fn stopper_can_outlive_original() {
298 let stop2 = {
299 let stop1 = Stopper::new();
300 stop1.clone()
301 };
302 // Original is dropped, but clone still works
303 assert!(!stop2.should_stop());
304 }
305
306 #[test]
307 fn cancel_is_idempotent() {
308 let stop = Stopper::new();
309 stop.cancel();
310 stop.cancel();
311 stop.cancel();
312 assert!(stop.is_cancelled());
313 }
314}