Skip to main content

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}