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, but every check is an Acquire load |
76//! | [`ChildStopper`] | alloc | Hierarchical parent-child cancellation |
77//! | [`StopToken`] | alloc | **Type-erased dynamic dispatch** - Arc-based, `Clone` |
78//! | [`BoxedStop`] | alloc | Deprecated: use `StopToken` |
79//! | [`WithTimeout`] | std | Add deadline to any `Stop` (reads the clock every check) |
80//! | [`DebouncedTimeout`] | std | Like `WithTimeout`, reads the clock every N ≤ 64 checks, N timed from two checks |
81//!
82//! ## StopExt Extension Trait
83//!
84//! The [`StopExt`] trait adds combinator methods to any [`Stop`] implementation:
85//!
86//! ```rust
87//! use almost_enough::{StopSource, Stop, StopExt};
88//!
89//! let timeout = StopSource::new();
90//! let cancel = StopSource::new();
91//!
92//! // Combine: stop if either stops
93//! let combined = timeout.as_ref().or(cancel.as_ref());
94//! assert!(!combined.should_stop());
95//!
96//! cancel.cancel();
97//! assert!(combined.should_stop());
98//! ```
99//!
100//! ## Type Erasure with `StopToken`
101//!
102//! [`StopToken`] wraps `Arc<dyn Stop>` — it's `Clone` (cheap Arc increment),
103//! type-erased, and can be sent across threads. [`Stopper`] and
104//! [`SyncStopper`] convert to `StopToken` via `From`/`Into` without
105//! allocating (the existing Arc is reused). A `Stopper` is then checked as a
106//! direct atomic load, a `SyncStopper` through the vtable.
107//!
108//! Use [`CloneStop`] (a trait alias for `Stop + Clone + 'static`) to accept
109//! any clonable stop, then erase with `into_token()` at the boundary:
110//!
111//! ```rust
112//! # #[cfg(feature = "alloc")]
113//! # fn main() {
114//! use almost_enough::{CloneStop, StopToken, Stopper, Stop, StopExt};
115//!
116//! fn outer(stop: impl CloneStop) {
117//!     // Erase the concrete type — StopToken is Clone
118//!     let stop: StopToken = stop.into_token();
119//!     inner(&stop);
120//! }
121//!
122//! fn inner(stop: &StopToken) {
123//!     let stop2 = stop.clone(); // cheap Arc increment
124//!     // Only one version of this function exists
125//!     while !stop.should_stop() {
126//!         break;
127//!     }
128//! }
129//!
130//! let stop = Stopper::new();
131//! outer(stop);
132//! # }
133//! # #[cfg(not(feature = "alloc"))]
134//! # fn main() {}
135//! ```
136//!
137//! ## Hierarchical Cancellation with `.child()`
138//!
139//! Create child stops that inherit cancellation from their parent:
140//!
141//! ```rust
142//! # #[cfg(feature = "alloc")]
143//! # fn main() {
144//! use almost_enough::{Stopper, Stop, StopExt};
145//!
146//! let parent = Stopper::new();
147//! let child = parent.child();
148//!
149//! // Child cancellation doesn't affect parent
150//! child.cancel();
151//! assert!(!parent.should_stop());
152//!
153//! // But parent cancellation propagates to children
154//! let child2 = parent.child();
155//! parent.cancel();
156//! assert!(child2.should_stop());
157//! # }
158//! # #[cfg(not(feature = "alloc"))]
159//! # fn main() {}
160//! ```
161//!
162//! ## Stop Guards (RAII Cancellation)
163//!
164//! Automatically stop on scope exit unless explicitly disarmed:
165//!
166//! ```rust
167//! # #[cfg(feature = "alloc")]
168//! # fn main() {
169//! use almost_enough::{Stopper, StopDropRoll};
170//!
171//! fn do_work(source: &Stopper) -> Result<(), &'static str> {
172//!     let guard = source.stop_on_drop();
173//!
174//!     // If we return early or panic, source is stopped
175//!     risky_operation()?;
176//!
177//!     // Success! Don't stop.
178//!     guard.disarm();
179//!     Ok(())
180//! }
181//!
182//! fn risky_operation() -> Result<(), &'static str> {
183//!     Ok(())
184//! }
185//!
186//! let source = Stopper::new();
187//! do_work(&source).unwrap();
188//! # }
189//! # #[cfg(not(feature = "alloc"))]
190//! # fn main() {}
191//! ```
192//!
193//! ## Feature Flags
194//!
195//! - **`std`** (default) - Full functionality including timeouts
196//! - **`alloc`** - Arc-based types, `into_token()`, `child()`, `StopDropRoll`
197//! - **None** - Core trait and stack-based types only
198
199#![cfg_attr(not(feature = "std"), no_std)]
200#![forbid(unsafe_code)]
201#![warn(missing_docs)]
202#![warn(clippy::all)]
203
204#[cfg(feature = "alloc")]
205extern crate alloc;
206
207// Re-export everything from enough
208#[allow(deprecated)]
209pub use enough::{Never, Stop, StopReason, Unstoppable};
210
211/// Trait alias for stop tokens that can be cloned and sent across threads.
212///
213/// This is `Stop + Clone + 'static` — the minimum needed to clone a stop
214/// token and send it to other threads. Since `Stop` already requires
215/// `Send + Sync`, `CloneStop` types are fully thread-safe.
216///
217/// Use `impl CloneStop` in public API signatures when you need to clone
218/// the stop token, then erase with [`StopToken`] internally:
219///
220/// ```rust
221/// # #[cfg(feature = "alloc")]
222/// # fn main() {
223/// use almost_enough::{CloneStop, StopToken, Stop};
224///
225/// pub fn parallel_work(stop: impl CloneStop) {
226///     let stop = StopToken::new(stop);
227///     let s2 = stop.clone(); // Arc increment
228/// }
229/// # }
230/// # #[cfg(not(feature = "alloc"))]
231/// # fn main() {}
232/// ```
233pub trait CloneStop: Stop + Clone + 'static {}
234
235/// Blanket implementation: any `Stop + Clone + 'static` is `CloneStop`.
236impl<T: Stop + Clone + 'static> CloneStop for T {}
237
238// Core modules (no_std, no alloc)
239mod func;
240mod or;
241mod source;
242
243pub use func::FnStop;
244pub use or::OrStop;
245pub use source::{StopRef, StopSource};
246
247// Alloc-dependent modules
248#[cfg(feature = "alloc")]
249mod boxed;
250#[cfg(feature = "alloc")]
251mod stopper;
252#[cfg(feature = "alloc")]
253mod sync_stopper;
254#[cfg(feature = "alloc")]
255mod tree;
256
257#[cfg(feature = "alloc")]
258#[allow(deprecated)]
259pub use boxed::BoxedStop;
260#[cfg(feature = "alloc")]
261mod stop_token;
262#[cfg(feature = "alloc")]
263pub use stop_token::StopToken;
264#[cfg(feature = "alloc")]
265pub use stopper::Stopper;
266#[cfg(feature = "alloc")]
267pub use sync_stopper::SyncStopper;
268#[cfg(feature = "alloc")]
269pub use tree::ChildStopper;
270
271// Std-dependent modules
272#[cfg(feature = "std")]
273pub mod time;
274#[cfg(feature = "std")]
275pub use time::{DebouncedTimeout, DebouncedTimeoutExt, TimeoutExt, WithTimeout};
276
277// Poll-latency instrumentation (opt-in: ~10-15ms compile cost, needs Instant)
278#[cfg(feature = "poll-meter")]
279mod meter;
280#[cfg(feature = "poll-meter")]
281pub use meter::{
282    DEFAULT_FAST_GAP, DEFAULT_FLOOD_CALLS, DEFAULT_SLOW_GAP, HISTOGRAM_BUCKETS, PollMeter,
283    PollProblem, PollReport, SiteReport,
284};
285
286// Cancel guard module
287#[cfg(feature = "alloc")]
288mod guard;
289#[cfg(feature = "alloc")]
290pub use guard::{CancelGuard, Cancellable, StopDropRoll};
291
292/// Extension trait providing ergonomic combinators for [`Stop`] implementations.
293///
294/// This trait is automatically implemented for all `Stop + Sized` types.
295///
296/// # Example
297///
298/// ```rust
299/// use almost_enough::{StopSource, Stop, StopExt};
300///
301/// let source_a = StopSource::new();
302/// let source_b = StopSource::new();
303///
304/// // Combine with .or()
305/// let combined = source_a.as_ref().or(source_b.as_ref());
306///
307/// assert!(!combined.should_stop());
308///
309/// source_b.cancel();
310/// assert!(combined.should_stop());
311/// ```
312pub trait StopExt: Stop + Sized {
313    /// Combine this stop with another, stopping if either stops.
314    ///
315    /// This is equivalent to `OrStop::new(self, other)` but with a more
316    /// ergonomic method syntax that allows chaining.
317    ///
318    /// # Example
319    ///
320    /// ```rust
321    /// use almost_enough::{StopSource, Stop, StopExt};
322    ///
323    /// let timeout = StopSource::new();
324    /// let cancel = StopSource::new();
325    ///
326    /// let combined = timeout.as_ref().or(cancel.as_ref());
327    /// assert!(!combined.should_stop());
328    ///
329    /// cancel.cancel();
330    /// assert!(combined.should_stop());
331    /// ```
332    ///
333    /// # Chaining
334    ///
335    /// Multiple sources can be chained:
336    ///
337    /// ```rust
338    /// use almost_enough::{StopSource, Stop, StopExt};
339    ///
340    /// let a = StopSource::new();
341    /// let b = StopSource::new();
342    /// let c = StopSource::new();
343    ///
344    /// let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
345    ///
346    /// c.cancel();
347    /// assert!(combined.should_stop());
348    /// ```
349    #[inline]
350    fn or<S: Stop>(self, other: S) -> OrStop<Self, S> {
351        OrStop::new(self, other)
352    }
353
354    /// Convert this stop into a [`BoxedStop`]. Deprecated: use
355    /// [`into_token()`](StopExt::into_token), whose [`StopToken`] checks the
356    /// same way and is `Clone`.
357    #[cfg(feature = "alloc")]
358    #[deprecated(
359        since = "0.4.5",
360        note = "use `into_token()`, whose `StopToken` checks the same way and is `Clone`"
361    )]
362    #[allow(deprecated)]
363    #[inline]
364    fn into_boxed(self) -> BoxedStop
365    where
366        Self: 'static,
367    {
368        BoxedStop::new(self)
369    }
370
371    /// Convert this stop into a shared, cloneable [`StopToken`].
372    ///
373    /// The result is `Clone` (via `Arc` increment) and can be sent across
374    /// threads. Use this when you need owned, cloneable type erasure.
375    ///
376    /// If `self` is already a `StopToken`, this is a no-op (no double-wrapping).
377    ///
378    /// # Example
379    ///
380    /// ```rust
381    /// # #[cfg(feature = "alloc")]
382    /// # fn main() {
383    /// use almost_enough::{Stopper, StopToken, Stop, StopExt};
384    ///
385    /// let stop = Stopper::new();
386    /// let dyn_stop: StopToken = stop.into_token();
387    /// let clone = dyn_stop.clone(); // cheap Arc increment
388    /// # }
389    /// # #[cfg(not(feature = "alloc"))]
390    /// # fn main() {}
391    /// ```
392    #[cfg(feature = "alloc")]
393    #[inline]
394    fn into_token(self) -> StopToken
395    where
396        Self: 'static,
397    {
398        StopToken::new(self)
399    }
400
401    /// Create a child stop that inherits cancellation from this stop.
402    ///
403    /// The returned [`ChildStopper`] will stop if:
404    /// - Its own `cancel()` is called
405    /// - This parent stop is cancelled
406    ///
407    /// Cancelling the child does NOT affect the parent.
408    ///
409    /// # Example
410    ///
411    /// ```rust
412    /// # #[cfg(feature = "alloc")]
413    /// # fn main() {
414    /// use almost_enough::{Stopper, Stop, StopExt};
415    ///
416    /// let parent = Stopper::new();
417    /// let child = parent.child();
418    ///
419    /// // Child cancellation is independent
420    /// child.cancel();
421    /// assert!(!parent.should_stop());
422    /// assert!(child.should_stop());
423    ///
424    /// // Parent cancellation propagates
425    /// let child2 = parent.child();
426    /// parent.cancel();
427    /// assert!(child2.should_stop());
428    /// # }
429    /// # #[cfg(not(feature = "alloc"))]
430    /// # fn main() {}
431    /// ```
432    #[cfg(feature = "alloc")]
433    #[inline]
434    fn child(&self) -> ChildStopper
435    where
436        Self: Clone + 'static,
437    {
438        ChildStopper::with_parent(self.clone())
439    }
440
441    /// Wrap this stop in a [`PollMeter`] that records time between polls.
442    ///
443    /// For tests and perf investigations — see the `meter` module docs.
444    ///
445    /// # Example
446    ///
447    /// ```rust
448    /// # #[cfg(feature = "poll-meter")]
449    /// # fn main() {
450    /// use almost_enough::{Stopper, Stop, StopExt};
451    ///
452    /// let meter = Stopper::new().metered();
453    /// meter.check().unwrap();
454    /// assert_eq!(meter.report().calls, 1);
455    /// # }
456    /// # #[cfg(not(feature = "poll-meter"))]
457    /// # fn main() {}
458    /// ```
459    #[cfg(feature = "poll-meter")]
460    #[inline]
461    fn metered(self) -> PollMeter<Self> {
462        PollMeter::new(self)
463    }
464}
465
466// Blanket implementation for all Stop + Sized types
467impl<T: Stop + Sized> StopExt for T {}
468
469// Compile the READMEs' examples as doctests.
470#[cfg(all(doctest, feature = "std"))]
471#[doc = include_str!("../../../README.md")]
472struct RootReadme;
473#[cfg(all(doctest, feature = "std"))]
474#[doc = include_str!("../README.md")]
475struct CrateReadme;
476// enough's README shows almost-enough's Stopper, so it compiles here.
477#[cfg(all(doctest, feature = "std"))]
478#[doc = include_str!("../../enough/README.md")]
479struct EnoughReadme;
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    #[test]
486    fn or_extension_works() {
487        let a = StopSource::new();
488        let b = StopSource::new();
489        let combined = a.as_ref().or(b.as_ref());
490
491        assert!(!combined.should_stop());
492
493        a.cancel();
494        assert!(combined.should_stop());
495    }
496
497    #[test]
498    fn or_chain_works() {
499        let a = StopSource::new();
500        let b = StopSource::new();
501        let c = StopSource::new();
502
503        let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
504
505        assert!(!combined.should_stop());
506
507        c.cancel();
508        assert!(combined.should_stop());
509    }
510
511    #[test]
512    fn or_with_unstoppable() {
513        let source = StopSource::new();
514        let combined = Unstoppable.or(source.as_ref());
515
516        assert!(!combined.should_stop());
517
518        source.cancel();
519        assert!(combined.should_stop());
520    }
521
522    #[test]
523    fn reexports_work() {
524        // Verify that re-exports from enough work
525        let _: StopReason = StopReason::Cancelled;
526        let _ = Unstoppable;
527        let source = StopSource::new();
528        let _ = source.as_ref();
529    }
530
531    #[cfg(feature = "alloc")]
532    #[test]
533    #[allow(deprecated)] // Exercises the deprecated BoxedStop.
534    fn alloc_reexports_work() {
535        let stop = Stopper::new();
536        let _ = stop.clone();
537        let _ = BoxedStop::new(Unstoppable);
538    }
539
540    #[cfg(feature = "alloc")]
541    #[test]
542    #[allow(deprecated)] // Exercises the deprecated BoxedStop.
543    fn into_boxed_works() {
544        let stop = Stopper::new();
545        let boxed: BoxedStop = stop.clone().into_boxed();
546
547        assert!(!boxed.should_stop());
548
549        stop.cancel();
550        assert!(boxed.should_stop());
551    }
552
553    #[cfg(feature = "alloc")]
554    #[test]
555    #[allow(deprecated)] // Exercises the deprecated BoxedStop.
556    fn into_boxed_with_unstoppable() {
557        let boxed: BoxedStop = Unstoppable.into_boxed();
558        assert!(!boxed.should_stop());
559    }
560
561    #[cfg(feature = "alloc")]
562    #[test]
563    #[allow(deprecated)] // Exercises the deprecated BoxedStop.
564    fn into_boxed_prevents_monomorphization() {
565        // This test verifies the pattern compiles correctly
566        fn outer(stop: impl Stop + 'static) {
567            inner(stop.into_boxed());
568        }
569
570        fn inner(stop: BoxedStop) {
571            let _ = stop.should_stop();
572        }
573
574        let stop = Stopper::new();
575        outer(stop);
576        outer(Unstoppable);
577    }
578
579    #[cfg(feature = "alloc")]
580    #[test]
581    fn child_extension_works() {
582        let parent = Stopper::new();
583        let child = parent.child();
584
585        assert!(!child.should_stop());
586
587        parent.cancel();
588        assert!(child.should_stop());
589    }
590
591    #[cfg(feature = "alloc")]
592    #[test]
593    fn child_independent_cancel() {
594        let parent = Stopper::new();
595        let child = parent.child();
596
597        child.cancel();
598
599        assert!(child.should_stop());
600        assert!(!parent.should_stop());
601    }
602
603    #[cfg(feature = "alloc")]
604    #[test]
605    fn child_chain() {
606        let grandparent = Stopper::new();
607        let parent = grandparent.child();
608        let child = parent.child();
609
610        grandparent.cancel();
611
612        assert!(parent.should_stop());
613        assert!(child.should_stop());
614    }
615}