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