Skip to main content

almost_enough/
lib.rs

1//! # almost-enough
2//!
3//! Cooperative cancellation for Rust. Cancel long-running operations from another thread.
4//!
5//! ## Which Crate?
6//!
7//! - **Application code**: Use `almost-enough` (this crate) - has all implementations
8//! - **Library code**: Depend on [`enough`](https://docs.rs/enough) (minimal) and accept `impl Stop`
9//!
10//! ## Complete Example
11//!
12//! ```rust
13//! # #[cfg(feature = "alloc")]
14//! # fn main() {
15//! use almost_enough::{Stopper, Stop};
16//! use std::thread;
17//! use std::time::Duration;
18//!
19//! // 1. Create a stopper
20//! let stop = Stopper::new();
21//!
22//! // 2. Clone and pass to worker thread
23//! let worker_stop = stop.clone();
24//! let handle = thread::spawn(move || {
25//!     for i in 0..1000 {
26//!         // 3. Check periodically - exit early if cancelled
27//!         if worker_stop.should_stop() {
28//!             return Err("cancelled");
29//!         }
30//!         // ... do work ...
31//!         # std::hint::black_box(i);
32//!     }
33//!     Ok("completed")
34//! });
35//!
36//! // 4. Cancel from main thread (or signal handler, timeout, etc.)
37//! thread::sleep(Duration::from_millis(1));
38//! stop.cancel();
39//!
40//! // Worker exits early
41//! let result = handle.join().unwrap();
42//! // result is either Ok("completed") or Err("cancelled")
43//! # }
44//! # #[cfg(not(feature = "alloc"))]
45//! # fn main() {}
46//! ```
47//!
48//! ## Quick Reference
49//!
50//! ```rust,no_run
51//! # #[cfg(feature = "alloc")]
52//! # fn main() {
53//! # use almost_enough::{Stopper, Stop, StopReason};
54//! # fn example() -> Result<(), StopReason> {
55//! let stop = Stopper::new();
56//! stop.cancel();              // Trigger cancellation
57//! stop.should_stop();         // Returns true if cancelled
58//! stop.check()?;              // Returns Err(StopReason) if cancelled
59//! # Ok(())
60//! # }
61//! # }
62//! # #[cfg(not(feature = "alloc"))]
63//! # fn main() {}
64//! ```
65//!
66//! ## Type Overview
67//!
68//! | Type | Feature | Use Case |
69//! |------|---------|----------|
70//! | [`Unstoppable`] | core | Zero-cost "never stop" |
71//! | [`StopSource`] / [`StopRef`] | core | Stack-based, borrowed, zero-alloc |
72//! | [`FnStop`] | core | Wrap any closure |
73//! | [`OrStop`] | core | Combine multiple stops |
74//! | [`Stopper`] | alloc | **Default choice** - Arc-based, clone to share |
75//! | [`SyncStopper`] | alloc | Like Stopper with Acquire/Release ordering |
76//! | [`ChildStopper`] | alloc | Hierarchical parent-child cancellation |
77//! | [`StopToken`] | alloc | **Type-erased dynamic dispatch** - Arc-based, `Clone` |
78//! | [`BoxedStop`] | alloc | Type-erased (prefer `StopToken`) |
79//! | [`WithTimeout`] | std | Add deadline to any `Stop` |
80//!
81//! ## StopExt Extension Trait
82//!
83//! The [`StopExt`] trait adds combinator methods to any [`Stop`] implementation:
84//!
85//! ```rust
86//! use almost_enough::{StopSource, Stop, StopExt};
87//!
88//! let timeout = StopSource::new();
89//! let cancel = StopSource::new();
90//!
91//! // Combine: stop if either stops
92//! let combined = timeout.as_ref().or(cancel.as_ref());
93//! assert!(!combined.should_stop());
94//!
95//! cancel.cancel();
96//! assert!(combined.should_stop());
97//! ```
98//!
99//! ## Type Erasure with `StopToken`
100//!
101//! [`StopToken`] wraps `Arc<dyn Stop>` — it's `Clone` (cheap Arc increment),
102//! type-erased, and can be sent across threads. [`Stopper`] and
103//! [`SyncStopper`] convert to `StopToken` at zero cost via `From`/`Into`
104//! (the existing Arc is reused, no double-wrapping).
105//!
106//! Use [`CloneStop`] (a trait alias for `Stop + Clone + 'static`) to accept
107//! any clonable stop, then erase with `into_token()` at the boundary:
108//!
109//! ```rust
110//! # #[cfg(feature = "alloc")]
111//! # fn main() {
112//! use almost_enough::{CloneStop, StopToken, Stopper, Stop, StopExt};
113//!
114//! fn outer(stop: impl CloneStop) {
115//!     // Erase the concrete type — StopToken is Clone
116//!     let stop: StopToken = stop.into_token();
117//!     inner(&stop);
118//! }
119//!
120//! fn inner(stop: &StopToken) {
121//!     let stop2 = stop.clone(); // cheap Arc increment
122//!     // Only one version of this function exists
123//!     while !stop.should_stop() {
124//!         break;
125//!     }
126//! }
127//!
128//! let stop = Stopper::new();
129//! outer(stop);
130//! # }
131//! # #[cfg(not(feature = "alloc"))]
132//! # fn main() {}
133//! ```
134//!
135//! ## Hierarchical Cancellation with `.child()`
136//!
137//! Create child stops that inherit cancellation from their parent:
138//!
139//! ```rust
140//! # #[cfg(feature = "alloc")]
141//! # fn main() {
142//! use almost_enough::{Stopper, Stop, StopExt};
143//!
144//! let parent = Stopper::new();
145//! let child = parent.child();
146//!
147//! // Child cancellation doesn't affect parent
148//! child.cancel();
149//! assert!(!parent.should_stop());
150//!
151//! // But parent cancellation propagates to children
152//! let child2 = parent.child();
153//! parent.cancel();
154//! assert!(child2.should_stop());
155//! # }
156//! # #[cfg(not(feature = "alloc"))]
157//! # fn main() {}
158//! ```
159//!
160//! ## Stop Guards (RAII Cancellation)
161//!
162//! Automatically stop on scope exit unless explicitly disarmed:
163//!
164//! ```rust
165//! # #[cfg(feature = "alloc")]
166//! # fn main() {
167//! use almost_enough::{Stopper, StopDropRoll};
168//!
169//! fn do_work(source: &Stopper) -> Result<(), &'static str> {
170//!     let guard = source.stop_on_drop();
171//!
172//!     // If we return early or panic, source is stopped
173//!     risky_operation()?;
174//!
175//!     // Success! Don't stop.
176//!     guard.disarm();
177//!     Ok(())
178//! }
179//!
180//! fn risky_operation() -> Result<(), &'static str> {
181//!     Ok(())
182//! }
183//!
184//! let source = Stopper::new();
185//! do_work(&source).unwrap();
186//! # }
187//! # #[cfg(not(feature = "alloc"))]
188//! # fn main() {}
189//! ```
190//!
191//! ## Feature Flags
192//!
193//! - **`std`** (default) - Full functionality including timeouts
194//! - **`alloc`** - Arc-based types, `into_boxed()`, `child()`, `StopDropRoll`
195//! - **None** - Core trait and stack-based types only
196
197#![cfg_attr(not(feature = "std"), no_std)]
198#![forbid(unsafe_code)]
199#![warn(missing_docs)]
200#![warn(clippy::all)]
201
202#[cfg(feature = "alloc")]
203extern crate alloc;
204
205// Re-export everything from enough
206#[allow(deprecated)]
207pub use enough::{Never, Stop, StopReason, Unstoppable};
208
209/// Trait alias for stop tokens that can be cloned and sent across threads.
210///
211/// This is `Stop + Clone + 'static` — the minimum needed to clone a stop
212/// token and send it to other threads. Since `Stop` already requires
213/// `Send + Sync`, `CloneStop` types are fully thread-safe.
214///
215/// Use `impl CloneStop` in public API signatures when you need to clone
216/// the stop token, then erase with [`StopToken`] internally:
217///
218/// ```rust
219/// # #[cfg(feature = "alloc")]
220/// # fn main() {
221/// use almost_enough::{CloneStop, StopToken, Stop};
222///
223/// pub fn parallel_work(stop: impl CloneStop) {
224///     let stop = StopToken::new(stop);
225///     let s2 = stop.clone(); // Arc increment
226/// }
227/// # }
228/// # #[cfg(not(feature = "alloc"))]
229/// # fn main() {}
230/// ```
231pub trait CloneStop: Stop + Clone + 'static {}
232
233/// Blanket implementation: any `Stop + Clone + 'static` is `CloneStop`.
234impl<T: Stop + Clone + 'static> CloneStop for T {}
235
236// Core modules (no_std, no alloc)
237mod func;
238mod or;
239mod source;
240
241pub use func::FnStop;
242pub use or::OrStop;
243pub use source::{StopRef, StopSource};
244
245// Alloc-dependent modules
246#[cfg(feature = "alloc")]
247mod boxed;
248#[cfg(feature = "alloc")]
249mod stopper;
250#[cfg(feature = "alloc")]
251mod sync_stopper;
252#[cfg(feature = "alloc")]
253mod tree;
254
255#[cfg(feature = "alloc")]
256pub use boxed::BoxedStop;
257#[cfg(feature = "alloc")]
258mod stop_token;
259#[cfg(feature = "alloc")]
260pub use stop_token::StopToken;
261#[cfg(feature = "alloc")]
262pub use stopper::Stopper;
263#[cfg(feature = "alloc")]
264pub use sync_stopper::SyncStopper;
265#[cfg(feature = "alloc")]
266pub use tree::ChildStopper;
267
268// Std-dependent modules
269#[cfg(feature = "std")]
270pub mod time;
271#[cfg(feature = "std")]
272pub use time::{TimeoutExt, WithTimeout};
273
274// Cancel guard module
275#[cfg(feature = "alloc")]
276mod guard;
277#[cfg(feature = "alloc")]
278pub use guard::{CancelGuard, Cancellable, StopDropRoll};
279
280/// Extension trait providing ergonomic combinators for [`Stop`] implementations.
281///
282/// This trait is automatically implemented for all `Stop + Sized` types.
283///
284/// # Example
285///
286/// ```rust
287/// use almost_enough::{StopSource, Stop, StopExt};
288///
289/// let source_a = StopSource::new();
290/// let source_b = StopSource::new();
291///
292/// // Combine with .or()
293/// let combined = source_a.as_ref().or(source_b.as_ref());
294///
295/// assert!(!combined.should_stop());
296///
297/// source_b.cancel();
298/// assert!(combined.should_stop());
299/// ```
300pub trait StopExt: Stop + Sized {
301    /// Combine this stop with another, stopping if either stops.
302    ///
303    /// This is equivalent to `OrStop::new(self, other)` but with a more
304    /// ergonomic method syntax that allows chaining.
305    ///
306    /// # Example
307    ///
308    /// ```rust
309    /// use almost_enough::{StopSource, Stop, StopExt};
310    ///
311    /// let timeout = StopSource::new();
312    /// let cancel = StopSource::new();
313    ///
314    /// let combined = timeout.as_ref().or(cancel.as_ref());
315    /// assert!(!combined.should_stop());
316    ///
317    /// cancel.cancel();
318    /// assert!(combined.should_stop());
319    /// ```
320    ///
321    /// # Chaining
322    ///
323    /// Multiple sources can be chained:
324    ///
325    /// ```rust
326    /// use almost_enough::{StopSource, Stop, StopExt};
327    ///
328    /// let a = StopSource::new();
329    /// let b = StopSource::new();
330    /// let c = StopSource::new();
331    ///
332    /// let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
333    ///
334    /// c.cancel();
335    /// assert!(combined.should_stop());
336    /// ```
337    #[inline]
338    fn or<S: Stop>(self, other: S) -> OrStop<Self, S> {
339        OrStop::new(self, other)
340    }
341
342    /// Convert this stop into a boxed trait object.
343    ///
344    /// This is useful for preventing monomorphization at API boundaries.
345    /// Instead of generating a new function for each `impl Stop` type,
346    /// you can erase the type to `BoxedStop` and have a single implementation.
347    ///
348    /// # Example
349    ///
350    /// ```rust
351    /// # #[cfg(feature = "alloc")]
352    /// # fn main() {
353    /// use almost_enough::{Stopper, BoxedStop, Stop, StopExt};
354    ///
355    /// // This function is monomorphized for each Stop type
356    /// fn process_generic(stop: impl Stop + 'static) {
357    ///     // Erase type at boundary
358    ///     process_concrete(stop.into_boxed());
359    /// }
360    ///
361    /// // This function has only one implementation
362    /// fn process_concrete(stop: BoxedStop) {
363    ///     while !stop.should_stop() {
364    ///         break;
365    ///     }
366    /// }
367    ///
368    /// let stop = Stopper::new();
369    /// process_generic(stop);
370    /// # }
371    /// # #[cfg(not(feature = "alloc"))]
372    /// # fn main() {}
373    /// ```
374    /// Convert this stop into a boxed trait object.
375    ///
376    /// **Prefer [`into_token()`](StopExt::into_token)** which returns a [`StopToken`]
377    /// that is `Clone` and supports indirection collapsing.
378    #[cfg(feature = "alloc")]
379    #[inline]
380    fn into_boxed(self) -> BoxedStop
381    where
382        Self: 'static,
383    {
384        BoxedStop::new(self)
385    }
386
387    /// Convert this stop into a shared, cloneable [`StopToken`].
388    ///
389    /// The result is `Clone` (via `Arc` increment) and can be sent across
390    /// threads. Use this when you need owned, cloneable type erasure.
391    ///
392    /// If `self` is already a `StopToken`, this is a no-op (no double-wrapping).
393    ///
394    /// # Example
395    ///
396    /// ```rust
397    /// # #[cfg(feature = "alloc")]
398    /// # fn main() {
399    /// use almost_enough::{Stopper, StopToken, Stop, StopExt};
400    ///
401    /// let stop = Stopper::new();
402    /// let dyn_stop: StopToken = stop.into_token();
403    /// let clone = dyn_stop.clone(); // cheap Arc increment
404    /// # }
405    /// # #[cfg(not(feature = "alloc"))]
406    /// # fn main() {}
407    /// ```
408    #[cfg(feature = "alloc")]
409    #[inline]
410    fn into_token(self) -> StopToken
411    where
412        Self: 'static,
413    {
414        StopToken::new(self)
415    }
416
417    /// Create a child stop that inherits cancellation from this stop.
418    ///
419    /// The returned [`ChildStopper`] will stop if:
420    /// - Its own `cancel()` is called
421    /// - This parent stop is cancelled
422    ///
423    /// Cancelling the child does NOT affect the parent.
424    ///
425    /// # Example
426    ///
427    /// ```rust
428    /// # #[cfg(feature = "alloc")]
429    /// # fn main() {
430    /// use almost_enough::{Stopper, Stop, StopExt};
431    ///
432    /// let parent = Stopper::new();
433    /// let child = parent.child();
434    ///
435    /// // Child cancellation is independent
436    /// child.cancel();
437    /// assert!(!parent.should_stop());
438    /// assert!(child.should_stop());
439    ///
440    /// // Parent cancellation propagates
441    /// let child2 = parent.child();
442    /// parent.cancel();
443    /// assert!(child2.should_stop());
444    /// # }
445    /// # #[cfg(not(feature = "alloc"))]
446    /// # fn main() {}
447    /// ```
448    #[cfg(feature = "alloc")]
449    #[inline]
450    fn child(&self) -> ChildStopper
451    where
452        Self: Clone + 'static,
453    {
454        ChildStopper::with_parent(self.clone())
455    }
456}
457
458// Blanket implementation for all Stop + Sized types
459impl<T: Stop + Sized> StopExt for T {}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn or_extension_works() {
467        let a = StopSource::new();
468        let b = StopSource::new();
469        let combined = a.as_ref().or(b.as_ref());
470
471        assert!(!combined.should_stop());
472
473        a.cancel();
474        assert!(combined.should_stop());
475    }
476
477    #[test]
478    fn or_chain_works() {
479        let a = StopSource::new();
480        let b = StopSource::new();
481        let c = StopSource::new();
482
483        let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
484
485        assert!(!combined.should_stop());
486
487        c.cancel();
488        assert!(combined.should_stop());
489    }
490
491    #[test]
492    fn or_with_unstoppable() {
493        let source = StopSource::new();
494        let combined = Unstoppable.or(source.as_ref());
495
496        assert!(!combined.should_stop());
497
498        source.cancel();
499        assert!(combined.should_stop());
500    }
501
502    #[test]
503    fn reexports_work() {
504        // Verify that re-exports from enough work
505        let _: StopReason = StopReason::Cancelled;
506        let _ = Unstoppable;
507        let source = StopSource::new();
508        let _ = source.as_ref();
509    }
510
511    #[cfg(feature = "alloc")]
512    #[test]
513    fn alloc_reexports_work() {
514        let stop = Stopper::new();
515        let _ = stop.clone();
516        let _ = BoxedStop::new(Unstoppable);
517    }
518
519    #[cfg(feature = "alloc")]
520    #[test]
521    fn into_boxed_works() {
522        let stop = Stopper::new();
523        let boxed: BoxedStop = stop.clone().into_boxed();
524
525        assert!(!boxed.should_stop());
526
527        stop.cancel();
528        assert!(boxed.should_stop());
529    }
530
531    #[cfg(feature = "alloc")]
532    #[test]
533    fn into_boxed_with_unstoppable() {
534        let boxed: BoxedStop = Unstoppable.into_boxed();
535        assert!(!boxed.should_stop());
536    }
537
538    #[cfg(feature = "alloc")]
539    #[test]
540    fn into_boxed_prevents_monomorphization() {
541        // This test verifies the pattern compiles correctly
542        fn outer(stop: impl Stop + 'static) {
543            inner(stop.into_boxed());
544        }
545
546        fn inner(stop: BoxedStop) {
547            let _ = stop.should_stop();
548        }
549
550        let stop = Stopper::new();
551        outer(stop);
552        outer(Unstoppable);
553    }
554
555    #[cfg(feature = "alloc")]
556    #[test]
557    fn child_extension_works() {
558        let parent = Stopper::new();
559        let child = parent.child();
560
561        assert!(!child.should_stop());
562
563        parent.cancel();
564        assert!(child.should_stop());
565    }
566
567    #[cfg(feature = "alloc")]
568    #[test]
569    fn child_independent_cancel() {
570        let parent = Stopper::new();
571        let child = parent.child();
572
573        child.cancel();
574
575        assert!(child.should_stop());
576        assert!(!parent.should_stop());
577    }
578
579    #[cfg(feature = "alloc")]
580    #[test]
581    fn child_chain() {
582        let grandparent = Stopper::new();
583        let parent = grandparent.child();
584        let child = parent.child();
585
586        grandparent.cancel();
587
588        assert!(parent.should_stop());
589        assert!(child.should_stop());
590    }
591}