graceful-worker
Cooperative shutdown, and a retry backoff that sleeps in slices so a long
wait never delays noticing SIGTERM.
use ;
# async
# async
The problem
A container platform stops a process by sending SIGTERM and then waiting a
fixed grace period — often thirty seconds — before SIGKILL. Two things have
to be true inside that window, and they pull against each other.
Work in flight must finish. A worker that has taken a message off a queue
and not yet acknowledged it must not be cancelled mid-message, or the message
is lost. So shutdown here is cooperative: Shutdown asks loops to stop at
their next opportunity rather than cancelling whatever is running.
A long retry wait must not outlast the grace period. This is the part other backoff crates get wrong.
The slicing
backoff, backon and exponential-backoff all sleep monolithically. A
fifteen-minute backoff is one fifteen-minute sleep, and a SIGTERM arriving
one second into it is noticed fifteen minutes later — long after the platform
gave up waiting and sent SIGKILL. The process dies mid-message, which is the
exact outcome cooperative shutdown existed to prevent.
Backoff::wait takes a Watcher and sleeps in bounded slices, so the wait is
interruptible at every slice boundary regardless of the total delay:
use Duration;
use ;
# async
The slice is the worst case for how long a stop goes unnoticed during a wait, so set it comfortably shorter than your platform's grace period.
Configuring the schedule
The defaults — five seconds, doubling to a fifteen-minute ceiling, sliced at a minute — are a working set, not a recommendation. What matters for a given system is the relationship between the slice and the grace period, and only you know either.
use Duration;
use Backoff;
let backoff = new
.with_initial_delay
.with_max_delay
.with_slice
.with_factor;
Guarded against the ways this goes wrong: an initial delay longer than the
ceiling is clamped rather than counting downwards, a factor of zero is treated
as one rather than retrying instantly forever, and a slice is never zero while
time remains — which is what stops wait spinning.
Two decisions worth knowing
Dropping a Shutdown does not stop anything. A worker's shutdown must be
something someone asked for, not a consequence of where a value happened to go
out of scope. The alternative is a refactor that moves a binding and silently
turns a long-running process into one that exits immediately.
failed() returns the delay that applied to this failure, not the
multiplied value the next one will use — so a metric charts the actual
schedule.
Features
tracing(default) — a few log lines about signals and stops.
License
MIT or Apache-2.0, at your option.