Repository navigation
CAS GC: delete a condemn marker only for the condemnation that owns it - #2463
Conversation
The GC delete job removed whatever `.meta` condemn marker was current for the blob. When a blob was re-published and condemned again between the job's scheduling and its run, the job deleted the marker of the LATER condemnation, and the next graduation of that blob ran without durable evidence. An in-process registry also let graduation skip the marker read after a leader restart, and a late `PUT Clean If-Match` by the publisher could recreate the marker bytes under a stale token. Rules, all in `CasGc.cpp` / `CasGcMetaWriter.cpp`: - The delete job reads the marker and deletes it with `If-Match` of the token it read, only while the marker is `Condemned` at the entry's condemn round or older. - A gate refusal re-stamps the marker with the current round (`If-Match` replace of absent, `Clean` or older-round content). - The gate accepts a marker `Condemned` at the entry's round or later. - Graduation always reads the marker; the in-process confirmation registry is removed. Reads are issued ahead through the fold read-ahead pool, bounded by `cas_gc_io_concurrency`, and a hint never changes a decision: the reservation prefix stops at a touched row, so the round graduates the same set at every concurrency. Also: `GcReadAhead::pendingHeads`, `PriorEdgeCursor::scanAhead`, `GraduationReadHints`, `kGraduationLookaheadRows`; user docs (`garbage-collection.md`, `blob-protocol.md`, `debugging.md`, `system.md`) incl. the upgrade procedure; three probabilistic backoff assertions removed from tests (`Retry::backoff` draws uniform(0, ceiling)). Model: `docs/superpowers/models/CaGcCondemnMarkerGate.tla` (cas-gc-rebuild) gains the round rules; 9 bug cfgs violate, 7 fixed cfgs are green, the 25.8M-state `pm_g1_big_noregistry` is green. Testing: release `CAS*` 2591, ASan 2594, TSan green; 12 mutation checks each caught by a named test; gc-ab stand S11: `fold_reduce` per working round 1063 -> 821 ms at `cas_gc_io_concurrency` 16 (base -> PR), 1623 -> 2234 ms at 1 (every graduation candidate now reads its marker). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com>
| { | ||
| const auto lm = loadMeta(op, layout, ref); | ||
| if (!lm) | ||
| if (!lm || lm->meta.state != MetaState::Condemned || lm->meta.condemn_round > condemn_round) |
There was a problem hiding this comment.
Seems it would be expected behavior to have orphans?
| /// A bounded spin that fails the test instead of hanging. | ||
| inline void awaitCondition(const std::function<bool()> & condition) | ||
| { | ||
| const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); |
There was a problem hiding this comment.
nit: use cv/barriers instead time for avoiding tests flaps.
CI Failures AnalysisRun 36864698169 at Compared with MasterCI on Related to this PRNone. The diff is CAS GC marker deletion ( Pre-existing (does not block this PR)
Infrastructure (does not block this PR)
Unknown (blocks until resolved)
Recommendations
|
Integration rerunBoth shards that failed on the first attempt passed on the rerun. They ran the full suites.
Nothing in this rerun is a |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…3 wall-clock flake (CAS-340), occurrences on CAS-74/252 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CAS GC delete job removed whatever
.metacondemn marker was current for a blob. A blob re-published and condemned again between the job's scheduling and its run lost the marker of the later condemnation, so its next graduation ran without durable evidence. Two related holes: the in-process confirmation registry let graduation skip the marker read after a leader restart, and a late publisherPUT Clean If-Matchcould recreate marker bytes under a stale token.The bug.
Xis a blob hash;X.metais its condemn marker — one object shared by every life ofX, and the only thing that tells a writer "do not reuse these bytes, GC is about to delete them". Single GC, no failures:sequenceDiagram participant W as Writer participant S as Store<br/>X bytes / X.meta participant G as GC W->>S: PUT X bytes G->>S: round c: X has no referrer, HEAD X present G->>S: PUT X.meta = Condemned(c) Note right of G: remembers "X confirmed" W->>S: reconcile after publish: PUT X.meta = Clean Note over S: X.meta = Clean Note left of W: transaction aborts, X unreferenced again Note over G: round c+1: BUG — graduates X from memory,<br/>never reads X.meta, misses Clean W->>S: writer 2: HEAD X present, GET X.meta = Clean Note left of W: Clean = safe to reuse: adopts X in a new part G->>S: DELETE X bytes Note over S: writer 2's part now points at deleted bytesThe race is GC's
HEAD/PUTlanding between the writer's bodyPUTand its reconcile — rare, but it needs no failure (spec §2.3, S2; the TLA+ cfgrv_bugreaches it). Three more schedules end the same way: a marker-delete job queued for an EARLIER life ofXthat carries no round and runs late, after a leader change, removing the current condemnation's marker (S1, S3); and a writer's latePUT Clean If-Matchmatching a marker GC re-created with the same bytes and so the same S3 etag (S4). One root cause: nothing on the delete or graduation path names the condemnation it acts for, and graduation trusts process memory instead of the object.After this PR
X.metaand accepts onlyCondemnedat round>= c;Clean, absent or an older round refuses and re-stamps the marker with the current round. The in-memory note is gone. Writer 2 then seesCondemnedand re-uploads instead of adopting.Condemnedat that round or older (S1, S3).PUTwas conditioned on (S4).Changes
If-Matchof the token it read, only while the marker isCondemnedat the entry's condemn round or older (deleteConfirmedMeta).Condemnedat the entry's round or later (writeCondemnedMeta,settleEntry).GraduationReadHints,PriorEdgeCursor::scanAhead,kGraduationLookaheadRows), bounded bycas_gc_io_concurrency; a hint never changes a decision — the reservation prefix stops at a touched row, so a round graduates the same set at every concurrency.garbage-collection.md,blob-protocol.md,debugging.md,monitoring.md,correctness.md,system.md(incl. the upgrade procedure).Retry::backoffdraws uniform(0, ceiling), solongestPause > 0could fail by draw.Risks / notes
fold_reduceper working round went 1063 → 821 ms atcas_gc_io_concurrency16 (default) and 1623 → 2234 ms at 1.garbage-collection.md. Accepted residuals (publisher-sideCleanwrite, lostgc/state) are listed in the specdocs/superpowers/specs/2026-09-30-cas-gc-meta-delete-names-its-condemnation-design.md§10; the writer rule that stops the publisher from turningCondemnedintoCleanis a separate PR.Testing
CASGcMetaWriter*,CASGCReadAhead*,CASBlobInDegree*,CASGcAckFloor*), releaseCAS*2591, ASan 2594, TSan green; 12 mutation checks, each caught by a named test.CaGcCondemnMarkerGate.tlawith the round rules — 9 bug cfgs violate the expected invariant, 7 fixed cfgs green, 25.8M-statepm_g1_big_noregistrygreen.Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
Fix the CAS GC deleting the
.metacondemn marker of a later condemnation of a re-published blob; graduation now always reads the marker (with read-ahead undercas_gc_io_concurrency) and the delete job removes a marker only for the round that owns it.Documentation entry for user-facing changes
docs/en/antalya/cas/architecture/garbage-collection.md,blob-protocol.md,docs/en/sql-reference/statements/system.md)CI/CD Options
Regression jobs to run: