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