Skip to content

feat(mediorum): cap ops pruning at the active-peer cursor + retention-gap signal#304

Open
RolfAris wants to merge 2 commits into
OpenAudio:mainfrom
RolfAris:feat/mediorum-ops-retention-minimal
Open

feat(mediorum): cap ops pruning at the active-peer cursor + retention-gap signal#304
RolfAris wants to merge 2 commits into
OpenAudio:mainfrom
RolfAris:feat/mediorum-ops-retention-minimal

Conversation

@RolfAris

@RolfAris RolfAris commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

#325 prunes the crudr ops log purely by age (ulid < now - retention). As noted there, a still-active peer behind the retention window — or one bootstrapping from genesis — can silently skip the pruned range. This adds the protocol-level safety #325 deferred to this PR:

  • Active-peer cursor floor. pruneOps caps its cutoff at min(time_cutoff, slowest_active_peer_cursor), so it never deletes ops a still-active peer hasn't swept. Caught-up peers sweep ~every 10 min (cursor far newer than any month/year cutoff), so this is a no-op in steady state; it only restricts pruning when an active peer is legitimately behind. The floor ignores self, decommissioned peers, and non-peer worker rows (e.g. qm_fix_truncated, which stores a CID under its own host) that would otherwise pin it.
  • Retention-gap signal. When a peer sweeps from a cursor below our oldest available op, serveCrudSweep advertises the lowest available ULID (X-Mediorum-Retention-Gap / X-Mediorum-Available-Min-Ulid). The peer advances its cursor across the gap explicitly instead of silently skipping it, with a first-contact guard and implausible-ULID rejection. Wire format unchanged; older clients ignore the headers.

Supersedes this PR's original contents (one-time dormant cleanup + opt-in per-table retention sweep), which #325 made redundant — the dormant cleanup never drained the actively-written tables, and #325's pruner replaces the opt-in sweep so there aren't two systems.

+121/−1 across four files plus a focused floor test.

Tests

go test ./pkg/mediorum/crudrTestSlowestActivePeerCursor{,_NoActivePeers,_IgnoresImplausibleActiveCursor} cover the floor: slowest active peer wins; self, decommissioned, non-peer, and implausible cursors are ignored. go build ./pkg/mediorum/... and go vet clean.

@RolfAris RolfAris force-pushed the feat/mediorum-ops-retention-minimal branch from afe1a61 to 6e59e39 Compare May 21, 2026 02:46
@RolfAris RolfAris changed the title feat(mediorum/crudr): tighten ops retention cursor invariants feat(mediorum): bound ops table via dormant cleanup, gap signal, and opt-in retention May 21, 2026
@RolfAris

Copy link
Copy Markdown
Contributor Author

Rollout note from live fleet readiness on 2026-05-21:

  • Pushed b596358 to gate EnsureOpsTableIndex behind OPENAUDIO_MEDIORUM_ENSURE_OPS_TABLE_INDEX=true (default off). This avoids a canary boot attempting a production-scale composite index build by default.
  • val005 is the better official canary candidate by current headroom, but still only has about 4 GiB free on /.
  • val005 ops is ~89.7 GiB total; existing ops_pkey alone is ~5.8 GiB. Building ops("table", ulid) on that disk budget is not safe without first freeing/adding space.
  • Built a local amd64 candidate tarball for the gated commit: ghcr.io/rolfaris/go-openaudio:pr304-mediorum-ops-retention-b596358-local@sha256:dd8d4507d745ab5c840bbf53da590b1457b53246b197de6f1299a33fbfab60ba; tarball SHA256 c1374930e8c82994b7af4bc2c312622708804d17246fd14b55c014828708b81c.
  • Read-only preflight on val005 is blocked only by candidate_image_not_loaded_locally; runtime/compose/external health are otherwise OK.

Recommendation: canary #304 with the index gate left off first. Only enable OPENAUDIO_MEDIORUM_ENSURE_OPS_TABLE_INDEX=true after a separate disk-headroom step or an explicit index-build window.

@RolfAris

Copy link
Copy Markdown
Contributor Author

