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}