🐛 fix(storage): honor reclaim-guard expiry when admitting references - #2025
Merged
Conversation
A reclaim guard carries a 300-second lease. Arming honors it, so a lapsed row is available to the next collector, but reference admission read no clock and rejected a commit whenever a row was present. A confirmed orphan purge that died between deleting the bytes and disarming its guard left a row nothing ever clears, and every later publication of that digest failed with BlobReclaiming for as long as the store lived. The admission check now takes the store's clock and blocks only while the lease holds, dropping the lapsed row in the same write transaction it admits past so an abandoned guard cannot outlive its lease. MetaStore gained the clock as an injectable field rather than reading wall time at the call site, which is what lets a test step across a lease boundary instead of waiting one out. The release pass in the purge no longer asks the blob store whether the bytes are present. An expired lease already proves its collector let go, and requiring absence was exactly what kept a failed delete's guard alive forever. Dropping the presence probe also removes the digest parse and the head request that only existed to answer it. Guarding the guard table's existence keeps the check from creating it, so publishing a blob reference on a fresh store no longer materializes a distributed domain table.
Merging this PR will not alter performance
Comparing Footnotes
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A reclaim guard carries a 300-second lease.
compare_and_arm_reclaim_guardshonors it, so a lapsed row is available to the next collector, but the reference-admission check incommit_driver_txn_with_journalread no clock and rejected a commit whenever a row was present. 🕒 An operator runningperyx cache purge orphaned-blobs --yesarms a guard over each candidate, deletes its bytes, then disarms. Kill that loop, or let oneblobs.blocking().deletereturn an error, and the row survives with a timestamp that lapses five minutes later. Nothing clears it.compare_and_disarm_reclaim_guardis the only removal path in the tree, and the release pass ahead of the purge disarmed an expired guard whenblobs.headreported the bytes gone, which no longer holds after a failed delete or after a client re-uploads the same content.What an operator sees depends on which path reaches the digest. A PyPI upload (
POST /{index}/) and a promote (PUT .../promote) both surfaceMetaErrorthroughCacheError::Meta, whichcache_error_statusmaps to500, so the client getsmetadata store error: blob <sha256> is being reclaimed; publish the reference again once its deletion finisheswhile no deletion is in flight; the operation ledger row stays pending, so every retry re-drives the same write and fails again. The finalize sweep logs at debug and leaves the intent pending, retrying on every tick forever.peryx importprints arejectedline per file. There is no expiry, no sweep and no admin endpoint that ends it, so the digest is unpublishable for the life of the store. One condition narrows the blast radius. The check short-circuits on an empty journal, PyPI writes a journal entry only under replication, and the purge refuses to run outsideAvailabilityMode::None. Reaching the stuck state takes a purge interrupted on a node that later serves in a distributed mode.The admission check now takes the store's clock and blocks only while
!ReclaimGuard::is_expired_at(now), matching the arming side, and drops the lapsed row in the same write transaction that admits past it so an abandoned guard cannot outlive its lease.MetaStoreholds the clock as an injectableperyx_core::Clockfield defaulting to wall time, so a test can step the clock across a lease boundary instead of waiting one out. The release pass inpurge_orphaned_blobsno longer probes the blob store. An expired lease proves its collector let go, and demanding absence was the condition that kept a failed delete's guard alive. That also retires the digest parse and theheadrequest that existed only to answer it. Reading the guard table now checks for its existence first, because opening a table inside a redb write transaction creates it, so publishing a blob reference on a fresh store no longer materializes a distributed domain table.Two items in the issue's required-change list are out of scope here. Distinguishing a collector guard from a claim-backed one belongs to #1383, and
mainhas no claim-backed guard to distinguish, sincecompare_and_arm_reclaim_guardshas one caller, the orphan purge. Reviewers of #2017 should expect a conflict incheck_blob_reclaim_guardand incrates/peryx-ha-distributed/tests/unit/reclaim_guard_tests.rs; this branch sits onupstream/mainand merges in either order.Closes #1573