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/// A cancellation primitive with unified clone semantics.
42///
43/// This is the recommended default for most use cases. Clone it to share
44/// the cancellation state - any clone can cancel or check status.
45///
46/// # Example
47///
48/// ```rust
49/// use almost_enough::{Stopper, Stop};
50///
51/// let stop = Stopper::new();
52///
53/// // Pass a clone to another thread
54/// let stop2 = stop.clone();
55/// std::thread::spawn(move || {
56///     while !stop2.should_stop() {
57///         // do work
58///         break;
59///     }
60/// }).join().unwrap();
61///
62/// // Cancel from original
63/// stop.cancel();
64/// ```
65///
66/// # Performance
67///
68/// - Size: 8 bytes (one pointer)
69/// - `check()`: ~1-2ns (single atomic load with Relaxed ordering)
70/// - `clone()`: atomic increment
71/// - `cancel()`: atomic store
72#[derive(Debug, Clone)]
73pub struct Stopper {
74    cancelled: Arc<AtomicBool>,
75}
76
77impl Stopper {
78    /// Create a new stopper.
79    #[inline]
80    pub fn new() -> Self {
81        Self {
82            cancelled: Arc::new(AtomicBool::new(false)),
83        }
84    }
85
86    /// Create a stopper that is already cancelled.
87    ///
88    /// Useful for testing or when you want to signal immediate stop.
89    #[inline]
90    pub fn cancelled() -> Self {
91        Self {
92            cancelled: Arc::new(AtomicBool::new(true)),
93        }
94    }
95
96    /// Signal all clones to stop.
97    ///
98    /// This is idempotent - calling it multiple times has no additional effect.
99    #[inline]
100    pub fn cancel(&self) {
101        self.cancelled.store(true, Ordering::Relaxed);
102    }
103
104    /// Check if cancellation has been requested.
105    #[inline]
106    pub fn is_cancelled(&self) -> bool {
107        self.cancelled.load(Ordering::Relaxed)
108    }
109}
110
111impl Default for Stopper {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl Stop for Stopper {
118    #[inline]
119    fn check(&self) -> Result<(), StopReason> {
120        if self.cancelled.load(Ordering::Relaxed) {
121            Err(StopReason::Cancelled)
122        } else {
123            Ok(())
124        }
125    }
126
127    #[inline]
128    fn should_stop(&self) -> bool {
129        self.cancelled.load(Ordering::Relaxed)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn stopper_basic() {
139        let stop = Stopper::new();
140        assert!(!stop.is_cancelled());
141        assert!(!stop.should_stop());
142        assert!(stop.check().is_ok());
143
144        stop.cancel();
145
146        assert!(stop.is_cancelled());
147        assert!(stop.should_stop());
148        assert_eq!(stop.check(), Err(StopReason::Cancelled));
149    }
150
151    #[test]
152    fn stopper_cancelled_constructor() {
153        let stop = Stopper::cancelled();
154        assert!(stop.is_cancelled());
155        assert!(stop.should_stop());
156    }
157
158    #[test]
159    fn stopper_clone_shares_state() {
160        let stop1 = Stopper::new();
161        let stop2 = stop1.clone();
162
163        assert!(!stop1.should_stop());
164        assert!(!stop2.should_stop());
165
166        // Either clone can cancel
167        stop2.cancel();
168
169        assert!(stop1.should_stop());
170        assert!(stop2.should_stop());
171    }
172
173    #[test]
174    fn stopper_is_default() {
175        let stop: Stopper = Default::default();
176        assert!(!stop.is_cancelled());
177    }
178
179    #[test]
180    fn stopper_is_send_sync() {
181        fn assert_send_sync<T: Send + Sync>() {}
182        assert_send_sync::<Stopper>();
183    }
184
185    #[test]
186    fn stopper_can_outlive_original() {
187        let stop2 = {
188            let stop1 = Stopper::new();
189            stop1.clone()
190        };
191        // Original is dropped, but clone still works
192        assert!(!stop2.should_stop());
193    }
194
195    #[test]
196    fn cancel_is_idempotent() {
197        let stop = Stopper::new();
198        stop.cancel();
199        stop.cancel();
200        stop.cancel();
201        assert!(stop.is_cancelled());
202    }
203}