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}