Expand description
This crate provides a user-friendly way to implement cooperative cancellation in Rust based on a wide range of criteria, including triggers, timers, OS signals (Ctrl+C), memory limit, or the Python interpreter linked using PyO3. It also provides liveness monitoring of “cancellation aware” code.
Why not use async instead of cooperative cancellation? In principle,
async was designed to solve a different problem, and that’s executing IO-bound
tasks in a non-blocking fashion. It is not really designed for CPU-bound tasks.
Consequently, using async adds a lot of unnecessary overhead to your project
which cancel_this does not have (see also the Performance section in the project README).
Why not use stop-token,
CancellationToken
or other cooperative cancellation crates? So far, all crates I have seen require you
to pass the cancellation token around and generally do not make it easy to
combine the effects of multiple tokens. In cancel_this, the goal was to
make cancellation dead simple: You register however many cancellation triggers
you want, each trigger is valid within a specific scope (and thread) and can be checked
by a macro anywhere in your code.
§Current features
- Scoped cancellation using thread-local “cancellation triggers”.
- Out-of-the-box support for triggers based on atomics and timers.
- With feature
ctrlcenabled, support for cancellation usingSIGINTsignals. - With feature
pyo3enabled, support for cancellation usingPython::check_signals. - With feature
memoryenabled, support for cancellation based on memory consumption returned bymemory-stats. - With feature
livenessenabled, you can register a per-thread handler invoked once the thread becomes unresponsive (i.e., cancellation is not checked periodically within the desired interval). - Practically no overhead in cancellable code when cancellation is not actively used.
- Very small overhead for “atomic-based” cancellation triggers and PyO3 cancellation.
- All triggers and guards generate
logmessages (tracefor normal operation,warnfor issues where panic can be avoided).
§Simple example
A simple counter that is eventually canceled by a one-second timeout:
fn cancellable_counter(count: usize) -> Cancellable<()> {
for _ in 0..count {
is_cancelled!()?;
std::thread::sleep(Duration::from_millis(10));
}
Ok(())
}
let result: Cancellable<()> = cancel_this::on_timeout(Duration::from_secs(1), || {
cancellable_counter(5)?;
cancellable_counter(10)?;
cancellable_counter(100)?;
Ok(())
});
assert!(result.is_err());§Complex example
This example uses most of the features, including error conversion, never-cancel blocks, and liveness monitoring.
enum ComputeError {
Zero,
Cancelled
}
impl From<Cancelled> for ComputeError {
fn from(value: Cancelled) -> Self {
ComputeError::Cancelled
}
}
fn compute(input: u32) -> Result<String, ComputeError> {
if input == 0 {
Err(ComputeError::Zero)
} else {
let mut total: u32 = 0;
for _ in 0..input {
total += input;
is_cancelled!()?;
std::thread::sleep(Duration::from_millis(10));
}
Ok(total.to_string())
}
}
let guard = LivenessGuard::new(Duration::from_secs(2), |is_alive| {
eprintln!("Thread has not responded in the last two seconds.");
});
let result: Result<String, ComputeError> = cancel_this::on_timeout(Duration::from_millis(200), || {
let r1 = cancel_this::on_sigint(|| {
// This operation can be canceled using Ctrl+C, but the timeout still applies.
compute(5)
})?;
assert_eq!(r1.as_str(), "25");
// This will be canceled. Instead of using `?`, we check
// that the operation actually got canceled.
let r2 = compute(20);
assert!(matches!(r2, Err(ComputeError::Cancelled)));
// Even though the execution is now canceled, we can still execute code in
// the "cancel-never" blocks.
let r3 = cancel_this::never(|| compute(10))?;
assert_eq!(r3.as_str(), "100");
compute(10) // This should get immediately canceled.
});
// The liveness monitoring is active while `guard` is in scope. Once `guard` is dropped here,
// the liveness monitoring is turned off as well.§Multi-threaded example
Virtually all triggers and guards provided by cancel_this only apply to the current
thread. However, since triggers can be safely shared across threads, it is possible to
transfer them from one thread to another. Note that the transferred triggers also inherently
update the liveness guard of the original thread.
let guard = LivenessGuard::new(Duration::from_millis(10), |is_alive| {
// In this test, the liveness guard should never trigger, even though the original
// thread goes to sleep for a long time, waiting to join with the spawned thread.
assert!(is_alive);
});
let result: Cancellable<u32> = cancel_this::on_timeout(Duration::from_millis(100), || {
let active = cancel_this::active_triggers();
let t1: JoinHandle<Cancellable<u32>> = std::thread::spawn(|| {
cancel_this::on_trigger(active, || {
let mut result = 0u32;
// This cycle is eventually going to get canceled
// by the timer which is "transferred" from the spawning thread.
for i in 0..50 {
result += 1;
is_cancelled!()?;
std::thread::sleep(Duration::from_millis(5));
}
Ok(result)
})
});
// Put the spawning thread to sleep until `t1` finishes.
t1.join().unwrap()
});
assert!(result.is_err());Doing the same without transferring cancellation triggers will cause the spawning thread to be registered as unresponsive and the compute thread to never actually get canceled:
let guard = LivenessGuard::new(Duration::from_millis(10), |is_alive| {
// In this test, the liveness guard should never trigger, even though the original
// thread goes to sleep for a long time, waiting to join with the spawned thread.
assert!(!is_alive);
});
let result: Cancellable<u32> = cancel_this::on_timeout(Duration::from_millis(100), || {
let t1: JoinHandle<Cancellable<u32>> = std::thread::spawn(|| {
let mut result = 0u32;
// This cycle is never going to get canceled because we didn't transfer
// the timeout trigger from the original thread.
for i in 0..50 {
result += 1;
is_cancelled!()?;
std::thread::sleep(Duration::from_millis(5));
}
Ok(result)
});
// Put the spawning thread to sleep until `t1` finishes.
t1.join().unwrap()
});
assert!(result.is_ok());§Cached triggers example
If you need the absolute lowest overhead, you might want to sacrifice some of the
ergonomics provided by cancel_this. To reduce overhead, you can create a local copy
of the thread-local triggers the same way as in the multithreaded example and use it
directly with is_cancelled. This significantly reduces the overhead of each
cancellation check.
let result: Cancellable<u32> = cancel_this::on_timeout(Duration::from_millis(100), || {
let cache = cancel_this::active_triggers();
let mut result = 0u32;
while true {
// The overhead of this `is_cancelled` call is reduced because triggers
// are cached in a local variable. However, the cache is only valid within
// the scope where it was obtained.
is_cancelled!(cache)?;
result += 1;
}
Ok(result)
});
assert!(result.is_err());Macros§
- is_
cancelled - Call this macro every time your code wants to check for cancellation. It returns
Result<(), Cancelled>, which can typically be propagated using the?operator.
Structs§
- Cancel
Atomic - Implementation of
CancellationTriggerthat is canceled manually by callingCancelAtomic::cancel. See alsoon_atomic. - Cancel
Chain - Implementation of
CancellationTriggerwhich chains together several trigger implementations. - Cancel
Never - Implementation of
CancellationTriggerthat is never canceled. - Cancel
Timer - Implementation of
CancellationTriggerthat is canceled once the specifiedDurationelapsed. The “timer” is started immediately upon creation. - Cancelled
- Cancellation error type. Should include the cause of cancellation (name of the
crate::CancellationTriggertype that caused the error).
Constants§
- UNKNOWN_
CAUSE - The “default”
Cancelledcause, reported when the trigger type is unknown.
Traits§
- Cancellation
Trigger - Defines an object that can be used to trigger cancellation.
Functions§
- active_
triggers - Get a snapshot of the current thread-local cancellation trigger.
- check_
cancellation - Returns
CancelledifCancellationTrigger::is_cancelledof the giventriggeris true. In typical situations, you don’t use this method directly, but instead use theis_cancelledmacro. - check_
local_ cancellation - Check if the current thread-local cancellation trigger is canceled. In typical situations,
you don’t use this method directly, but instead use the
is_cancelledmacro. - never
- Run the given
actionby overriding current cancellation criteria withCancelNever, meaning they do not apply and the action is never canceled. - on_
atomic - Run the given
action, cancelling it if the providedCancelAtomictriggeris canceled by some external mechanism. - on_
timeout - Run the given
action, cancelling it if the provideddurationof time has elapsed, measured by theCancelTimer. - on_
trigger - Run the
actionin a context where a cancellation can be signaled using the giventrigger.
Type Aliases§
- Cancellable
- A result of a cancellable operation.
- Dynamic
Cancellation Trigger - A dynamic boxed
CancellationTrigger.