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