Conversation
Nothing on the request path frees bytes. A manifest delete retracts edges and a repository delete releases the name, but whether one repository was a layer's last user is a question about the whole store. A background pass answers it, on the repository sweeper's shape: an interval tick, a bounded resumable step per key range, and no state anywhere but the store. Five stages, in an order where each releases work for the next: untagged manifests (opt-in), stale memberships, blobs, abandoned uploads, and empty names. Blobs need a clock. "Is this blob referenced" is one seek, but "does any repository still hold it" is not askable in this schema - `P` is keyed <repo> <digest>, so one blob's memberships are scattered across the range. A mount makes the gap visible: it writes `P` and no `R`, so a layer mounted a moment ago looks exactly like one nothing has wanted for a year. The new `C <digest>` -> BlobMark records when the pass first saw a blob with no `R` edge. The mark has to stand for a whole grace period before the bytes go, and every path that creates a reference or a membership retracts it in the batch it was already writing - which is a blind delete precisely because it is its own key rather than a field on BlobRecord. Metadata commits first and the bytes are removed after, the mirror of the write path: an `L` with no file is a pull that fails, while a file with no `L` is inert. The one lock is a digest's, striped 256 ways, held from a blob's rename to its metadata commit and by the pass over its decision to reclaim, which is the only window where those two can interleave. Archive manifest copies are not reclaimed: they carry no `L` by design and this pass walks `L`. Collecting them needs an orphan-file scrub, which does not exist yet, so the docs say that rather than implying some later pass picks them up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E1BhkZmass74aoBLJcHAcW
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.
Implements #2.
Five stages, in an order where each releases work for the next: untagged manifests (opt-in), stale
Pmemberships, blobs, abandoned uploads, empty names. Every stage is a bounded resumable step over one key range, with no state anywhere but the store, so a pass killed half way through is not lost work — it is work the next pass does.The blob stage, and the one new key
"Is this blob referenced" is one seek over
R <digest>. "Does any repository still hold it" is not askable at all:Pis keyed<repo> <digest>, so a blob's memberships are scattered across the range in repository order. Nor can the stage be driven by watching memberships disappear —finish_repo_sweepdrops a dead repository's wholePrange with oneDeletePrefixand never enumerates it.So the stage walks
L, and the gap it has to cover is the mount:commit_blobaddsPand noR, so a blob mounted a moment ago looks exactly like one nothing has wanted for a year.C <digest>— purge's mark — closes it. Written the first time a blob is seen unreferenced; it has to stand for a whole grace period before the bytes go; and every path that creates a reference or a membership retracts it in the batch it was already writing. Retracting a membership retracts the mark too, and that rule is what makes a push safe without a lock: a manifest may only name a blob its repository holds, so a push that plans successfully saw either anRedge (which stops the collection outright) or a liveP, whose existence and whose removal both reset the clock. A push has milliseconds between plan and apply; the collection is a grace period away.A new prefix rather than a field on
BlobRecord, so there is noSCHEMA_VERSIONbump and no migration — an existing store simply has no marks and acquires them on the first pass.LandCstay out of the prefix-extractor domain, so the bloom filters are unchanged.The one lock
BlobLocks: 256 mutexes striped by digest, on the pattern of the tag locks from 0e18360. Held from a blob's rename to its metadata commit, and by purge over its decision to reclaim — the only window where those two interleave. The state it prevents is anLrecord naming a file that is gone, which is the failure this registry treats as corruption rather than as garbage. Taken after the body is written and never around it, so it costs an fsync and a batch rather than a layer transfer.Empty names need no lock: ids are handed out in order and never reused, so the pass keeps the id counter as it stood at the start of the previous pass and retires only names below it. A name interned during a pass is never retired by it, which closes the intern-then-write gap without touching the upload path. The first pass after a restart therefore retires nothing.
Retirement is narrower than "no manifests", too. A repository that ever had a tag has
H/Jevents, and history outliving what it describes is a promise the README makes — so a name goes only whenM,P,H,JandAare all empty beneath it and no upload holds it open.Untagged manifests
Off by default, gated on
--purge-untagged, because pulling by digest is ordinary and a digest-pinned deployment names manifests no tag does. Even on: an index's children, a referrer whose subject still exists, and anything pushed inside--purge-untagged-min-ageare kept. The cosign leakcosign.rsdocuments closes here — deleting a subject makes its signature reclaimable through the ordinary path, which is what the synthesisedFedges were for.Surfaces
--purge-interval1h,--purge-grace24h,--upload-ttl24h,--no-purgestops the schedule and not the endpointPOST /api/v1/purgeruns a pass and answers with what it did;?dry-run=truecounts and writes nothing;GETreturns the last passNo offline
summ purgesubcommand: RocksDB holds the store to one process, so a one-shot run would mean stopping the registry to clean it.Known limit, stated in the module docs
The archived manifest copies are not reclaimed. They carry no metadata by design, so the pass never touches one — and never takes one either, after its manifest is deleted. Finding those means walking
blobs/and askingMabout every file, which is the orphan-file scrub: its own opt-in pass, not this one.Tests
summ-registry/tests/purge.rs— ten, over the passes and every guard, including the mark-clearing rule that keeps a push safe.summ-server/tests/purge.rs— seven over the real stack, which is the only place "did the bytes actually leave the disk" can be asked.summ-server/tests/concurrency.rsS8 — a pass running flat out against commits and pulls of the same blobs: a few hundred collections per run with no torn read, no5xxand no deadlock between the tag and digest locks. Its doc says what it does not establish: the windowBlobLockscloses is a hundred microseconds wide, and removing the lock does not reliably fail this test.cargo fmt,cargo clippy --workspace --all-targets -D warningsandcargo test --workspaceare clean. Verified by hand against a runningsumm serve: push, delete, sweep, two passes, bytes gone from the disk and the counts onGET /api/v1/purge.🤖 Generated with Claude Code
https://claude.ai/code/session_01139cg98Xh2bzaqd6xB92aR