fix(service): reconcile re-inspects a container before reaping its row (UC-20) - #360
Merged
Merged
Conversation
…w (UC-20)
Reconcile's "container gone" branch deletes the sandbox store row AND its
cluster FSM placement whenever a sandbox ID is absent from a single bulk
ListManaged() snapshot. That snapshot is one racy read per tick: the
containerd driver's ListManaged silently skips a container whose per-item
Inspect errors at that instant (mid-adopt / mid-start — lifecycle.go's
`if err != nil { continue }`). A create that is merely still settling is
then indistinguishable from a durably-dead container, so reconcile reaps a
healthy sandbox — dropping its row and placement, which surfaced as an
intermittent cross-node snapshot 404 (UC-20: create+own on node A, snapshot
routed to node B → 404 because A's reconcile already deleted it).
Fix: reconcileGoneContainerConfirmed re-Inspects the specific container
before the destructive teardown. A container the driver still resolves
(no error, non-empty runtime identity) proves the bulk snapshot was stale,
so the row is spared and the next steady-state pass adopts it. A genuinely
gone container (Inspect error / empty identity) still reaps, so orphan-row
cleanup and host_port freeing are unchanged.
Regression: TestReconcileReInspectsBeforeReap (verified fails without the
guard — reaped on a stale snapshot — and passes with it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
microvm | 3e085bc | Jul 23 2026, 12:54 AM |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…nches Adds a direct unit test for the guard's nil-row and unregistered-runtime- driver paths, which the Reconcile-level regression can't reach. Brings the helper to 100% and lifts codecov/patch above the project target. Co-Authored-By: Claude Opus 4.8 <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
Reconcile's "container gone" branch (
internal/service/service.go) deletes the sandbox store row and its cluster FSM placement whenever a sandbox ID is absent from a single bulkListManaged()snapshot. That snapshot is one racy read per tick — the containerd driver'sListManagedsilently skips a container whose per-itemInspecterrors at that instant (mid-adopt / mid-start;lifecycle.godoesif err != nil { continue }). A create that is merely still settling is then indistinguishable from a durably-dead container, so a healthy sandbox gets reaped.Observed live (UC-20, cluster-mixed-benchmark-with-obs): node A creates+owns
sb-…(POST 201), its own 5-min reconcile tick logsaudit reconcile destroyed~180ms later, and the follow-up snapshot — routed by the domain LB to node B — returns 404 because A already deleted the row + placement.flowchart TD C[create returns 201 on owner node A] --> R[reconcile tick: bulk ListManaged] R --> M{sb.ID in snapshot?} M -- yes --> OK[steady-state update] M -- no --> G{"BEFORE: reap immediately<br/>AFTER: re-Inspect the container"} G -- "AFTER: driver resolves it (alive)" --> SP[spare row; next pass adopts] G -- "gone (error / empty identity)" --> D[reap: delete row + FSM placement]Fix
reconcileGoneContainerConfirmedre-Inspects the specific container before the destructive teardown. A container the driver still resolves (no error, non-empty runtime identity) proves the bulk snapshot was stale → spare the row. A genuinely gone container (Inspect error / empty identity) still reaps, so orphan-row cleanup and host_port freeing are unchanged.pr-review.md call-outs
Reconcile, a background 5-min sweep.CreateSandboxand its callees are untouched. First-call case: N/A.continues — no partial caddy/store writes. This directly addresses the "reconcile as routine cleanup" hazard by not leaning on a single racy signal to destroy state.host_port) still runs for genuinely-gone containers;TestReconcileDestroyedRowFreesHostPortandTestDestroyEventFreesHostPortstill pass.deleteSelfOwnedClusterPlacement; the guard makes placement deletion fire only after a targeted Inspect confirms the container is gone — strictly fewer spurious FSM placement deletions. No split-brain / replay implications (this only makes local reap more conservative; it never writes to the FSM). Single-node unaffected:runtimeForSandbox+Inspectbehave the same, and the Noop cluster'sdeleteSelfOwnedClusterPlacementis already a no-op.Test plan
TestReconcileReInspectsBeforeReap(internal/service/reconcile_destroy_test.go): re-inspectable container → spared; not-found → reaped. Verified fails without the guard (reaped on a stale snapshot) and passes with it.go test ./internal/service/...green (42s);go vet+go build ./...+gofmtclean.Known limitation
This closes the mechanism where the bulk
ListManagedtransiently missed a container that was actually alive. If the deeper trigger is a genuine container removal ~180ms post-create (the ordering evidence is consistent with that too), re-Inspect also fails and the row still (correctly) reaps — UC-20 would not fully close and the true culprit would remain. Distinguishing needs an instrumented live run (log every containerd container-delete with its caller); tracked separately, not in this PR.🤖 Generated with Claude Code