almost-enough
Batteries-included ergonomic extensions for the enough cooperative cancellation crate.
While enough provides only the minimal Stop trait, this crate provides all concrete implementations, combinators, and helpers. It re-exports everything from enough for convenience.
Install
[]
= "0.4.5"
The default std feature pulls in everything (Arc-based stoppers, timeouts, guards). For no_std, disable default features and opt into alloc for the Arc-based types: almost-enough = { version = "0.4.5", default-features = false, features = ["alloc"] }. See Features.
Start Here: Stopper
If you're not sure which to use, reach for Stopper. It's the default: clones share one flag, and cancel() on any of them stops them all. The others, one line each:
ChildStopper, fromstop.child(), builds a tree: cancelling a parent stops its children, and cancelling a child leaves its parent and siblings running.OrStop, froma.or(b), stops when either one does, such as a cancel button or a shutdown signal.StopSourceis a flag on the stack or in astatic, with no allocation; it lends outStopRefs withas_ref()and works inno_std..with_timeout(Duration::from_secs(30)), fromTimeoutExt, adds a deadline to any stop.StopTokenis an owned stop to store or move into another thread; it clones with a reference-count bump.
The type table below lists the rest.
Quick Start
use ;
let stop = new;
let stop2 = stop.clone; // Clone to share the same flag
// Check it (any clone)
assert!;
// Cancel it (any clone)
stop.cancel;
assert!;
The two methods you need:
stop.cancel()— flip the flag.cancel(&self)is an inherent method onStopper(it takes&self, notself, so it never consumes the stopper), and it's idempotent. Construct withStopper::new().stop.should_stop() -> bool—trueonce cancelled. This comes from theStoptrait (almost_enough::Stop), whichStopperimplements directly, so theStoptrait must be in scope to call it.
Prefer the ? idiom in fallible code: stop.check()? returns Result<(), StopReason> — Ok(()) while running, Err(StopReason::Cancelled) once cancelled. check is also a Stop-trait method, and should_stop() is just self.check().is_err().
Cancel a Worker Thread
Stopper is Send + Sync (it's an Arc<AtomicBool> under the hood, and the Stop trait requires Send + Sync), so a clone can move into another thread and the original can cancel it from across the thread boundary:
use ;
use thread;
// `Stopper` is Send + Sync, so a clone is safe to move into a worker thread.
;
let stop = new;
let worker_stop = stop.clone; // clone shares the same cancellation flag
let handle = spawn;
// Cancel from the parent thread (or a signal handler, a timeout, etc.).
stop.cancel;
let done = handle.join.unwrap;
let _ = done;
Inside a fallible worker, use check()? instead of the while loop:
use ;
use thread;
let stop = new;
let worker_stop = stop.clone;
let handle = spawn;
stop.cancel;
// The worker returns Err(StopReason::Cancelled) (or Ok if it finished first).
let _ = handle.join.unwrap;
Accepting a stop in your own functions
Library code usually accepts an Option<&dyn Stop>; a caller passes Some(&stop), or None when nothing should stop the work. To store the stop or move it into another thread, accept a StopToken instead (see below):
use ;
let stop = new;
run.unwrap;
run.unwrap;
Check every 10 ms of work or so: often enough that cancelling feels immediate, rarely enough that the call through &dyn costs nothing measurable.
Type Overview
| Type | Feature | Use Case |
|---|---|---|
Unstoppable |
core | Zero-cost "never stop" |
StopSource / StopRef |
core | Stack-based, borrowed, zero-alloc |
FnStop |
core | Wrap any closure |
OrStop |
core | Combine multiple stops |
Stopper |
alloc | Default choice - Arc-based, clone to share |
SyncStopper |
alloc | Like Stopper, but every check is an Acquire load |
ChildStopper |
alloc | Hierarchical parent-child cancellation |
StopToken |
alloc | Type-erased dynamic dispatch - Arc-based, Clone |
BoxedStop |
alloc | Deprecated: use StopToken |
WithTimeout |
std | Add deadline to any Stop (reads the clock every check) |
DebouncedTimeout |
std | Like WithTimeout, reads the clock every N ≤ 64 checks, N timed from two checks |
Features
std(default) - Full functionality including timeoutsalloc- Arc-based types,into_token(),child(), guards- None - Core trait and stack-based types only (
no_stdcompatible)
Extension Traits
The StopExt trait adds combinator methods to any Stop:
use ;
let timeout = new;
let cancel = new;
// Combine: stop if either stops
let combined = timeout.as_ref.or;
assert!;
cancel.cancel;
assert!;
Hierarchical Cancellation
Create child stops that inherit cancellation from their parent:
use ;
let parent = new;
let child = parent.child;
// Child cancellation doesn't affect parent
child.cancel;
assert!;
// But parent cancellation propagates to children
let child2 = parent.child;
parent.cancel;
assert!;
Stop Guards (RAII)
Automatically cancel on scope exit unless explicitly disarmed:
use ;
Keeping a stop: StopToken
A &dyn Stop is borrowed for one call. To store a stop or move it into another
thread, take a StopToken: it's owned, and clones with a reference-count
bump. Stopper and SyncStopper convert to StopToken via Into without
allocating — the existing Arc is reused. A Stopper is then checked directly,
a SyncStopper through the vtable. into_token() converts any other
Stop + 'static:
use ;
use thread;
use Duration;
let stopper = new;
let worker = Worker ; // reuses the Stopper's Arc
let handle = worker.spawn;
stopper.cancel;
handle.join.unwrap;
// Any other stop, such as one with a deadline:
let worker = Worker ;
Optimizing Hot Loops with dyn Stop
Call live() once to skip overhead for no-op stops behind &dyn Stop:
use ;
// Unstoppable can never stop, so stop is None — zero overhead
assert!;
StopToken automatically optimizes away no-op stops — when
wrapping Unstoppable, check() short-circuits without any vtable dispatch:
use ;
hot_loop.unwrap; // zero overhead
hot_loop.unwrap; // one dispatch per check
See Also
enough- Minimal core trait (for library authors)enough-tokio- Tokio CancellationToken bridgeenough-ffi- FFI helpers for C#, Python, Node.js
License
MIT OR Apache-2.0