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