Canary update from val005:

  • b596358 did start and pass HTTP health, but it was not safe to keep running: with the index build gated off, the default dormant cleanup still launched SELECT MAX(ulid) FROM ops WHERE "table" = $1 on val005's ~90 GB ops relation. pg_stat_activity showed it active for >5 min on IO/DataFileRead, so I rolled it back.
  • Rollback exposed that PR304 has a forward-only DB migration from the baseline runtime's perspective: v1.2.13 fails core startup with unknown migration in database after PR304 has recorded 00034_drop_redundant_core_tx_stats_tx_hash_index.sql. So post-canary rollback needs a PR304-compatible image or a DB migration cleanup plan; baseline v1.2.13 is not a clean rollback target after this migration runs.
  • I pushed 35e3045, which makes dormant cleanup opt-in as well as keeping the composite index build opt-in. val005 is now running ghcr.io/rolfaris/go-openaudio@sha256:f06edada8e4d914aa1d6b864edbc3d7822e5b2074e82e471868bde2a495835a7.
  • Verification after roll-forward: val005 check-fleet passes with http=200, live=True, storage=True; full fleet check-fleet --forks=20 completed successfully; logs show ops table index ensure disabled, dormant ops cleanup disabled, and ops retention disabled; pg_stat_activity shows no active long-running ops scan.

Implication: PR304 is not a physical disk-space reclamation fix by itself. It is now safe enough to continue canary observation with the expensive ops cleanup paths default-off, but actual filesystem space reclaim still needs a separate controlled ops physical reclaim plan.

@RolfAris

Copy link
Copy Markdown
Contributor Author

Final val005 canary/fold result from the 20-validator fleet, 2026-05-22 UTC:

  • 35e3045 stayed healthy with the expensive paths default-off: external health 200, storage healthy, version=1.3.0, no restarts, and no long-running ops scan.
  • No reclaim happened while default-off. The only observed ops index was ops_pkey; no retention/dormant cleanup env gates were enabled. A short sample showed +4,178 ops rows and +3,112,960 relation bytes over ~374.6s, roughly 11.15 rows/s and 0.72 GB/day in that window.
  • I folded val005 back to official openaudio/go-openaudio:v1.3.0@sha256:af19d4fdeb135ccec3aae6fbcac9496f02308064a3099ed843adab9318d5ec34 before reclaim.
  • The final physical reclaim used the operator-side guarded cursor-seeded reset, not PR304 retention. Fresh keyset proof showed val001 was not the right reference because uploads differed; val009 matched val005 exactly for uploads, audio_previews, and qm_audio_analyses, so the reset used val009 and failed closed on that match.
  • val005 disk moved from /dev/sda1 used=202,032,758,784B available=4,850,745,344B use=98% to used=104,683,884,544B available=102,199,619,584B use=51% immediately after reset. Removing unused PR304 and old v1.2.13 images moved it to used=101,878,546,432B available=105,004,957,696B use=50%.
  • ops was 97,349,885,952B before the reset, 16,384B after truncate inside the transaction, and 64MB in the post-reset fleet probe after new rows resumed.
  • Final verification: image drift clean on 20/20, full check-fleet.yml --forks=20 green, targeted val005 checks green after Docker cleanup, manual controller health wrapper green, cron-health wrapper green.

Operator implication: this PR is a useful safety/observability building block when the expensive paths are default-off, but it is not disk relief by itself. For production operators, the next upstream bar is either proven bounded growth or actual physical reclaim semantics. DELETE-only retention is not enough unless paired with a story that returns filesystem space; partition detach/drop, a safe table swap, or snapshot/bootstrap plus partitioned retention are the shapes that would close the operator problem.

@raymondjacobson

Copy link
Copy Markdown
Contributor

Thanks for this @RolfAris — there's real correctness work here, and I want to be clear up front that we're not closing this. The gap-signal + cursor-floor design is the right long-term direction and we want to come back to it.

For the immediate situation, though, we're going to ship #325 first as a more targeted fix, with default pruning enabled. Reasoning / sequencing:

