cancel_this/
lib.rs

1//! This crate provides a user-friendly way to implement cooperative
2//! cancellation in Rust based on a wide range of criteria, including
3//! *triggers*, *timers*, *OS signals* (Ctrl+C), *memory limit*, or the *Python
4//! interpreter linked using PyO3*. It also provides liveness monitoring
5//! of "cancellation aware" code.
6//!
7//! **Why not use `async` instead of cooperative cancellation?** In principle,
8//! `async` was designed to solve a different problem, and that's executing IO-bound
9//! tasks in a non-blocking fashion. It is not *really* designed for CPU-bound tasks.
10//! Consequently, using `async` adds a lot of unnecessary overhead to your project
11//! which `cancel_this` does not have (see also the *Performance* section in the project README).
12//!
13//! **Why not use [`stop-token`](https://crates.io/crates/stop-token),
14//! [`CancellationToken`](https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html)
15//! or other cooperative cancellation crates?** So far, all crates I have seen require you
16//! to pass the cancellation token around and generally do not make it easy to
17//! combine the effects of multiple tokens. In `cancel_this`, the goal was to
18//! make cancellation dead simple: You register however many cancellation triggers
19//! you want, each trigger is valid within a specific scope (and thread) and can be checked
20//! by a macro anywhere in your code.
21//!
22//! ### Current features
23//!
24//! - Scoped cancellation using thread-local "cancellation triggers".
25//! - Out-of-the-box support for triggers based on atomics and timers.
26//! - With feature `ctrlc` enabled, support for cancellation using `SIGINT` signals.
27//! - With feature `pyo3` enabled, support for cancellation using `Python::check_signals`.
28//! - With feature `memory` enabled, support for cancellation based on memory consumption returned by `memory-stats`.
29//! - With feature `liveness` enabled, you can register a per-thread handler invoked
30//!   once the thread becomes unresponsive (i.e., cancellation is not checked periodically
31//!   within the desired interval).
32//! - Practically no overhead in cancellable code when cancellation is not actively used.
33//! - Very small overhead for "atomic-based" cancellation triggers and PyO3 cancellation.
34//! - All triggers and guards generate [`log`](https://crates.io/crates/log) messages
35//!   (`trace` for normal operation, `warn` for issues where panic can be avoided).
36//!
37//!
38//! ### Simple example
39//!
40//! A simple counter that is eventually canceled by a one-second timeout:
41//!
42//! ```rust
43//! # use std::time::Duration;
44//! # use cancel_this::{Cancellable, is_cancelled};
45//! # let _ = env_logger::builder().is_test(true).try_init();
46//! fn cancellable_counter(count: usize) -> Cancellable<()> {
47//!     for _ in 0..count {
48//!         is_cancelled!()?;
49//!         std::thread::sleep(Duration::from_millis(10));
50//!     }
51//!     Ok(())
52//! }
53//!
54//! let result: Cancellable<()> = cancel_this::on_timeout(Duration::from_secs(1), || {
55//!     cancellable_counter(5)?;
56//!     cancellable_counter(10)?;
57//!     cancellable_counter(100)?;
58//!     Ok(())
59//! });
60//!
61//! assert!(result.is_err());
62//! ```
63//!
64//! ## Complex example
65//!
66//! This example uses most of the features, including error conversion, never-cancel blocks,
67//! and liveness monitoring.
68//!
69//! ```rust
70//! # use std::time::Duration;
71//! # use cancel_this::{Cancelled, is_cancelled, LivenessGuard};
72//! # let _ = env_logger::builder().is_test(true).try_init();
73//! enum ComputeError {
74//!     Zero,
75//!     Cancelled
76//! }
77//!
78//! impl From<Cancelled> for ComputeError {
79//!     fn from(value: Cancelled) -> Self {
80//!        ComputeError::Cancelled
81//!    }
82//! }
83//!
84//!
85//! fn compute(input: u32) -> Result<String, ComputeError> {
86//!     if input == 0 {
87//!         Err(ComputeError::Zero)
88//!     } else {
89//!         let mut total: u32 = 0;
90//!         for _ in 0..input {
91//!             total += input;
92//!             is_cancelled!()?;
93//!             std::thread::sleep(Duration::from_millis(10));
94//!         }
95//!         Ok(total.to_string())
96//!     }
97//! }
98//!
99//! let guard = LivenessGuard::new(Duration::from_secs(2), |is_alive| {
100//!     eprintln!("Thread has not responded in the last two seconds.");
101//! });
102//!
103//! let result: Result<String, ComputeError> = cancel_this::on_timeout(Duration::from_millis(200), || {
104//!     let r1 = cancel_this::on_sigint(|| {
105//!         // This operation can be canceled using Ctrl+C, but the timeout still applies.
106//!         compute(5)
107//!     })?;
108//!
109//!     assert_eq!(r1.as_str(), "25");
110//!     // This will be canceled. Instead of using `?`, we check
111//!     // that the operation actually got canceled.
112//!     let r2 = compute(20);
113//!     assert!(matches!(r2, Err(ComputeError::Cancelled)));
114//!     // Even though the execution is now canceled, we can still execute code in
115//!     // the "cancel-never" blocks.
116//!     let r3 = cancel_this::never(|| compute(10))?;
117//!     assert_eq!(r3.as_str(), "100");
118//!     compute(10) // This should get immediately canceled.
119//! });
120//!
121//! // The liveness monitoring is active while `guard` is in scope. Once `guard` is dropped here,
122//! // the liveness monitoring is turned off as well.
123//! ```
124//!
125//! ## Multi-threaded example
126//!
127//! Virtually all triggers and guards provided by `cancel_this` only apply to the current
128//! thread. However, since triggers can be safely shared across threads, it is possible to
129//! transfer them from one thread to another. Note that the transferred triggers also inherently
130//! update the liveness guard of the original thread.
131//!
132//! ```rust
133//! # use std::thread::JoinHandle;
134//! # use std::time::Duration;
135//! # use cancel_this::{is_cancelled, Cancellable, LivenessGuard};
136//! # let _ = env_logger::builder().is_test(true).try_init();
137//! let guard = LivenessGuard::new(Duration::from_millis(10), |is_alive| {
138//!     // In this test, the liveness guard should never trigger, even though the original
139//!     // thread goes to sleep for a long time, waiting to join with the spawned thread.
140//!     assert!(is_alive);
141//! });
142//!
143//! let result: Cancellable<u32> = cancel_this::on_timeout(Duration::from_millis(100), || {
144//!     let active = cancel_this::active_triggers();
145//!     let t1: JoinHandle<Cancellable<u32>> = std::thread::spawn(|| {
146//!         cancel_this::on_trigger(active, || {
147//!             let mut result = 0u32;
148//!             // This cycle is eventually going to get canceled
149//!             // by the timer which is "transferred" from the spawning thread.
150//!             for i in 0..50 {
151//!                 result += 1;
152//!                 is_cancelled!()?;
153//!                 std::thread::sleep(Duration::from_millis(5));
154//!             }
155//!             Ok(result)
156//!         })
157//!     });
158//!     // Put the spawning thread to sleep until `t1` finishes.
159//!     t1.join().unwrap()
160//! });
161//!
162//! assert!(result.is_err());
163//! ```
164//!
165//! Doing the same without transferring cancellation triggers will cause the spawning
166//! thread to be registered as unresponsive and the compute thread to never actually get
167//! canceled:
168//!
169//! ```rust
170//! # use std::thread::JoinHandle;
171//! # use std::time::Duration;
172//! # use cancel_this::{is_cancelled, Cancellable, LivenessGuard};
173//! # let _ = env_logger::builder().is_test(true).try_init();
174//! let guard = LivenessGuard::new(Duration::from_millis(10), |is_alive| {
175//!     // In this test, the liveness guard should never trigger, even though the original
176//!     // thread goes to sleep for a long time, waiting to join with the spawned thread.
177//!     assert!(!is_alive);
178//! });
179//!
180//! let result: Cancellable<u32> = cancel_this::on_timeout(Duration::from_millis(100), || {
181//!     let t1: JoinHandle<Cancellable<u32>> = std::thread::spawn(|| {
182//!         let mut result = 0u32;
183//!         // This cycle is never going to get canceled because we didn't transfer
184//!         // the timeout trigger from the original thread.
185//!         for i in 0..50 {
186//!             result += 1;
187//!             is_cancelled!()?;
188//!             std::thread::sleep(Duration::from_millis(5));
189//!         }
190//!         Ok(result)
191//!     });
192//!     // Put the spawning thread to sleep until `t1` finishes.
193//!     t1.join().unwrap()
194//! });
195//!
196//! assert!(result.is_ok());
197//! ```
198//!
199//! ## Cached triggers example
200//!
201//! If you need the absolute lowest overhead, you might want to sacrifice some of the
202//! ergonomics provided by `cancel_this`. To reduce overhead, you can create a local copy
203//! of the thread-local triggers the same way as in the multithreaded example and use it
204//! directly with `is_cancelled`. This significantly reduces the overhead of each
205//! cancellation check.
206//!
207//! ```rust
208//! # use std::time::Duration;
209//! # use cancel_this::{is_cancelled, Cancellable};
210//! # let _ = env_logger::builder().is_test(true).try_init();
211//! let result: Cancellable<u32> = cancel_this::on_timeout(Duration::from_millis(100), || {
212//!     let cache = cancel_this::active_triggers();
213//!     let mut result = 0u32;
214//!     while true {
215//!         // The overhead of this `is_cancelled` call is reduced because triggers
216//!         // are cached in a local variable. However, the cache is only valid within
217//!         // the scope where it was obtained.
218//!         is_cancelled!(cache)?;
219//!         result += 1;
220//!     }
221//!     Ok(result)
222//! });
223//! assert!(result.is_err());
224//! ```
225//!
226
227/// Cancellation error type.
228mod error;
229
230/// Various types of triggers, including corresponding `when_*` helper functions.
231mod triggers;
232
233/// Implements a "liveness guard", which monitors the frequency with which cancellations are
234/// checked, making sure the process
235#[cfg(feature = "liveness")]
236mod liveness;
237#[cfg(feature = "liveness")]
238pub use liveness::*;
239
240#[cfg(not(feature = "liveness"))]
241mod liveness {
242    #[derive(Clone, Default)]
243    pub(crate) struct LivenessInterceptor<R: crate::CancellationTrigger + Clone>(R);
244
245    impl<R: crate::CancellationTrigger + Clone> LivenessInterceptor<R> {
246        pub fn as_inner_mut(&mut self) -> &mut R {
247            &mut self.0
248        }
249
250        pub fn as_inner(&self) -> &R {
251            &self.0
252        }
253    }
254
255    impl LivenessInterceptor<crate::CancelChain> {
256        pub fn clone_and_flatten(&self) -> crate::triggers::DynamicCancellationTrigger {
257            // If liveness monitoring is off, we can just use normal flattening.
258            self.as_inner().clone_and_flatten()
259        }
260    }
261
262    impl<R: crate::CancellationTrigger + Clone> crate::CancellationTrigger for LivenessInterceptor<R> {
263        fn is_cancelled(&self) -> bool {
264            // If liveness monitoring is off, we do nothing.
265            self.0.is_cancelled()
266        }
267
268        fn type_name(&self) -> &'static str {
269            self.0.type_name()
270        }
271    }
272}
273
274pub use error::*;
275use liveness::LivenessInterceptor;
276use std::cell::RefCell;
277pub use triggers::*;
278
279/// The "default" [`Cancelled`] cause, reported when the trigger type is unknown.
280pub const UNKNOWN_CAUSE: &str = "UnknownCancellationTrigger";
281
282thread_local! {
283    /// The correct usage of this value lies in the fact that references to the `CancelChain`
284    /// will never leak out of this crate (we can hand out copies to the downstream users).
285    /// Within the crate, the triggers are either read when checking cancellation status or
286    /// written when entering/leaving the scope. However, these two actions are never performed
287    /// simultaneously.
288    static TRIGGER: RefCell<LivenessInterceptor<CancelChain>> = RefCell::new(LivenessInterceptor::default());
289}
290
291/// Call this macro every time your code wants to check for cancellation. It returns
292/// `Result<(), Cancelled>`, which can typically be propagated using the `?` operator.
293#[macro_export]
294macro_rules! is_cancelled {
295    () => {
296        $crate::check_local_cancellation()
297    };
298    ($handler:ident) => {
299        $crate::check_cancellation(&$handler)
300    };
301}
302
303/// Returns [`Cancelled`] if [`CancellationTrigger::is_cancelled`] of the given
304/// `trigger` is true. In typical situations, you don't use this method directly,
305/// but instead use the [`is_cancelled`] macro.
306pub fn check_cancellation<TCancel: CancellationTrigger>(
307    trigger: &TCancel,
308) -> Result<(), Cancelled> {
309    if trigger.is_cancelled() {
310        Err(Cancelled::new(trigger.type_name()))
311    } else {
312        Ok(())
313    }
314}
315
316/// Check if the current thread-local cancellation trigger is canceled. In typical situations,
317/// you don't use this method directly, but instead use the [`is_cancelled`] macro.
318///
319/// To avoid a repeated borrow of the thread-local value in performance-sensitive applications,
320/// you can use [`active_triggers`] to cache the value in a local variable.
321pub fn check_local_cancellation() -> Result<(), Cancelled> {
322    TRIGGER.with_borrow(check_cancellation)
323}
324
325/// Get a snapshot of the current thread-local cancellation trigger.
326///
327/// This value can be either used to initialize triggers in a new thread using [`on_trigger`],
328/// or used directly as an argument to the [`is_cancelled`] macro to speed up cancellation checks.
329pub fn active_triggers() -> DynamicCancellationTrigger {
330    TRIGGER.with_borrow(|trigger| trigger.clone_and_flatten())
331}
332
333/// Run the `action` in a context where a cancellation can be signaled using the given `trigger`.
334///
335/// Once the action is completed, the trigger is deregistered and does not apply
336/// to further code execution.
337pub fn on_trigger<TResult, TError, TCancel, TAction>(
338    trigger: TCancel,
339    action: TAction,
340) -> Result<TResult, TError>
341where
342    TCancel: CancellationTrigger + 'static,
343    TAction: FnOnce() -> Result<TResult, TError>,
344    TError: From<Cancelled>,
345{
346    TRIGGER.with_borrow_mut(|thread_trigger| thread_trigger.as_inner_mut().push(trigger));
347    let result = action();
348    TRIGGER.with_borrow_mut(|thread_trigger| thread_trigger.as_inner_mut().pop());
349    result
350}