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//! Uses Relaxed ordering for best performance. If you need to synchronize
34//! other memory writes with cancellation, use [`SyncStopper`](crate::SyncStopper).
35
36use alloc::sync::Arc;
37use core::sync::atomic::{AtomicBool, Ordering};
38
39use crate::{Stop, StopReason};
40
41/// Inner state for [`Stopper`] — implements [`Stop`] directly so that
42/// `Arc<StopperInner>` can be widened to `Arc<dyn Stop>` without double-wrapping.
43pub(crate) struct StopperInner {
44    cancelled: AtomicBool,
45}
46
47impl Stop for StopperInner {
48    #[inline]
49    fn check(&self) -> Result<(), StopReason> {
50        if self.cancelled.load(Ordering::Relaxed) {
51            Err(StopReason::Cancelled)
52        } else {
53            Ok(())
54        }
55    }
56
57    #[inline]
58    fn should_stop(&self) -> bool {
59        self.cancelled.load(Ordering::Relaxed)
60    }
61}
62
63/// A cancellation primitive with unified clone semantics.
64///
65/// This is the recommended default for most use cases. Clone it to share
66/// the cancellation state - any clone can cancel or check status.
67///
68/// Converts to [`StopToken`](crate::StopToken) via `From`/`Into` with zero
69/// overhead — the existing `Arc` is reused, not double-wrapped.
70///
71/// # Example
72///
73/// ```rust
74/// use almost_enough::{Stopper, Stop};
75///
76/// let stop = Stopper::new();
77///
78/// // Pass a clone to another thread
79/// let stop2 = stop.clone();
80/// std::thread::spawn(move || {
81///     while !stop2.should_stop() {
82///         // do work
83///         break;
84///     }
85/// }).join().unwrap();
86///
87/// // Cancel from original
88/// stop.cancel();
89/// ```
90///
91/// # Performance
92///
93/// - Size: 8 bytes (one pointer)
94/// - `check()`: ~1-2ns (single atomic load with Relaxed ordering)
95/// - `clone()`: atomic increment
96/// - `cancel()`: atomic store
97/// - `into() -> StopToken`: zero-cost (Arc pointer widening)
98#[derive(Debug, Clone)]
99pub struct Stopper {
100    pub(crate) inner: Arc<StopperInner>,
101}
102
103impl Stopper {
104    /// Create a new stopper.
105    #[inline]
106    pub fn new() -> Self {
107        Self {
108            inner: Arc::new(StopperInner {
109                cancelled: AtomicBool::new(false),
110            }),
111        }
112    }
113
114    /// Create a stopper that is already cancelled.
115    ///
116    /// Useful for testing or when you want to signal immediate stop.
117    #[inline]
118    pub fn cancelled() -> Self {
119        Self {
120            inner: Arc::new(StopperInner {
121                cancelled: AtomicBool::new(true),
122            }),
123        }
124    }
125
126    /// Signal all clones to stop.
127    ///
128    /// This is idempotent - calling it multiple times has no additional effect.
129    #[inline]
130    pub fn cancel(&self) {
131        self.inner.cancelled.store(true, Ordering::Relaxed);
132    }
133
134    /// Check if cancellation has been requested.
135    #[inline]
136    pub fn is_cancelled(&self) -> bool {
137        self.inner.cancelled.load(Ordering::Relaxed)
138    }
139}
140
141impl Default for Stopper {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147impl Stop for Stopper {
148    #[inline]
149    fn check(&self) -> Result<(), StopReason> {
150        self.inner.check()
151    }
152
153    #[inline]
154    fn should_stop(&self) -> bool {
155        self.inner.should_stop()
156    }
157}
158
159impl core::fmt::Debug for StopperInner {
160    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
161        f.debug_struct("StopperInner")
162            .field("cancelled", &self.cancelled.load(Ordering::Relaxed))
163            .finish()
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn stopper_basic() {
173        let stop = Stopper::new();
174        assert!(!stop.is_cancelled());
175        assert!(!stop.should_stop());
176        assert!(stop.check().is_ok());
177
178        stop.cancel();
179
180        assert!(stop.is_cancelled());
181        assert!(stop.should_stop());
182        assert_eq!(stop.check(), Err(StopReason::Cancelled));
183    }
184
185    #[test]
186    fn stopper_cancelled_constructor() {
187        let stop = Stopper::cancelled();
188        assert!(stop.is_cancelled());
189        assert!(stop.should_stop());
190    }
191
192    #[test]
193    fn stopper_clone_shares_state() {
194        let stop1 = Stopper::new();
195        let stop2 = stop1.clone();
196
197        assert!(!stop1.should_stop());
198        assert!(!stop2.should_stop());
199
200        // Either clone can cancel
201        stop2.cancel();
202
203        assert!(stop1.should_stop());
204        assert!(stop2.should_stop());
205    }
206
207    #[test]
208    fn stopper_is_default() {
209        let stop: Stopper = Default::default();
210        assert!(!stop.is_cancelled());
211    }
212
213    #[test]
214    fn stopper_is_send_sync() {
215        fn assert_send_sync<T: Send + Sync>() {}
216        assert_send_sync::<Stopper>();
217    }
218
219    #[test]
220    fn stopper_can_outlive_original() {
221        let stop2 = {
222            let stop1 = Stopper::new();
223            stop1.clone()
224        };
225        // Original is dropped, but clone still works
226        assert!(!stop2.should_stop());
227    }
228
229    #[test]
230    fn cancel_is_idempotent() {
231        let stop = Stopper::new();
232        stop.cancel();
233        stop.cancel();
234        stop.cancel();
235        assert!(stop.is_cancelled());
236    }
237}