The fire is disk pressure on live StoreAll nodes (one is at ~122 GB of ops, ~90% disk). The bloat is in uploads and qm_audio_analyses, which are written constantly. As written, this PR doesn't drain that out of the box:

  • CleanupDormantOps keys on MAX(ulid) per table, so it only drops ops for entirely dormant tables. Actively-written tables like uploads/qm_audio_analyses never qualify, even with dormant cleanup enabled.
  • The mechanism that does trim live bloat — the per-table retention sweep — is opt-in via OPENAUDIO_MEDIORUM_OPS_RETENTION_DAYS, default off.

So out of the box this PR is correctness-additive but doesn't reduce the footprint until an operator sets the retention flag. #325 is on-by-default at a 90-day window and prunes the actively-written tables directly, which is what we need to stop the bleeding now. It's ~130 LoC, easy to review and revert.

Where your PR is genuinely better, and why we want it later: #325 handles a still-active peer that's >90 days behind (or a fresh node bootstrapping) by documentation + config, not by protocol — it can let such a peer silently skip ops. Your retention-gap signal (X-Mediorum-Retention-Gap / Available-Min-Ulid) plus slowest-active-peer cursor-floor gating is the principled fix for exactly that, and the qm_fix_truncated cursor-row floor-pinning edge is a real sharp edge, not gold-plating.

Proposed sequencing:

  1. Ship feat(mediorum): prune the append-only crudr ops log #325 now (default-on, targeted, drains the live bloat).
  2. Revisit this PR to layer the gap-signal + cursor-floor on top as the protocol-level safety upgrade. At that point the dormant-cleanup component can probably be dropped (it doesn't address the live bloat) and the opt-in retention sweep reconciled with feat(mediorum): prune the append-only crudr ops log #325's pruner so we don't run two systems.

One small thing for the record: the PR description says dormant cleanup is opt-out/on-boot, but the code gates it behind ENABLE_DORMANT_OPS_CLEANUP (default off) — worth aligning the description when we pick this back up.

Tracking PR: #325.

…-gap signal

OpenAudio#325 prunes the crudr ops log purely by age, which can let a still-active
peer behind the retention window (or one bootstrapping from genesis)
silently skip the pruned range. Add the protocol safety OpenAudio#325 deferred:

- pruneOps caps its cutoff at min(time_cutoff, slowest_active_peer_cursor),
  so it never deletes ops a still-active peer has not yet swept. Ignores
  self, decommissioned peers, and non-peer cursor rows (e.g. qm_fix_truncated)
  that would otherwise pin the floor.
- ServeCrudSweep advertises the lowest available ULID when a peer sweeps
  from below it (X-Mediorum-Retention-Gap / X-Mediorum-Available-Min-Ulid),
  so the peer advances across the gap explicitly instead of silently
  skipping it, with a first-contact guard and implausible-ULID rejection.

Supersedes the original dormant-cleanup + opt-in retention sweep, which
OpenAudio#325 made redundant.
@RolfAris RolfAris force-pushed the feat/mediorum-ops-retention-minimal branch from 35e3045 to 7ef282b Compare June 11, 2026 03:42
@RolfAris RolfAris changed the title feat(mediorum): bound ops table via dormant cleanup, gap signal, and opt-in retention feat(mediorum): cap ops pruning at the active-peer cursor + retention-gap signal Jun 11, 2026
@RolfAris

Copy link
Copy Markdown
Contributor Author

Reshaped per your sequencing above. Dropped the one-time dormant cleanup and the opt-in retention sweep (both redundant once #325 prunes on-by-default), and layered the two pieces you wanted directly onto #325's pruneOps:

  • slowest-active-peer cursor floor capping the prune cutoff (with the qm_fix_truncated non-peer-row filter so a shared cursor row can't pin it), and
  • the retention-gap signal on the sweep endpoint so a peer below the floor advances explicitly instead of silently skipping.

~2,200 → ~120 LOC over four files + a focused floor test. The dormant-cleanup doc/code mismatch you flagged is gone with the component. Build/vet/go test ./pkg/mediorum/crudr green on current main.

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.

2 participants