Skip to content

feat(dolt): drain the off-box backlog by CLI push before the sql-server starts (vp-6hb8) - #131

Merged
bourgois merged 1 commit into
mainfrom
gc/vp-6hb8-boot-drain
Aug 11, 2026
Merged

feat(dolt): drain the off-box backlog by CLI push before the sql-server starts (vp-6hb8)#131
bourgois merged 1 commit into
mainfrom
gc/vp-6hb8-boot-drain

Conversation

@bourgois

Copy link
Copy Markdown
Collaborator

The trap this closes

Pushing over the sql-server pays a cold-open cost once per server lifetime per database: the git-blobstore transport has no server-side range reads, so the first push of a lifetime spools the store's whole remote blobset (measured: 822 MB / 311 git calls for a 1 GB store) while the listener's 15s read_timeout kills the query mid-open. A store that misses one push accumulates backlog, making the next attempt larger — measured live: 0 → 8,514 unpushed commits in five days, unbackable the whole time. Every server restart re-arms the trap.

Every alternative was ruled out by measurement: session-scoped timeouts are ignored by the listener; raising the global read_timeout re-arms the 2026-06-15 connection pileup (wait_timeout verified inert — 8 idle connections alive at t+48s under wait_timeout=30); retry loops make literally zero progress (12 consecutive attempts, backlog unchanged — the push dies during the remote-db open, before any chunk transmits).

The mechanism

Drain each database by CLI dolt push before the sql-server starts. The start path has just waited for the data-dir LOCK to be free, so no server owns the store and CLI access is safe — and a CLI push has no listener in front of it: no timeout raise, no restore, no second restart. The server boots cold but with zero backlog, which fits the production window (measured 2026-08-06: all nine stores passed at 15s immediately after a drain, largest 13.2s); the patrol keeps it current from there.

Failure posture

  • Never blocks the boot — five of nine fleet stores have hit remote-side Blob not found archive corruption; a dead remote must not brick the local server. Every failure is loud, boot proceeds.
  • Never force-pushes — a diverged store needs an ownership decision this code cannot make (vp-ukvx).
  • Pushes each database's own checked-out head from repo_state.json; unreadable state is UNKNOWN and skipped with a reason, never pushed at a guessed branch.
  • Budget exhaustion names the databases it did not attempt.
  • Default ON (GC_DOLT_BOOT_DRAIN=off to disable; GC_DOLT_BOOT_DRAIN_BUDGET, default 10m) — an unattended restart must not silently re-arm the ratchet.

Verification

5 behavioural mutations, each verified applied (anchor exactly once, mutant compiles) before its result counted; two initial mutants were invalid Go or mis-anchored and were redone rather than recorded. Failure-stops-sweep → 1 red; guessed-branch → 1; silent exhaustion → 1; mangled-state-pushed → 2; kill-switch-default-off → 1.

Both repo governance guards were triggered and satisfied deliberately: the two new env vars are registered in the GC_* vocabulary golden, and the resource census's no-new-sleeps invariant pushed the budget test onto an injected clock seam (bootDrainNowFn) — deterministic instead of timing-dependent.

Deploy note

Takes effect on the next gc binary rollout + city restart; on that first boot it will also self-heal whatever backlog has accumulated by then.

…er starts (vp-6hb8)

THE TRAP. Pushing over the sql-server pays a cold-open cost once per server
lifetime per database: the git-blobstore transport has no server-side range
reads, so the first push of a lifetime spools the store's whole remote blobset
(measured: 822 MB / 311 git calls for a 1 GB store) while the listener's
read_timeout — 15s in production, deliberately: it is the only working
idle-connection reaper, wait_timeout being accepted but inert (verified
empirically 2026-08-11, 8 idle connections alive at t+48s under
wait_timeout=30) — kills the query mid-open. A store that misses one push
accumulates backlog, which makes the next attempt larger, which makes it miss
again: measured live, one store ratcheted 0 -> 8,514 unpushed commits in five
days, unbackable the whole time. Every server restart re-arms the trap for
every large store at once. Session-scoped timeouts are ignored by the listener;
raising the global read_timeout reproduces the 2026-06-15 connection pileup;
retry loops make literally zero progress (twelve consecutive attempts, backlog
unchanged — the push dies during the remote-db open, before any chunk
transmits).

THE MECHANISM. Drain each database by CLI `dolt push` BEFORE the sql-server
starts. startManagedDoltProcessWithOptions has just waited for the data-dir
LOCK to be free, so no server owns the store and CLI access is safe — and a
CLI push has no listener in front of it, so no read_timeout applies, no config
raise/restore, no second restart. The server then boots cold but with ZERO
backlog, and a cold push of a near-empty delta fits the production window
(measured 2026-08-06: every store passed at 15s immediately after a drain,
largest 13.2s), after which the store is warm and stays current via the patrol.

FAILURE POSTURE. The drain never blocks the boot: a store with an unreachable
or corrupted remote (five of nine fleet stores have hit remote-side "Blob not
found" archive corruption) still needs its LOCAL server, so every per-database
failure is loud and boot proceeds. It never force-pushes — a diverged store
needs an ownership decision this code cannot make (vp-ukvx). The branch pushed
is the database's OWN checked-out head from repo_state.json; an unreadable
state is UNKNOWN and skipped with a reason, never pushed at a guessed branch.
Budget exhaustion NAMES the databases it did not attempt (a truncated sweep
that reads as complete is the vp-g2m4 shape). Default ON — the drain exists so
an UNATTENDED restart cannot silently re-arm the ratchet — with
GC_DOLT_BOOT_DRAIN=off for maintenance windows that manage draining
themselves, and GC_DOLT_BOOT_DRAIN_BUDGET for the wall-clock cap (default 10m;
a full-fleet drain after five days of backlog measured ~6m worst case).
Fprintf returns to the operator log are explicitly ignored: best-effort
logging must never block a boot.

Mutations, each verified applied (anchor exactly once, mutant compiles) before
its result counted; two initial mutants were themselves invalid Go (unused
variable) or mis-anchored and were REDONE rather than recorded:
  M1 failure stops the sweep            -> 1 failure
  M2 pushed at a guessed branch          -> 1 failure
  M3 exhaustion stops naming the rest    -> 1 failure
  M4 mangled repo_state pushed anyway    -> 2 failures
  M5 kill switch defaults off            -> 1 failure
@bourgois
bourgois merged commit c4eff4d into main Aug 11, 2026
82 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant