Skip to main content

graceful_worker/
lib.rs

1//! Cooperative shutdown, and a retry backoff that sleeps in slices.
2//!
3//! Every long-running worker has the same skeleton: do a unit of work,
4//! repeat until told to stop, and wait a bit longer each time something
5//! goes wrong. This crate is that skeleton's two hard parts.
6//!
7//! # The problem it exists for
8//!
9//! A container platform stops a process by sending `SIGTERM` and then
10//! waiting a fixed grace period — often thirty seconds — before `SIGKILL`.
11//! Two things have to be true inside that window, and they pull against
12//! each other:
13//!
14//! 1. **Work in flight must finish.** A worker that has taken a message off
15//!    a queue and not yet acknowledged it must not be cancelled mid-message,
16//!    or the message is lost. So shutdown is *cooperative*: [`Shutdown`]
17//!    asks loops to stop at their next opportunity rather than cancelling
18//!    whatever is running.
19//!
20//! 2. **A long retry wait must not outlast the grace period.**
21//!    `backoff`, `backon` and `exponential-backoff` compute a schedule and
22//!    sleep it; none takes a shutdown signal, so a worker has to wrap every
23//!    wait in its own `select!` and get that right at each call site.
24//!    [`Backoff::wait`] takes a [`Watcher`] and returns `false` when a stop
25//!    arrived mid-wait, so a fifteen-minute backoff is abandoned the moment
26//!    `SIGTERM` lands rather than fifteen minutes later.
27//!
28//! # The loop
29//!
30//! ```
31//! # #[tokio::main(flavor = "current_thread")]
32//! # async fn main() {
33//! use graceful_worker::{Backoff, Shutdown};
34//!
35//! let shutdown = Shutdown::new();
36//! shutdown.listen_for_signals();
37//!
38//! let watcher = shutdown.watcher();
39//! let mut backoff = Backoff::new();
40//!
41//! # shutdown.stop();
42//! while watcher.is_running() {
43//!     match do_some_work().await {
44//!         Ok(()) => backoff.succeeded(),
45//!         Err(_) => {
46//!             backoff.failed();
47//!             // False means a stop arrived mid-wait: leave, do not retry.
48//!             if !backoff.wait(&watcher).await {
49//!                 break;
50//!             }
51//!         }
52//!     }
53//! }
54//! # async fn do_some_work() -> Result<(), ()> { Ok(()) }
55//! # }
56//! ```
57//!
58//! # Two decisions worth knowing
59//!
60//! **Dropping a [`Shutdown`] does not stop anything.** A worker's shutdown
61//! must be something someone asked for, not a consequence of where a value
62//! happened to go out of scope.
63//!
64//! **The schedule is configurable and the defaults are not a
65//! recommendation.** Five seconds, doubling to fifteen minutes. What
66//! matters for a given system is the ceiling against the platform's grace
67//! period, and only the caller knows either.
68//!
69//! **Slicing is a knob, not the mechanism.** [`Backoff::wait`] sleeps in
70//! slices, but that is not what makes it interruptible — each slice is a
71//! `select!` against the cancellation token, so an unsliced sleep would be
72//! just as responsive. See the [`backoff`] module documentation.
73//!
74//! # Features
75//!
76//! - `tracing` *(default)* — a few log lines about signals and stops.
77
78#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
79
80pub mod backoff;
81pub mod shutdown;
82
83pub use backoff::Backoff;
84pub use shutdown::{Shutdown, Watcher};