feat: Docker events watcher for near-instant liveness reconcile (#1018 slice B) - #1020
Merged
Conversation
…slice B) The reactive proxy recovery (#1019) collapses the recovery window for a request that hits a dead upstream, but a spec with no incoming traffic (or a min-replicas warm pool) still waited up to a full ~10s scaler tick to notice an external `docker rm -f` / `docker restart`. This subscribes to the Docker event stream so the runtime reconciles within ~1s. - core: `ContainerEvent`/`ContainerEventKind` + `ContainerEventStream` and a `ContainerBackend::container_events()` trait method. Default returns an empty stream — events are a latency optimization, never the source of truth, so a backend without support (mocks, future backends) just relies on the periodic reconcile. - ruscker-docker: LocalDockerBackend streams bollard events filtered server-side to our containers' lifecycle actions (type=container, label=ruscker.replica_id, event in start/die/stop/kill/destroy/oom); the stream ends on a transport error so the consumer reconnects. MultiHostDockerBackend merges per-host streams; a host that can't open one is skipped (periodic reconcile covers it) and never fails the whole call. - ruscker-admin: `scaler::reconcile_liveness_once` factors the #1017 reconcile pass so the events watcher and the periodic tick take the exact same authoritative, idempotent path. `spawn_event_watcher` consumes the stream, coalesces a burst (a restart is die+start; a rm is die+destroy) into one reconcile, is leader-gated like the tick, and reconnects with backoff. Wired into serve alongside the scaler; the periodic reconcile remains the fallback for anything missed during a reconnect gap. The event is only a "reconcile now" nudge — the authoritative `replica_liveness` pass decides the action — so a missed or duplicated event is always safe. Tests: unit for the action->kind map and the EventMessage->ContainerEvent mapping (non-container / unlabelled ignored); a scaler test that `reconcile_liveness_once` prunes a Missing replica (the watcher's entry point); and a docker-it real smoke that opens the stream and asserts Start on spawn + die/destroy on external removal against a live daemon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
Review (adversarial pass) — MERGE-OK
Real smoke: docker-it opens the stream and asserts No blockers. |
milkway
marked this pull request as ready for review
July 20, 2026 19:29
8 tasks
milkway
added a commit
that referenced
this pull request
Jul 20, 2026
Self-healing when a managed container changes outside Ruscker: authoritative + bidirectional liveness reconcile (#1017, closes #1015/#1016), reactive proxy recovery on a confirmed-dead upstream (#1019), and a Docker events watcher for near-instant reconcile (#1020, both close #1018). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Slice B of #1018 — the second, independent half of the follow-up to #1015/#1016.
recovers immediately instead of waiting for the tick.
But a spec with no incoming traffic — or a
min-replicaswarm pool that nobodyis actively hitting — still waited up to a full ~10 s scaler tick to notice an
external
docker rm -f/docker restart. This subscribes to the Docker eventstream so the runtime reconciles within ~1 s, with no request needed to trigger
it.
How
ContainerEvent/ContainerEventKind+ContainerEventStream, and aContainerBackend::container_events()trait method. Default returns an emptystream — events are a latency optimization, never the source of truth, so mocks
and any backend without support just rely on the periodic reconcile.
LocalDockerBackend): streams bollard events filteredserver-side to our containers' lifecycle actions
(
type=container,label=ruscker.replica_id,event ∈ {start,die,stop,kill,destroy,oom}),so daemon-wide churn and noisy actions (exec/health) never reach us. The stream
ends on a transport error so the consumer reconnects.
MultiHostDockerBackend): merges per-host streams withselect_all; a host that can't open one is skipped (its containers stillrecover via the periodic reconcile) and never fails the whole call. A host stream
that later ends just drops out of the merge; the watcher's reconnect re-establishes
every host.
scaler::reconcile_liveness_oncefactors the fix: authoritative + bidirectional liveness reconcile for external docker changes (#1015, #1016) #1017 reconcilepass so the events watcher and the periodic tick take the exact same
authoritative, idempotent path.
spawn_event_watcherconsumes the stream,coalesces a burst (a restart is die+start; a rm is die+destroy) into one
reconcile once the stream goes quiet for 400 ms, is leader-gated like the tick,
and reconnects with backoff. Wired into
servenext to the scaler.The event is only a "reconcile now" nudge — the authoritative
replica_livenesspassdecides the action — so a missed or duplicated event is always safe, and the
periodic reconcile remains the fallback for anything missed during a reconnect gap.
Acceptance criteria (slice B)
with no request needed to trigger it.
"silence" — only the authoritative reconcile prunes.
with no prune or duplicate.
Interaction with #1016 / #1019
The watcher just runs the same reconcile: a
destroy→Missing→ prune + respawn;a restart's
die+startcoalesce into one pass that sees the container alreadyRunning→ re-adopt (orStopped→ grace window if the restart is slow). Concurrentruns with the periodic tick are safe (registry writes serialized, every action
re-checks under lock).
Tests
action → ContainerEventKindmap;EventMessage → ContainerEvent(a non-container or unlabelled event maps to
None);reconcile_liveness_onceprunes a
Missingreplica (the watcher's entry point).Starton spawn and adie/stop/destroyon external removal (fired from delayed tasks while the stream isalready being polled, since bollard connects lazily). Run:
cargo test -p ruscker-docker --features docker-it.Gate
cargo test· clippy (clean on touched files) ·./scripts/i18n-check.sh·docker-it (ruscker-docker 52 passed, ruscker-admin 429 passed) against real
Docker 29.6.1 — all green.
🤖 Generated with Claude Code