Repository navigation
compact: reclaim superseded duplicates left after a full check --repair, fixes #10471 - #10476
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #10476 +/- ##
==========================================
+ Coverage 88.80% 88.91% +0.11%
==========================================
Files 103 103
Lines 19515 19589 +74
Branches 3050 3061 +11
==========================================
+ Hits 17330 17418 +88
+ Misses 1516 1505 -11
+ Partials 669 666 -3 ☔ View full report in Codecov by Harness. |
There was a problem hiding this comment.
Reviewed at 399dea2. The approach looks right: counting the gaps in pass 1 lets both --threshold and the drop decision see them, and merge_packs still copies whole files, so gaps survive a merge. The PR's tests pass locally. Two problems reproduced with tests, plus some smaller points.
1. Data loss when the indexed copy is in a truncated pack (blocking)
untrusted_pack_ids is stale_packs | corrupt_packs. A pack whose index entries claim more bytes than the file holds is logged as an error and skipped in the fate loop, but is not untrusted. So a gap copy whose indexed copy lives in that pack still counts as a superseded duplicate.
Repro: pack A holds only copies of X and Y. Pack B indexes X and Y but is truncated to 100 bytes in the store. compact logs index claims more data than the file holds, run "borg check", then Deleting 0 unused objects and 2 superseded duplicates, and deletes pack A, which held the only intact copies of X and Y.
def test_compact_keeps_duplicates_indexed_in_truncated_pack(tmp_path):
location = os.fspath(tmp_path / "repo")
with Repository(location, exclusive=True, create=True, key_loader=make_test_key) as repository:
manifest = gc_manifest(repository)
repo_objs = manifest.repo_objs
datas = [os.urandom(1000), os.urandom(1000)]
_, pack_a = put_pack(repository, repo_objs, datas)
ids, pack_b = put_pack(repository, repo_objs, [b"new"] + datas)
key_b = "packs/" + bin_to_hex(pack_b)
repository.store.store(key_b, repository.store.load(key_b)[:100]) # X and Y are gone from B
mark_used(repository, set(ids))
gc = ArchiveGarbageCollector(repository, manifest, stats=False, threshold=10)
gc.chunks = repository.chunks
gc.compact_packs()
assert pack_a in pack_sizes(repository) # fails: pack A was droppedcompact_pack already had this gap before, but there it only mattered when a pack was rewritten for its unused indexed bytes. Now a pack holding nothing but such copies is deleted outright. Suggested fix: determine the packs failing the indexed > total check (and those failing check_pack_objects) before the gap walk, and add them to untrusted_pack_ids for both pass 1 and compact_pack, the same reasoning as #10474.
2. Orphan packs are walked on every run
A pack without any index entries (e.g. left by a crashed borg create) is in gap_packs on every run, and the gap walk does one store range read per object. Measured: 500 range reads per run for a 500-object orphan pack; master does none, as it never looks at such a pack. On ssh/REST/S3 stores that is one request per object, on every compact (including --dry-run), until someone runs borg check --repair.
Skipping such packs is not an option, as the #10471 case is exactly an unindexed pack holding only superseded duplicates. But the walk could read a gap pack in one piece (bounded by the pack size limit), so it costs one request per pack instead of one per object.
3. Smaller points
superseded_duplicates()iterates the whole chunk index even whengap_packsis empty, the normal case. Return early on an empty set.- An overlap detected by
check_pack_objectsin any gap pack now raisesIntegrityErrorand aborts the whole compact run. Before, that only happened for a pack being rewritten. The neighbouring caseindexed > totalis handled per pack with an error log, so this is inconsistent. - The superseded count logged for rewritten packs comes from pass 1. When a dropped pack held the indexed copy, its index entry is deleted before the rewrite, so
compact_packkeeps that gap copy (safe) and the log line overstates the count. - The nothing-to-do path still logs
Deleting 0 unused objects..., not matching the new message. - A tiny pack whose superseded duplicates reach
--thresholdis now a rewrite candidate instead of a merge candidate. If the all-packs gate then cancels the rewrites, that pack is neither rewritten nor merged.
|
please rebase |
a353355 to
9f5ac68
Compare
|
Rebased on #10475 (now with 4eca5c3) and addressed in 86efd29 (the commits of this PR are squashed into it). 1. Truncated pack Pass 1 now checks the index entries of every pack with For that check, pass 1 keeps the byte range of each index entry as one int in an
2. Orphan packs
A rewrite drops the superseded duplicates pass 1 found ( The walk refreshes the repository lock before each pack ( 3. Smaller points
docs: the crash paragraph in |
f3b98cf to
86efd29
Compare
|
please rebase, so i can merge. |
…ing or corrupt pack, fixes borgbackup#10474
… a pack with a defect chunk or in a truncated pack
…ir, fixes borgbackup#10471 - pass 1 walks the gaps of the packs that have any and counts their superseded duplicates as reclaimable. A pack holding only unused indexed objects and superseded duplicates is deleted. The walk refreshes the lock per pack, shows progress and stops on Ctrl-C. - compact_pack drops the superseded duplicates found in pass 1 (superseded_ranges). - superseded_gap_ranges reads GAP_READ_SIZE bytes after a small object, so small objects share a store request, and META_READ_SIZE bytes after a large one. - compact and repo-compress check the index entries of every pack. A pack in which they overlap or reach past the end of the file is treated as untrusted. compact logs it as an error and keeps it unchanged. - a tiny pack that the all-packs gate keeps from being dropped or rewritten is a merge candidate.
86efd29 to
8211bb7
Compare
Description
A superseded duplicate is an unindexed object in a pack whose chunk id the index maps to another location (see
superseded_gap_ranges).compact_packsonly counted indexed-but-unused bytes, so a pack holding used objects and superseded duplicates, or only superseded duplicates, was never rewritten or deleted. This is the state after a crashedborg createis re-run.compact_pack(superseded_ranges=...)), so the gaps are walked once and the logged count is what is dropped.--thresholdrewrite and the all-packs gate. A pack holding only unused indexed objects and superseded duplicates is deleted.borg repo-compressuses the same check for its untrusted packs, in place of the test for a pack ending before one of its index entries.Tested with the repro from the issue without the
borg check --repairstep, which leaves the same superseded duplicates (800 MB random file,kill -9after 3s, re-run kept): packs go from 1.1G to 764M in oneborg compact, the next run logs no hint,borg checkis rc 0.Depends on #10475, whose commits this branch contains.
fixes #10471
Checklist
master(or maintenance branch if only applicable there)toxor the relevant test subset)