Skip to content

fix(deploy): prune stale binary backups instead of accumulating them forever (#585) - #826

Merged
defenwycke merged 1 commit into
mainfrom
fix/585-prune-stale-deploy-backups
Sep 1, 2026
Merged

fix(deploy): prune stale binary backups instead of accumulating them forever (#585)#826
defenwycke merged 1 commit into
mainfrom
fix/585-prune-stale-deploy-backups

Conversation

@defenwycke

Copy link
Copy Markdown
Contributor

#585 step 0, finally automated.

The leak

Every deploy copies the live binary to $BINARY.bak.$TS (~25 MB). Nothing has ever deleted one. July's cleanup freed 16.3 GB fleet-wide by hand, and the plan said to make it automatic. It wasn't, so it came back — measured today after the v1.11.33 roll:

node files size
vm1 39 1.4 GB
vm4 38 765 MB
vm5 33 712 MB

A manual step that must run after every deploy is a step that does not run.

The fix

Keep the two most recent per binary, delete the rest. Two, not one: rollback restores the most recent, so keeping only that leaves nothing to fall back on if the rollback itself has to be undone. Runs after the soak record and smoke test, and is best-effort — it must never fail a deploy that otherwise succeeded.

⚠ The bug I wrote first

The offset is computed locally, and that is the whole point:

PRUNE_FROM=$((PRUNE_KEEP + 1))     # here — expands to 3
... "ls -1t ... | tail -n +$PRUNE_FROM"

Written as \$((PRUNE_KEEP + 1)) inside the remote payload, the arithmetic evaluates on the far side where PRUNE_KEEP does not exist. It expands to 1, tail -n +1 selects every backup including the one the deploy just made, and the only thing a rollback has to restore is deleted. That was my first version; it is now the control case.

S=$SUDO follows the existing swap payload's pattern — assigned locally, used remotely as \$S.

Tests

Five cases in test-deploy-gate.sh, run against a temp dir rather than through ssh, because the risk is in which files the selection picks, not the plumbing. Each asserts the surviving count and, separately, that the newest survived — losing the newest is the failure that matters and a count alone can hide it.

case result
keep 2 of 5 / of 3 2 kept, newest survives
fewer files than we keep / exactly at limit no-op
CONTROL: offset 1 deletes all 5, newest gone — reproduces the bug above

The control asserts the newest was destroyed, rather than surviving. Asserting "newest survived" unconditionally would have failed the control for doing its job — and a control indistinguishable from a regression is not a control.

⚠ A second trap, caught before pushing

The cases were first appended at the end of the file, after the summary and its exit 1. They ran, incremented fail, and nothing looked at it again — the script would have exited 0 with a failing test. Moved above the summary; the success line now names what it covers so it cannot under-report.

All 96 deploy-gate checks pass. check-inlined-copies, check-workflow-scalars, check-stratum-config-agreement all clean.

Not the whole of #585

This is step 0 only. Still open there: auto_vacuum is still 0 (verified today — deletes return nothing to the filesystem), /var/log is at 2.4 GB with no cap, and the dead pre-v41 share rows. Scoping those separately on the issue.

https://claude.ai/code/session_01Td1vvfowptTTnu88qG2iym

…forever (#585)

Every deploy copies the live binary to `$BINARY.bak.$TS` (~25 MB) and nothing
has ever removed one. #585 step 0 freed 16.3 GB fleet-wide by hand in July and
said to make it automatic; it was not, so it came straight back. Measured
2026-09-01 after the v1.11.33 roll:

    vm1  39 files  1.4 GB
    vm4  38 files  765 MB
    vm5  33 files  712 MB

A manual step that must run after every deploy is a step that does not run.

The deploy now keeps the two most recent backups per binary and deletes the
rest. TWO, not one: rollback restores the most recent, so keeping only that
leaves nothing to fall back on if the rollback itself has to be undone.

It runs after the soak record and the smoke test, and is best-effort — it must
never be what fails a deploy that otherwise succeeded.

⚠ The dangerous part is the offset, and it is computed LOCALLY. Written as
`\$((PRUNE_KEEP + 1))` inside the remote payload the arithmetic evaluates on
the far side, where `PRUNE_KEEP` does not exist: it expands to 1, `tail -n +1`
selects EVERY backup including the one the deploy just made, and the only thing
a rollback has to restore is deleted. I wrote it that way first. `S=$SUDO`
follows the existing swap payload's pattern — assigned locally, used remotely.

Five cases in the deploy-gate self-test, against a temp dir rather than through
ssh, because the risk is in WHICH files the selection picks, not in the
plumbing. They assert the count AND, separately, that the newest survived —
losing the newest is the failure that matters and a count alone can hide it.
The `offset 1` control reproduces the bug above.

⚠ The cases were initially appended at the END of the file, after the summary
and its `exit 1`. They ran, incremented `fail`, and nothing looked at it again
— the script would have exited 0 with a failing test. Moved above the summary,
and the success line now names what it covers so it cannot under-report.

Claude-Session: https://claude.ai/code/session_01Td1vvfowptTTnu88qG2iym
@defenwycke
defenwycke merged commit ee5ec04 into main Sep 1, 2026
12 checks passed
defenwycke added a commit that referenced this pull request Sep 1, 2026
The arming release. Two consensus-visible gates fire from this build, and both
require every node to be running it beforehand:

  * ADDRESS_PROOF_HEIGHT   966_000  (#824, H-7, #605)
  * MESH_ENVELOPE_V2_HEIGHT 966_400  (#825, H-11, #604)

Deliberately ~400 blocks apart rather than sharing a height, so a mesh problem
in that window is attributable to one gate rather than two. MESH_ENVELOPE_V2 is
the reason that matters: it does not degrade, it PARTITIONS — a node left on an
older binary rejects every mesh message from every upgraded peer, and the only
way back is upgrading it.

⛔ ALL EIGHT must carry this before 966_400. At 965,085 that is ~8 days.

Also carries #826: the deploy now prunes stale `$BINARY.bak.*` to the two most
recent instead of accumulating them forever. Measured before the fix, after the
v1.11.33 roll: vm1 39 files / 1.4 GB, vm4 38 / 765 MB, vm5 33 / 712 MB.

Both lockfiles synced; `tests/integration-sv2`, `crates/stratum-apps` and
`prototypes/ghast-bench` carry no workspace version and still resolve under
`--locked`. Checked with `grep -F`, since an unescaped `1.11.33` matches digits
inside dependency checksums.

Claude-Session: https://claude.ai/code/session_01Td1vvfowptTTnu88qG2iym
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