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}