compact: add all-packs reclaim gate and tiny-pack merging#9892
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #9892 +/- ##
==========================================
- Coverage 85.32% 85.24% -0.09%
==========================================
Files 93 93
Lines 15671 15898 +227
Branches 2387 2429 +42
==========================================
+ Hits 13372 13552 +180
- Misses 1598 1638 +40
- Partials 701 708 +7 ☔ View full report in Codecov by Harness. |
|
Guess the test failure is because |
|
will look into this , once #9877 is done and merged will need to rebase, as the complete flag and other related changes exist in |
Any compaction rewrites the whole chunk index, invalidating every client's cached copy, so only compact when the reclaimable space crosses a repo-wide threshold. Also merge small, fully-used packs once enough of them accumulate, since incremental backups otherwise leave one tiny pack behind per run with no way to consolidate them.
The all-packs reclaim gate can make compact_packs() a no-op when there is nothing worth reclaiming, so the chunk index rewrite (and the store write that used to trip borgstore's permission check) never happens. A read-only repo then silently succeeds instead of being refused. Add Repository.assert_writable(), a borg-level check of the namespace permission dict computed in __init__, and call it once up front in do_compact() before any analysis work, independent of what the gate decides.
10 tiny packs merging into a still-tiny pack would just get re-merged later, invalidating every client's index each time for no lasting gain. Trigger the merge only once the tiny packs total a full pack instead.
merge_packs batched individual indexed objects, so bytes no index entry covered (crashed writes, superseded duplicates) were silently dropped on every merge. Copy whole pack files instead, and raise before touching the store if an index entry overlaps another or extends past the pack's end.
802ae94 to
8cca880
Compare
Review by Claude Fable 5OverviewThree changes to
The overall design is sound and the crash-safety story is right: chunk indexes are deleted before the first store change, new packs are stored and indexed before sources are deleted, single-pack batches that defrag to the same content-hash name are correctly excluded from deletion, and whole-file copying preserves unindexed bytes (consistent with the #9868 policy). The index-write skip is safe: Issues, most important first1. 2. The "merged packs are never tiny again" invariant breaks when 3. 4. 5. Merge is uninterruptible and reports as one progress step. Smaller points
VerdictGood direction and careful crash-safety work; the repository-level tests (unindexed bytes carried forward, past-EOF detection, already-gone pack) mirror 🤖 Generated with Claude Code |
…efrag in merge_packs
…ruptible Cap the tiny-pack limit at half the max pack size so a merged full pack is never a merge candidate again. Refuse compaction up front on a restricted repo.
|
all addressed:
|
Follow-up review by Claude Fable 5Re-reviewed at head 1aa33c0. All five issues from the previous review are properly fixed, and the fixes are correct. Verification of each:
The smaller points were also addressed: docs (compact epilog section on both gates, updated CI: all test jobs pass (a few VM jobs still pending), Remaining nits, none blocking: the outer compact progress bar counts the whole merge as one step while Suggestion: generic
|
Problem
Any compaction rewrites the whole chunk index and invalidates every client's cached copy (borg2: compact always invalidates the whole index cache of clients #9817). The old policy decided per pack ("rewrite when unused bytes reach the threshold"), with no floor on whether the space reclaimed across the whole repository justified that cost. A single small wasteful pack could trigger a full index rewrite for very little gain.
Incremental backups leave one tiny, fully-used pack per run behind. Those packs have no unused bytes, so the per-pack policy never touched them and repositories accumulated thousands of small packs with no way to consolidate them.
Solution
All-packs reclaim gate
Drop or rewrite packs only when the space they would free reaches
--thresholddivided by 5 percent (2% at the default 10% threshold) of the total pack bytes. Below that floor the repository and the chunk index are left untouched, so client caches stay valid when the savings do not justify the cost.--threshold 0disables the gate.Tiny-pack merging
Collect fully-used packs smaller than the tiny limit, which is
min(MIN_PACK_SIZE, pack_max_size // 2)(MIN_PACK_SIZE is 1 MB). Merge them only once their combined size reaches one full pack (pack_max_size), so every merge produces at least one full-size pack that can never be a merge candidate again. Capping the limit at half the max pack size keeps that property even whenBORG_PACK_MAX_SIZEis set below 2 MB.The merge copies whole pack files through
store.defrag(content-addressed sha256 names), so bytes that no index entry covers (a chunk copy superseded by a later put, or objects from a backup that crashed before writing its index) are carried into the merged pack rather than dropped. Before writing anything, each source pack's index entries are checked for overlap or for claiming bytes past the pack's actual end; either means a corrupt index, and the merge raisesIntegrityErrorand leaves the store untouched.Packs are merged one batch at a time. Each batch's source packs are deleted only after its merged pack is stored and indexed, so a crash or Ctrl-C between batches never destroys the only stored copy of an object, and the store holds at most one batch of extra packs at a time. The merge reports progress per batch and stops cleanly at a batch boundary on Ctrl-C; the packs not yet merged are merged on the next run.
Index rewrite skip
Skip the full
write_chunkindex_to_repowhen the store did not change, so an unconditional index rewrite cannot defeat the all-packs gate.Refuse up front on a restricted repository
borg compactnow checks that the repository permissions allow write and delete onpacks/andindex/before doing any work, and raises a clear error instead of silently reaching a no-op or failing partway. A dry run only reads and reports, so it is still allowed on a read-only repository.Testing
New and updated tests:
test_compact_packs_merges_tiny_packs: tiny packs merge into fewer, larger packs, every object still reads back, and a secondborg compactfinds nothing to merge (guards against re-merging a pack it just produced).test_compact_packs_below_merge_size_gate_leaves_tiny_packs: merging does not fire on pack count alone, only once the combined size could fill a full pack.test_compact_packs_below_all_packs_gate_changes_nothing: a tiny unused pack beside a large used one is left untouched when the reclaim is below the gate.test_merge_packs_combines_whole_files,test_merge_packs_carries_unindexed_bytes_forward,test_merge_packs_detects_past_eof,test_merge_packs_skips_pack_already_gone: cover the merge path, the unindexed-bytes carry-forward, corrupt-index refusal, and a stale index entry pointing at a pack file already gone.test_assert_writable: covers the write and delete permission checks onpacks/andindex/.borg compactrefuses up front under no-delete, read-only, and write-only, and that a dry run is still allowed where reads are permitted.Closes #9816