feat(nds): garbage collect commit directories older than a given registry commit - #1207
Merged
Conversation
|
Durable storage space metrics for revision c663de3, at pull request
Full reportThese figures are for catching a regression before it merges. Compare them with |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1207 +/- ##
==========================================
+ Coverage 90.44% 90.54% +0.09%
==========================================
Files 153 155 +2
Lines 32995 33427 +432
Branches 32995 33427 +432
==========================================
+ Hits 29843 30266 +423
+ Misses 2152 2150 -2
- Partials 1000 1011 +11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
emturner
commented
Sep 8, 2026
emturner
force-pushed
the
emturner@nds-gc-w1-journal
branch
from
September 8, 2026 14:35
42e1c7f to
67cf2fb
Compare
emturner
marked this pull request as ready for review
September 8, 2026 14:37
emturner
force-pushed
the
emturner@nds-gc-w1-journal
branch
from
September 8, 2026 14:37
67cf2fb to
24a0a20
Compare
emturner
enabled auto-merge
September 8, 2026 14:37
|
Benchmark results for revision c663de3:
Full results
Compare the results above with those for the default branch. |
emturner
force-pushed
the
emturner@nds-gc-w1-journal
branch
2 times, most recently
from
September 9, 2026 12:57
ee4d100 to
f02b302
Compare
Collection needs to know which commits are newer than the one it is asked to collect at. That cannot be read off the commits themselves: a commit id is the registry root hash, so it says what a state contains and nothing about when it was reached. A repository now records each committed root in a journal beside the manifests, as a sequence number that increases with every commit. Retention follows the recorded order rather than ancestry. Collecting at a root therefore keeps every root recorded at or after it, and a root committed more than once keeps its highest position, so re-committing an old state extends how long it survives. The target itself is read the other way about: its earliest position is the floor, so collecting at a state committed more than once drops only what preceded the first time it was reached, never the states committed in between. Deletion/collection follows in subsequent commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collecting at a root drops the registry commits recorded before it, together with the database commits none of the surviving registry commits still reference. Database commits are content-addressed and therefore shared - unrelated registry commits name the same one whenever their states agree - so removal follows reachability rather than the order commits were made in. This reclaims the value side of a commit. A commit directory is a set of hard links, so unlinking the last directory referencing a file frees it, and values are keyed by user key, so overwrites become obsolete versions compaction discards. Merkle nodes are content-addressed and every version of a node is a distinct live key, so no amount of directory removal reaches them; that needs deletion of the node keys themselves and is the other half of the design. Collection is safe to interrupt and repeat. It prunes the journal to the retained roots, removes the manifests it dropped, then removes the unreachable database commits, in that order, so everything retained is intact at each point in between. Each step after the first enumerates what is present rather than what the journal says should be, so a round left unfinished is picked up by the next one. Pruning first is also what makes a stale target safe: a root an earlier round already collected past is no longer in the journal, so collecting at it is refused rather than taken as a floor that would retain commits whose data has gone. Collection must not overlap a commit or another collection, and that is the caller's responsibility for now. The operations sit on the repository trait, so the in-memory backend collects the same way the on-disk one does, and the space harness now drives the real implementation rather than a copy of it - the reclaim figures it reports measure what ships. The reachability logic the long test carried its own copy of is now the one function. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fmv6auJua8dSB18iGy6rES
The registry long test kept its own copy of collection: a reachability walk over the retained manifests, then directory and manifest removals, run over both backends. It has been a copy of the mechanism rather than a test of it, so the retention a run exercises has never been the retention that ships. Drive `collect()` instead. Epoch bases are the only commits a run records, so the window over them translates directly: keep the `keep` most recent and collect at the oldest of those, which retains it and everything recorded after it. This is also the only coverage the in-memory repository has for its side of collection. The `collect` tests reach it through `DirectoryManager`, and the crate's two test flavours are the same build in this respect - the self dev-dependency enables `rocksdb` whether or not default features are on - so `InMemoryRepo`'s journal and removal methods were never being run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
emturner
force-pushed
the
emturner@nds-gc-w1-journal
branch
from
September 9, 2026 12:59
f02b302 to
279fe69
Compare
victor-dumitrescu
approved these changes
Sep 9, 2026
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.
Part of TZX-209
What
Record the order of commits made, in an append only journal. Then allow for commits to be collected that are older than a given root (and not referenced since). Finally, bound the registry long test with that collection, in place of the copy of it the test kept.
Why
octez-smart-rollup-nodeneeds the storage to reclaim disk, and today it reclaimsnothing: a repository grows with the total number of commits over its lifetime.
Currently, a commit id is just the registry root hash, so contains no information about when the commit occurred. Two unrelated commits could share the same id, and ancestry is unavailable.
Simply removing directories removes a large proportion of what a repository holds. As commit directories are sets of hard links, removing the last directory containing a given hard link frees it. This is sufficient for garbage collection of the 'key-value' column family.
How
Commit Journal
Each committed root is recorded as a sequence number that increases with
every commit. Identity is untouched, so committing the same state twice still produces
the same id and the same manifest; only the journal grows. Retention follows the recorded
order rather than ancestry, which keeps states whose provenance is unclear. Collecting at
a root keeps every root recorded at or after it, and a root committed more than once
keeps its highest position, so re-committing an old state can only extend how long it
survives. The target is read the other way about: its earliest position is the floor,
so collecting at a state that has been committed twice drops only what preceded the first
time it was reached, and never the states committed in between. Both directions err
towards keeping a state.
Collection
Database commits are content-addressed and therefore shared — unrelated
registry commits name the same one whenever their states agree — so their removal follows
reachability rather than the order commits were made in.
It is safe to interrupt and repeat. It prunes the journal to the retained roots, removes
the manifests it dropped, then removes the unreachable database commits, in that order, so
everything retained is intact at each point in between. Each step after the first
enumerates what is present rather than what the journal says should be, so a round left
unfinished is picked up by the next one.
Both sit on the repository trait, so the in-memory backend records and collects the same
way the on-disk one does.
For now, collection must not overlap a commit or another collection; that is the caller's
responsibility for now, and is stated in the docs page.
Long Test Retention
The registry long test advances a shared base once per epoch, then runs a proptest of many
cases against that base. A case checks the base out and applies operations with proofs; it
never commits. Only the epoch advance does. So the only commits a run records are epoch
bases, in the order the epochs ran.
Bounding the repository over a long run therefore means dropping old epoch bases, and the
test used to do that with its own copy of collection: a window of recent bases, and on
eviction a reachability walk over the retained manifests, then database directory and
manifest removals, mirrored by hand onto both backends. That is the mechanism this PR
ships, written a second time, so the retention a run exercised was never quite the
retention that ships — and the two could drift apart without anything noticing.
Now the window is all the test keeps. It trims to the
keepmost recent bases and callscollect()at the oldest one still in the window, once per backend. Retention by recordedorder coincides with a window over epoch bases exactly because nothing between them is ever
recorded, so "every root recorded at or after this one" and "the bases still in the window"
are the same set.
Three things follow from the swap. A long randomised run now exercises the real
collect(), including the journal pruning the copy never touched. The bound is tighter,because the copy only ever considered the database commits of the base it was evicting,
leaving anything else unreachable on disk. And it is the only place the in-memory
repository's side of collection runs at all — see the note under Manually Testing.
Ordering is safe: collection must not overlap a commit, and the call sits between epochs,
after the proptest for that epoch has finished, so nothing else is touching the repository.
Manually Testing
The space harness now drives the real
collect()rather than a copy of it, so thereclaim figures it reports measure what ships:
So does the registry long test, on both backends:
That run is also the only coverage the in-memory repository has for its side of collection.
The
collecttests reach the trait throughDirectoryManager, and the crate's two testflavours are the same build in this respect — the self dev-dependency takes default
features, and
default = ["rocksdb"], so--no-default-featuresdoes not turn rocksdb offfor the test binary — which left nothing running
InMemoryRepo's journal and removalmethods.
Regressions
None. Nothing is deleted by the first commit, and the second only removes commit
directories and manifests that are no longer retained.
Tasks for the Author