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