almost_enough/source.rs
1//! Zero-allocation cancellation primitives.
2//!
3//! This module provides stack-based cancellation using borrowed references.
4//! Works in `no_std` environments without an allocator.
5//!
6//! # Overview
7//!
8//! - [`StopSource`] - A cancellation source that owns an `AtomicBool` on the stack
9//! - [`StopRef`] - A borrowed reference to check cancellation
10//!
11//! # Example
12//!
13//! ```rust
14//! use almost_enough::{StopSource, Stop};
15//!
16//! let source = StopSource::new();
17//! let stop = source.as_ref();
18//!
19//! assert!(!stop.should_stop());
20//!
21//! source.cancel();
22//! assert!(stop.should_stop());
23//! ```
24//!
25//! # When to Use
26//!
27//! Use `StopSource`/`StopRef` when:
28//! - You need zero-allocation cancellation
29//! - The source outlives all references (stack-based usage)
30//! - You're in a `no_std` environment
31//!
32//! Use [`Stopper`](crate::Stopper) when:
33//! - You need to share ownership (clone instead of borrow)
34//! - You want to pass stops across thread boundaries without lifetimes
35
36use core::sync::atomic::{AtomicBool, Ordering};
37
38use crate::{Stop, StopReason};
39
40/// A stack-based cancellation source.
41///
42/// This is a zero-allocation cancellation primitive. The source owns the
43/// atomic and can issue borrowed references via [`as_ref()`](Self::as_ref).
44///
45/// # Example
46///
47/// ```rust
48/// use almost_enough::{StopSource, Stop};
49///
50/// let source = StopSource::new();
51/// let stop = source.as_ref();
52///
53/// // Check in your operation
54/// assert!(!stop.should_stop());
55///
56/// // Cancel when needed
57/// source.cancel();
58/// assert!(stop.should_stop());
59/// ```
60///
61/// # Const Construction
62///
63/// `StopSource` can be created in const context:
64///
65/// ```rust
66/// use almost_enough::StopSource;
67///
68/// static GLOBAL_STOP: StopSource = StopSource::new();
69/// ```
70#[derive(Debug)]
71pub struct StopSource {
72 cancelled: AtomicBool,
73}
74
75impl StopSource {
76 /// Create a new cancellation source.
77 #[inline]
78 pub const fn new() -> Self {
79 Self {
80 cancelled: AtomicBool::new(false),
81 }
82 }
83
84 /// Create a source that is already cancelled.
85 ///
86 /// Useful for testing or when you want to signal immediate stop.
87 #[inline]
88 pub const fn cancelled() -> Self {
89 Self {
90 cancelled: AtomicBool::new(true),
91 }
92 }
93
94 /// Signal all references to stop.
95 ///
96 /// This is idempotent - calling it multiple times has no additional effect.
97 #[inline]
98 pub fn cancel(&self) {
99 self.cancelled.store(true, Ordering::Relaxed);
100 }
101
102 /// Check if this source has been cancelled.
103 #[inline]
104 pub fn is_cancelled(&self) -> bool {
105 self.cancelled.load(Ordering::Relaxed)
106 }
107
108 /// Get a borrowed reference to pass to operations.
109 ///
110 /// The reference borrows from this source, so it cannot outlive it.
111 /// For owned stops, use [`Stopper`](crate::Stopper).
112 #[inline]
113 pub fn as_ref(&self) -> StopRef<'_> {
114 StopRef {
115 cancelled: &self.cancelled,
116 }
117 }
118
119 /// Alias for [`as_ref()`](Self::as_ref) for migration from AtomicStop.
120 #[inline]
121 #[deprecated(since = "0.1.0", note = "use as_ref() instead")]
122 pub fn token(&self) -> StopRef<'_> {
123 self.as_ref()
124 }
125}
126
127impl Default for StopSource {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133impl Stop for StopSource {
134 #[inline]
135 fn check(&self) -> Result<(), StopReason> {
136 if self.cancelled.load(Ordering::Relaxed) {
137 Err(StopReason::Cancelled)
138 } else {
139 Ok(())
140 }
141 }
142
143 #[inline]
144 fn should_stop(&self) -> bool {
145 self.cancelled.load(Ordering::Relaxed)
146 }
147}
148
149/// A borrowed reference to a [`StopSource`].
150///
151/// This is a lightweight reference that can only check for cancellation -
152/// it cannot trigger it. Use the source to cancel.
153///
154/// # Example
155///
156/// ```rust
157/// use almost_enough::{StopSource, Stop};
158///
159/// fn process(data: &[u8], stop: impl Stop) {
160/// for (i, chunk) in data.chunks(100).enumerate() {
161/// if i % 10 == 0 && stop.should_stop() {
162/// return;
163/// }
164/// // process chunk...
165/// }
166/// }
167///
168/// let source = StopSource::new();
169/// process(&[0u8; 1000], source.as_ref());
170/// ```
171///
172/// # Copy Semantics
173///
174/// `StopRef` is `Copy`, so you can freely copy it without cloning:
175///
176/// ```rust
177/// use almost_enough::{StopSource, Stop};
178///
179/// let source = StopSource::new();
180/// let r1 = source.as_ref();
181/// let r2 = r1; // Copy
182/// let r3 = r1; // Still valid
183///
184/// source.cancel();
185/// assert!(r1.should_stop());
186/// assert!(r2.should_stop());
187/// assert!(r3.should_stop());
188/// ```
189#[derive(Debug, Clone, Copy)]
190pub struct StopRef<'a> {
191 cancelled: &'a AtomicBool,
192}
193
194impl Stop for StopRef<'_> {
195 #[inline]
196 fn check(&self) -> Result<(), StopReason> {
197 if self.cancelled.load(Ordering::Relaxed) {
198 Err(StopReason::Cancelled)
199 } else {
200 Ok(())
201 }
202 }
203
204 #[inline]
205 fn should_stop(&self) -> bool {
206 self.cancelled.load(Ordering::Relaxed)
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn stop_source_basic() {
216 let source = StopSource::new();
217 assert!(!source.is_cancelled());
218 assert!(!source.should_stop());
219 assert!(source.check().is_ok());
220
221 source.cancel();
222
223 assert!(source.is_cancelled());
224 assert!(source.should_stop());
225 assert_eq!(source.check(), Err(StopReason::Cancelled));
226 }
227
228 #[test]
229 fn stop_source_cancelled_constructor() {
230 let source = StopSource::cancelled();
231 assert!(source.is_cancelled());
232 assert!(source.should_stop());
233 }
234
235 #[test]
236 fn stop_ref_basic() {
237 let source = StopSource::new();
238 let stop = source.as_ref();
239
240 assert!(!stop.should_stop());
241 assert!(stop.check().is_ok());
242
243 source.cancel();
244
245 assert!(stop.should_stop());
246 assert_eq!(stop.check(), Err(StopReason::Cancelled));
247 }
248
249 #[test]
250 fn stop_ref_is_copy() {
251 let source = StopSource::new();
252 let r1 = source.as_ref();
253 let r2 = r1; // Copy
254 let _ = r1; // Still valid
255 let _ = r2;
256 }
257
258 #[test]
259 fn stop_source_is_default() {
260 let source: StopSource = Default::default();
261 assert!(!source.is_cancelled());
262 }
263
264 #[test]
265 fn stop_source_is_send_sync() {
266 fn assert_send_sync<T: Send + Sync>() {}
267 assert_send_sync::<StopSource>();
268 assert_send_sync::<StopRef<'_>>();
269 }
270
271 #[test]
272 fn cancel_is_idempotent() {
273 let source = StopSource::new();
274 source.cancel();
275 source.cancel();
276 source.cancel();
277 assert!(source.is_cancelled());
278 }
279
280 #[test]
281 fn const_construction() {
282 static SOURCE: StopSource = StopSource::new();
283 assert!(!SOURCE.is_cancelled());
284 }
285}