Skip to content

fix(cli): read the spawner's sessions field in the Docker sandbox drain - #5429

Merged
yannickmonney merged 1 commit into
mainfrom
fix/cli-sandbox-drain-status
Nov 13, 2026
Merged

yannickmonney merged 1 commit into
mainfrom
fix/cli-sandbox-drain-status

Conversation

@yannickmonney

Copy link
Copy Markdown
Contributor

Defect

Every Docker tale deploy that recreates the sandbox spawner waited the full 300 s drain budget, even when the spawner was idle.

Root cause

tools/cli/src/lib/actions/drain-sandbox.ts read an inFlight field from the spawner's GET /v1/drain-status. The spawner stopped sending that field in #2301 (2026-07-02). Since then it answers {draining, sessions, sessionIds} (services/sandbox/src/control-routes.ts). Every read parsed as "unknown", so the poll always ran to its deadline. The fake runtime still answered the old {"inFlight":0}, which is why no test caught it. The Incus path (deployment/runtime-spawner.ts) already read the right shape.

Fix

  • The drain-status schema is now defined once, as spawnerDrainStatusSchema / parseSpawnerDrainStatus in runtime-spawner.ts. The parse also checks that the count matches the unique ids. The managed (Incus) runtime and the Docker drain both use it.
  • The Docker drain waits for sessions === 0.
  • A failed status call is still "unknown" and polling goes on, as before.
  • An answer that breaks the contract (a missing or extra field, a count that disagrees with the ids, or text that isn't JSON) logs one warning and is also treated as "unknown" until the deadline. A drifted answer never reads as "drained", so Docker keeps its old fail-safe behaviour here.
  • The fake runtime now answers the spawner's real shape.

The inFlight readers that remain are the backend's /api/control/drain-status (drain-backend.ts), which really does send inFlight, and the Incus malformed-status fixtures, which use it on purpose. Neither is touched.

Regression test

drain-sandbox.test.ts now uses the spawner's real answer. One new test checks that an idle spawner returns after a single status read with no warning. Four more cover the broken-contract answers: each warns once and then waits as "unknown". To prove the regression, I put origin/main's drain-sandbox.ts back under the new tests: 7 of 16 fail. With the fix, 16 of 16 pass.

What was run (ops-lab3-ci, tools/cli, after bun run generate)

  • bun test on drain-sandbox, runtime-spawner, runtime-configuration and runtime-incus-apply: 68 pass, 0 fail.
  • tsc --noEmit: clean. oxlint: 0 warnings, 0 errors. oxfmt --check: clean.
  • Full bun test: 4602 pass, 4 fail. Clean origin/main fails the same 4 (4597 pass): 'cached site plans preserve Compose overrides…', 'Checks execution optimizations > platform Unit…', and two 'real non-cone scope and readiness checkouts' tests.

The Docker deploy's sandbox drain read an `inFlight` field from
GET /v1/drain-status, but the spawner has answered
{draining, sessions, sessionIds} since the one-shot lane was retired.
Every status read came back "unknown", so each Docker deploy waited the
full 300 s drain budget before recreating the spawner.

The drain-status schema now lives once in deployment/runtime-spawner.ts
and both the managed runtime and the Docker drain parse with it. A
failed status call is still "unknown" and keeps polling. An answer that
breaks the contract warns once and is also treated as "unknown": a
drifted answer proves nothing about the sessions still held, so it never
reads as drained. The fake runtime answers the spawner's real shape.
@yannickmonney
yannickmonney merged commit 137ef55 into main Nov 13, 2026
80 checks passed
@yannickmonney
yannickmonney deleted the fix/cli-sandbox-drain-status branch November 13, 2026 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant