Skip to main content

tokio_cancel_guard/
lib.rs

1//! Explicit cancellation-safety primitives for the Tokio async runtime.
2//!
3//! When a future loses a `tokio::select!` race, Tokio drops it immediately —
4//! at any internal `.await` point, without giving the future a chance to clean up.
5//! This is correct-by-spec but dangerous in practice: staged database writes get
6//! abandoned, locks are left held, and the drop happens without any trace in your
7//! telemetry.
8//!
9//! This crate gives you three focused tools to make select-driven code safe:
10//!
11//! | Primitive | What it does |
12//! |-----------|-------------|
13//! | [`DeferGuard`] | Runs a synchronous rollback closure if the future is dropped mid-execution |
14//! | [`Shield`] | Guarantees the future runs to completion even if select! drops it |
15//! | [`CancelTrack`] | Emits a `tracing` error event at the exact drop site (requires `tracing` feature) |
16//!
17//! # Quick start
18//!
19//! ## `DeferGuard` — rollback on cancellation
20//!
21//! ```rust
22//! use tokio_cancel_guard::DeferGuard;
23//! use std::sync::{Arc, Mutex};
24//!
25//! # #[tokio::main]
26//! # async fn main() {
27//! let dirty = Arc::new(Mutex::new(false));
28//! let dirty_clone = dirty.clone();
29//!
30//! let write_op = async move {
31//!     *dirty_clone.lock().unwrap() = true;  // stage
32//!     tokio::time::sleep(std::time::Duration::from_millis(50)).await;
33//!     *dirty_clone.lock().unwrap() = false; // commit
34//! };
35//!
36//! let rollback = {
37//!     let dirty = dirty.clone();
38//!     move || { *dirty.lock().unwrap() = false; }
39//! };
40//!
41//! let guarded = DeferGuard::new(write_op, rollback);
42//!
43//! tokio::select! {
44//!     _ = guarded => {}
45//!     _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
46//!         // timeout wins — rollback fires automatically
47//!     }
48//! }
49//! # }
50//! ```
51//!
52//! ## `Shield` — guaranteed completion
53//!
54//! ```rust
55//! use tokio_cancel_guard::Shield;
56//!
57//! # #[tokio::main]
58//! # async fn main() {
59//! let critical = async {
60//!     tokio::time::sleep(std::time::Duration::from_millis(50)).await;
61//!     // this always runs — even if select! drops the Shield handle
62//! };
63//!
64//! tokio::select! {
65//!     _ = Shield::new(critical) => {}
66//!     _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
67//!         // Shield migrates the future to a background task and lets it finish
68//!     }
69//! }
70//! # }
71//! ```
72//!
73//! ## `CancelTrack` — observability (feature `tracing`)
74//!
75//! ```rust,ignore
76//! # #[cfg(feature = "tracing")]
77//! use tokio_cancel_guard::CancelTrack;
78//!
79//! # #[cfg(feature = "tracing")]
80//! # #[tokio::main]
81//! # async fn main() {
82//! let op = async {
83//!     tokio::time::sleep(std::time::Duration::from_millis(50)).await;
84//! };
85//!
86//! let tracked = CancelTrack::new(op, "my_write_task");
87//! // On cancellation, emits a tracing::error! with task name, span ID,
88//! // and OpenTelemetry-compatible otel.status_code / otel.status_description fields.
89//! # }
90//! ```
91//!
92//! # Feature flags
93//!
94//! | Flag | Enables |
95//! |------|---------|
96//! | `tracing` | [`CancelTrack`] — cancellation telemetry via the `tracing` crate |
97//!
98//! # Testing utilities
99//!
100//! The [`test_utils`] module ships two helpers for writing deterministic
101//! cancellation tests in your own crate:
102//!
103//! - [`test_utils::simulate_drop_after`] — race a future against a timeout
104//! - [`test_utils::TrackPolls`] — count how many times a future was polled
105
106#[cfg(feature = "tracing")]
107pub mod cancel_track;
108pub mod defer_guard;
109pub mod shield;
110pub mod test_utils;
111
112#[cfg(feature = "tracing")]
113pub use cancel_track::CancelTrack;
114pub use defer_guard::DeferGuard;
115pub use shield::Shield;