Cancellation — Interrupting Gracefully
Cancellation in async code is notoriously difficult to get right. id_effect makes it explicit and cooperative.
The Model: Cooperative Cancellation
Fibers aren't forcibly killed. They're interrupted — given a signal to stop at the next safe checkpoint. The fiber cooperates by checking for interruption at yield points.
The simplest yield point is check_interrupt:
#![allow(unused)] fn main() { use id_effect::check_interrupt; effect! { for chunk in large_dataset.chunks(1000) { ~ check_interrupt(); // yields; if interrupted, stops here process_chunk(chunk); } "done" } }
Every ~ binding is also an implicit yield point. If a fiber is interrupted while awaiting an effect, the interruption propagates to the next ~ bind.
CancellationToken
For external cancellation (e.g., from HTTP request handlers or UI cancel buttons):
#![allow(unused)] fn main() { use id_effect::CancellationToken; // Create a cancellation token let token = CancellationToken::new(); // Pass it to a long-running effect let effect = long_running_job().with_cancellation(&token); // Spawn it let handle = effect.fork(); // Later, cancel from outside token.cancel(); // The fiber will stop at the next check_interrupt let exit = handle.join().await; // exit will be Exit::Failure(Cause::Interrupt) }
Tokens can be cloned and shared. Cancelling any clone cancels all effects sharing that token.
Interrupting Directly via FiberHandle
#![allow(unused)] fn main() { let handle = background_work().fork(); // After some timeout or external event: handle.interrupt(); let exit = handle.join().await; // Cleanup (finalizers, scopes) runs before the handle resolves }
.interrupt() sends the interruption signal. The fiber's finalizers (Chapter 10) still run. .join() waits for them to complete.
Graceful Shutdown
Interruption is the mechanism for graceful shutdown. The pattern:
- Signal all top-level fibers with
.interrupt() - Wait for all handles to join (with a timeout)
- If any fiber doesn't stop within the timeout, escalate
#![allow(unused)] fn main() { let handles: Vec<FiberHandle<_, _>> = workers.iter().map(|w| w.fork()).collect(); // Shutdown signal received for h in &handles { h.interrupt(); } // Wait with timeout for h in handles { tokio::time::timeout(Duration::from_secs(5), h.join()).await; } }
Because effect finalizers run on interruption, all resources are cleaned up as fibers stop — no manual cleanup required at the shutdown handler.
Uninterruptible Regions
Some operations shouldn't be interrupted mid-way (e.g., writing to a database inside a transaction). Mark them uninterruptible:
#![allow(unused)] fn main() { use id_effect::uninterruptible; // This block runs to completion even if interrupted let committed = uninterruptible(effect! { ~ begin_transaction(); ~ insert_records(); ~ commit(); }); }
The interruption is deferred until committed completes. Use sparingly — long uninterruptible regions delay shutdown.