handoff
Replace the binary of a running stateful daemon: no dropped connections, no lost writes, no visible socket close.
What you get
- No dropped connections. The supervisor holds listener FDs across swaps. The kernel's accept queue absorbs the gap between old and new processes.
- No lost writes. The old process drains in-flight requests and fsyncs before the new one touches the data directory. The protocol enforces this ordering.
- No split-brain. Exactly one process holds the data-directory flock at any time. The new binary cannot write until the old one releases it.
- Automatic rollback. If the new binary fails before declaring ready, the old process reacquires the flock, reopens its writer state, and resumes serving. No manual intervention.
How it works
Three roles: a supervisor that holds listener FDs and drives the swap, an incumbent (old binary) that drains and seals its state, and a successor (new binary) that takes over when the incumbent confirms it's clean. The protocol is acknowledged at every phase; a crash at any point leaves the system in a recoverable state.
See ARCHITECTURE.md for the wire protocol, state machine, and correctness invariants.
Platforms
Linux, macOS, and the BSDs. The mechanism is plain POSIX — fork/exec FD inheritance, flock, Unix-domain control sockets, and signals — with no Linux-only syscalls. Windows is unsupported: it has no fork/exec FD inheritance and no flock, so the handoff model doesn't map without a separate backend.
Integrate your daemon
1. Implement Drainable
use ;
use Instant;
2. Detect role at startup
use ;
use ReadinessSnapshot;
Trigger a handoff
Reference supervisor binary
# handoff.toml
= "/run/my-daemon/handoff.sock"
= "/run/my-daemon/handoff.trigger"
= "/usr/local/bin/my-daemon"
= 25
= 60
[[]]
= "http"
= "0.0.0.0:8080"
# Start the supervisor (it cold-starts your daemon):
# Later, trigger a swap to a new binary:
|
# ok: handoff_id=... committed=true abort_reason=None
Embedded in code
use ;
use Duration;
let sup = new?
.with_listener;
sup.resume_from_journal?; // clear any prior crash state
let outcome = sup.perform_handoff?;
if outcome.committed else
Crates
handoff: the library. Sync, no async runtime dependency. ImplementDrainable; spawnIncumbent::serveon a dedicated thread.handoff-supervisor: reference supervisor binary for local development, tests, and simple deployments. Production hosts link the library directly.
Build
License
MIT