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//! | [`BoxedStop`] | alloc | Type-erased dynamic dispatch |
78//! | [`WithTimeout`] | std | Add deadline to any `Stop` |
79//!
80//! ## StopExt Extension Trait
81//!
82//! The [`StopExt`] trait adds combinator methods to any [`Stop`] implementation:
83//!
84//! ```rust
85//! use almost_enough::{StopSource, Stop, StopExt};
86//!
87//! let timeout = StopSource::new();
88//! let cancel = StopSource::new();
89//!
90//! // Combine: stop if either stops
91//! let combined = timeout.as_ref().or(cancel.as_ref());
92//! assert!(!combined.should_stop());
93//!
94//! cancel.cancel();
95//! assert!(combined.should_stop());
96//! ```
97//!
98//! ## Type Erasure with `into_boxed()`
99//!
100//! Prevent monomorphization explosion at API boundaries:
101//!
102//! ```rust
103//! # #[cfg(feature = "alloc")]
104//! # fn main() {
105//! use almost_enough::{Stopper, BoxedStop, Stop, StopExt};
106//!
107//! fn outer(stop: impl Stop + 'static) {
108//!     // Erase the concrete type to avoid monomorphizing inner()
109//!     inner(stop.into_boxed());
110//! }
111//!
112//! fn inner(stop: BoxedStop) {
113//!     // Only one version of this function exists
114//!     while !stop.should_stop() {
115//!         break;
116//!     }
117//! }
118//!
119//! let stop = Stopper::new();
120//! outer(stop);
121//! # }
122//! # #[cfg(not(feature = "alloc"))]
123//! # fn main() {}
124//! ```
125//!
126//! ## Hierarchical Cancellation with `.child()`
127//!
128//! Create child stops that inherit cancellation from their parent:
129//!
130//! ```rust
131//! # #[cfg(feature = "alloc")]
132//! # fn main() {
133//! use almost_enough::{Stopper, Stop, StopExt};
134//!
135//! let parent = Stopper::new();
136//! let child = parent.child();
137//!
138//! // Child cancellation doesn't affect parent
139//! child.cancel();
140//! assert!(!parent.should_stop());
141//!
142//! // But parent cancellation propagates to children
143//! let child2 = parent.child();
144//! parent.cancel();
145//! assert!(child2.should_stop());
146//! # }
147//! # #[cfg(not(feature = "alloc"))]
148//! # fn main() {}
149//! ```
150//!
151//! ## Stop Guards (RAII Cancellation)
152//!
153//! Automatically stop on scope exit unless explicitly disarmed:
154//!
155//! ```rust
156//! # #[cfg(feature = "alloc")]
157//! # fn main() {
158//! use almost_enough::{Stopper, StopDropRoll};
159//!
160//! fn do_work(source: &Stopper) -> Result<(), &'static str> {
161//!     let guard = source.stop_on_drop();
162//!
163//!     // If we return early or panic, source is stopped
164//!     risky_operation()?;
165//!
166//!     // Success! Don't stop.
167//!     guard.disarm();
168//!     Ok(())
169//! }
170//!
171//! fn risky_operation() -> Result<(), &'static str> {
172//!     Ok(())
173//! }
174//!
175//! let source = Stopper::new();
176//! do_work(&source).unwrap();
177//! # }
178//! # #[cfg(not(feature = "alloc"))]
179//! # fn main() {}
180//! ```
181//!
182//! ## Feature Flags
183//!
184//! - **`std`** (default) - Full functionality including timeouts
185//! - **`alloc`** - Arc-based types, `into_boxed()`, `child()`, `StopDropRoll`
186//! - **None** - Core trait and stack-based types only
187
188#![cfg_attr(not(feature = "std"), no_std)]
189#![warn(missing_docs)]
190#![warn(clippy::all)]
191
192#[cfg(feature = "alloc")]
193extern crate alloc;
194
195// Re-export everything from enough
196#[allow(deprecated)]
197pub use enough::{Never, Stop, StopReason, Unstoppable};
198
199// Core modules (no_std, no alloc)
200mod func;
201mod or;
202mod source;
203
204pub use func::FnStop;
205pub use or::OrStop;
206pub use source::{StopRef, StopSource};
207
208// Alloc-dependent modules
209#[cfg(feature = "alloc")]
210mod boxed;
211#[cfg(feature = "alloc")]
212mod stopper;
213#[cfg(feature = "alloc")]
214mod sync_stopper;
215#[cfg(feature = "alloc")]
216mod tree;
217
218#[cfg(feature = "alloc")]
219pub use boxed::BoxedStop;
220#[cfg(feature = "alloc")]
221pub use stopper::Stopper;
222#[cfg(feature = "alloc")]
223pub use sync_stopper::SyncStopper;
224#[cfg(feature = "alloc")]
225pub use tree::ChildStopper;
226
227// Std-dependent modules
228#[cfg(feature = "std")]
229pub mod time;
230#[cfg(feature = "std")]
231pub use time::{TimeoutExt, WithTimeout};
232
233// Cancel guard module
234#[cfg(feature = "alloc")]
235mod guard;
236#[cfg(feature = "alloc")]
237pub use guard::{CancelGuard, Cancellable, StopDropRoll};
238
239/// Extension trait providing ergonomic combinators for [`Stop`] implementations.
240///
241/// This trait is automatically implemented for all `Stop + Sized` types.
242///
243/// # Example
244///
245/// ```rust
246/// use almost_enough::{StopSource, Stop, StopExt};
247///
248/// let source_a = StopSource::new();
249/// let source_b = StopSource::new();
250///
251/// // Combine with .or()
252/// let combined = source_a.as_ref().or(source_b.as_ref());
253///
254/// assert!(!combined.should_stop());
255///
256/// source_b.cancel();
257/// assert!(combined.should_stop());
258/// ```
259pub trait StopExt: Stop + Sized {
260    /// Combine this stop with another, stopping if either stops.
261    ///
262    /// This is equivalent to `OrStop::new(self, other)` but with a more
263    /// ergonomic method syntax that allows chaining.
264    ///
265    /// # Example
266    ///
267    /// ```rust
268    /// use almost_enough::{StopSource, Stop, StopExt};
269    ///
270    /// let timeout = StopSource::new();
271    /// let cancel = StopSource::new();
272    ///
273    /// let combined = timeout.as_ref().or(cancel.as_ref());
274    /// assert!(!combined.should_stop());
275    ///
276    /// cancel.cancel();
277    /// assert!(combined.should_stop());
278    /// ```
279    ///
280    /// # Chaining
281    ///
282    /// Multiple sources can be chained:
283    ///
284    /// ```rust
285    /// use almost_enough::{StopSource, Stop, StopExt};
286    ///
287    /// let a = StopSource::new();
288    /// let b = StopSource::new();
289    /// let c = StopSource::new();
290    ///
291    /// let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
292    ///
293    /// c.cancel();
294    /// assert!(combined.should_stop());
295    /// ```
296    #[inline]
297    fn or<S: Stop>(self, other: S) -> OrStop<Self, S> {
298        OrStop::new(self, other)
299    }
300
301    /// Convert this stop into a boxed trait object.
302    ///
303    /// This is useful for preventing monomorphization at API boundaries.
304    /// Instead of generating a new function for each `impl Stop` type,
305    /// you can erase the type to `BoxedStop` and have a single implementation.
306    ///
307    /// # Example
308    ///
309    /// ```rust
310    /// # #[cfg(feature = "alloc")]
311    /// # fn main() {
312    /// use almost_enough::{Stopper, BoxedStop, Stop, StopExt};
313    ///
314    /// // This function is monomorphized for each Stop type
315    /// fn process_generic(stop: impl Stop + 'static) {
316    ///     // Erase type at boundary
317    ///     process_concrete(stop.into_boxed());
318    /// }
319    ///
320    /// // This function has only one implementation
321    /// fn process_concrete(stop: BoxedStop) {
322    ///     while !stop.should_stop() {
323    ///         break;
324    ///     }
325    /// }
326    ///
327    /// let stop = Stopper::new();
328    /// process_generic(stop);
329    /// # }
330    /// # #[cfg(not(feature = "alloc"))]
331    /// # fn main() {}
332    /// ```
333    #[cfg(feature = "alloc")]
334    #[inline]
335    fn into_boxed(self) -> BoxedStop
336    where
337        Self: 'static,
338    {
339        BoxedStop::new(self)
340    }
341
342    /// Create a child stop that inherits cancellation from this stop.
343    ///
344    /// The returned [`ChildStopper`] will stop if:
345    /// - Its own `cancel()` is called
346    /// - This parent stop is cancelled
347    ///
348    /// Cancelling the child does NOT affect the parent.
349    ///
350    /// # Example
351    ///
352    /// ```rust
353    /// # #[cfg(feature = "alloc")]
354    /// # fn main() {
355    /// use almost_enough::{Stopper, Stop, StopExt};
356    ///
357    /// let parent = Stopper::new();
358    /// let child = parent.child();
359    ///
360    /// // Child cancellation is independent
361    /// child.cancel();
362    /// assert!(!parent.should_stop());
363    /// assert!(child.should_stop());
364    ///
365    /// // Parent cancellation propagates
366    /// let child2 = parent.child();
367    /// parent.cancel();
368    /// assert!(child2.should_stop());
369    /// # }
370    /// # #[cfg(not(feature = "alloc"))]
371    /// # fn main() {}
372    /// ```
373    #[cfg(feature = "alloc")]
374    #[inline]
375    fn child(&self) -> ChildStopper
376    where
377        Self: Clone + 'static,
378    {
379        ChildStopper::with_parent(self.clone())
380    }
381}
382
383// Blanket implementation for all Stop + Sized types
384impl<T: Stop + Sized> StopExt for T {}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn or_extension_works() {
392        let a = StopSource::new();
393        let b = StopSource::new();
394        let combined = a.as_ref().or(b.as_ref());
395
396        assert!(!combined.should_stop());
397
398        a.cancel();
399        assert!(combined.should_stop());
400    }
401
402    #[test]
403    fn or_chain_works() {
404        let a = StopSource::new();
405        let b = StopSource::new();
406        let c = StopSource::new();
407
408        let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
409
410        assert!(!combined.should_stop());
411
412        c.cancel();
413        assert!(combined.should_stop());
414    }
415
416    #[test]
417    fn or_with_unstoppable() {
418        let source = StopSource::new();
419        let combined = Unstoppable.or(source.as_ref());
420
421        assert!(!combined.should_stop());
422
423        source.cancel();
424        assert!(combined.should_stop());
425    }
426
427    #[test]
428    fn reexports_work() {
429        // Verify that re-exports from enough work
430        let _: StopReason = StopReason::Cancelled;
431        let _ = Unstoppable;
432        let source = StopSource::new();
433        let _ = source.as_ref();
434    }
435
436    #[cfg(feature = "alloc")]
437    #[test]
438    fn alloc_reexports_work() {
439        let stop = Stopper::new();
440        let _ = stop.clone();
441        let _ = BoxedStop::new(Unstoppable);
442    }
443
444    #[cfg(feature = "alloc")]
445    #[test]
446    fn into_boxed_works() {
447        let stop = Stopper::new();
448        let boxed: BoxedStop = stop.clone().into_boxed();
449
450        assert!(!boxed.should_stop());
451
452        stop.cancel();
453        assert!(boxed.should_stop());
454    }
455
456    #[cfg(feature = "alloc")]
457    #[test]
458    fn into_boxed_with_unstoppable() {
459        let boxed: BoxedStop = Unstoppable.into_boxed();
460        assert!(!boxed.should_stop());
461    }
462
463    #[cfg(feature = "alloc")]
464    #[test]
465    fn into_boxed_prevents_monomorphization() {
466        // This test verifies the pattern compiles correctly
467        fn outer(stop: impl Stop + 'static) {
468            inner(stop.into_boxed());
469        }
470
471        fn inner(stop: BoxedStop) {
472            let _ = stop.should_stop();
473        }
474
475        let stop = Stopper::new();
476        outer(stop);
477        outer(Unstoppable);
478    }
479
480    #[cfg(feature = "alloc")]
481    #[test]
482    fn child_extension_works() {
483        let parent = Stopper::new();
484        let child = parent.child();
485
486        assert!(!child.should_stop());
487
488        parent.cancel();
489        assert!(child.should_stop());
490    }
491
492    #[cfg(feature = "alloc")]
493    #[test]
494    fn child_independent_cancel() {
495        let parent = Stopper::new();
496        let child = parent.child();
497
498        child.cancel();
499
500        assert!(child.should_stop());
501        assert!(!parent.should_stop());
502    }
503
504    #[cfg(feature = "alloc")]
505    #[test]
506    fn child_chain() {
507        let grandparent = Stopper::new();
508        let parent = grandparent.child();
509        let child = parent.child();
510
511        grandparent.cancel();
512
513        assert!(parent.should_stop());
514        assert!(child.should_stop());
515    }
516}