check: use one chunk index for the checker and the repository, refs #10364 - #10368
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #10368 +/- ##
==========================================
+ Coverage 88.15% 88.17% +0.02%
==========================================
Files 103 103
Lines 18879 18890 +11
Branches 2917 2919 +2
==========================================
+ Hits 16642 16656 +14
+ Misses 1554 1551 -3
Partials 683 683 ☔ View full report in Codecov by Harness. |
|
Reviewed on the PR base What I verifiedThe memory claim, measured. Build counts alone can't show this (they're mostly the same as master), so I counted non-empty
Peak is one index everywhere, including the repair-with-changed-packs case that master had at three. "The one-liner alone is not enough". With only Tests. The two new tests pass locally (local + remote kinds); Reasoning I checked and agree with. Nothing in the checker reads Comments
Out of scope, noted for later
🤖 Review drafted with Claude Code |
|
ping @mr-raj12 |
4b6d54c to
4920b1c
Compare
|
Rebased and done: 1, 4, 6 (28 tests); 2: a check without |
|
Second review, on head What I verified
Findings
🤖 Review drafted with Claude Code |
|
Fixed here: 1 (index marked invalid before the first delete, cleared in finish()), 2 (clear_new() after every checker build), 3 (flush() before the index is reused or rebuilt), 4, 5, 6 (weakref), 9. |
|
Round 3 reviewed on head One new problem, then smaller things.
🤖 Review drafted with Claude Code |
…orgbackup#10364 The repository uses the index ArchiveChecker builds instead of loading or building a second one.
A check without --repair uses the index the repository check loaded. Tighten the comments.
Mark the index invalid before verify_data() deletes, flush before using the index, never store the checker's index at close().
27e5985 to
a1151bc
Compare
|
Round 4 is in a1151bc: items 1, 2, 4, 5 done as discussed; item 3 took your parametrization option, so |
Repository.delete() writes the chunk index invalid marker before rewriting a pack, finish() writes it before rebuilding the index, so an abort leaves no stale index. Tighten comments and docs, fix the vacuous verify-data abort test, add marker tests.
a1151bc to
9c05196
Compare
|
Amended the round 4 commit message only (no code change), head is now 9c05196. |
|
Round 4 reviewed on head Moving the marker into
Item 3 of the last round (non-repair check on a marker-set repo stores nothing) is fine as pinned by the new 🤖 Review drafted with Claude Code |
compact_pack writes the invalid marker just before deleting the old pack; tighten marker docs and tests.
|
Round 5 (c331fa2): compact_pack takes before_change, called just before the old pack is deleted, since defrag checks permissions and reads everything before writing; new test covers overlap, truncated pack and no-delete. Marker docs are generic now, the TODO reads TODO(#10378), and the #10378 rebase will drop both attributes. |
|
Round 5 reviewed on head Verified: a
🤖 Review drafted with Claude Code |
delete() checks permissions before any store change; compact_pack's hook is renamed before_old_pack_delete and runs before index entries are dropped. Tighten marker docs.
b24acbb to
4d9af76
Compare
|
Round 6 (4d9af76): delete() calls assert_writable() first, so it refuses before any store change on any backend; the no-delete case now expects Repository.PermissionDenied. |
Description
borg checkholds the chunk index in memory twice. Refs #10364, same change as #10359 does forborg compact.Where the second index comes from:
Repository.check()loads the index from theindex/fragments and keeps it asrepository.chunks.ArchiveChecker.check()then builds its own index while that one is still in memory.--archives-only:ArchiveChecker.check()builds its index, then the firstrepository.get()builds a second one through theRepository.chunksproperty.--repairwith changed packs:finish()rebuilds the index from the packs while the checker's index is still in memory.Changes in
ArchiveChecker:check(): callsrepository.flush()first. Rebuilding the index drops the entries of chunks still buffered in the pack writer, and a reused index can not resolve them inget().check()without--repair: uses the indexRepository.check()loaded, if there is one. That index is loaded from the sameindex/fragments the checker would load.check()otherwise: callsrepository.invalidate_chunk_index()before building the index, then setsrepository.chunks = self.chunks.get(),put()anddelete()of the repository use this index from then on.check()after its own build: callsself.chunks.clear_new(). A rebuild from the packs setsF_NEW(entry not stored in theindex/fragments yet) on every entry, andRepository.close()stores allF_NEWentries as a new fragment. So a check without--repairnever stores the index, also when the fragments are missing or corrupt, and a--repairthat stops beforefinish()does not store it either.add_callback(): compresses and encrypts a re-chunked item metadata chunk only when--repairstores it.add_reference(): with--repair, only callsrepository.put(), which adds the entry to the index and sets its pack location. Callingself.chunks.add()beforeput()on the same index fails theassert v.size == 0 or v.size == sizeinChunkIndex.add(). Without--repair, it does not change the index: entries without a pack location (F_PENDING) in the repository's index makeRepository.close()fail withAssertionError: chunk ... has no pack location yet.verify_data()with--repair: removesdel self.chunks[defect_chunk].repository.delete()already removes the entry from the index, so thedelraisesKeyError.verify_data()with--repair: marks the index invalid before the firstrepository.delete().delete()rewrites the pack, and theindex/fragments point its other chunks at the old pack untilfinish()stores the new index. If the check stops beforefinish(), the next use rebuilds the index from the packs.check(): no longer setsrepository.chunkindex_validate/chunkindex_drop_corrupt_tail. The lazyRepository.chunksbuild is not reached during a check.finish()with changed packs: callsrepository.invalidate_chunk_index()and setsself.chunks = Nonebefore rebuilding the index from the packs.finish(): clears the invalid marker after storing the index.Repository: thechunkssetter comment lists check next to compact as a caller that installs the index it built, and theinvalidate_chunk_index()docstring describes freeing the index before a rebuild.Tests:
test_check_holds_a_single_chunk_index, for--archives-only, full check and--repair, on a repo with a missing item metadata chunk. It asserts which index buildsArchiveCheckerdoes, thatrepository.chunksis not loaded at each of them and that no index the checker built before is still alive (weakref), thatrepository.chunksis the checker's index inrebuild_archives(), and that theRepository.chunksproperty builds no index. All three cases fail on master.test_check_without_repair_leaves_the_chunk_index_alone: a check without--repairon the same kind of repo does not change theindex/fragments or the chunk ids in the index, with and withoutindex/fragments before the check.test_check_with_buffered_chunks:ArchiveChecker.check()after aput(), with and without--repair.test_check_repair_verify_data_aborted_marks_the_index_invalid: a--repair --verify-datathat stops after the delete leaves the marker set, and every other chunk stays readable.test_check_repair_clears_the_invalid_marker:--repairon a repo with the marker set and noindex/fragments clears the marker.repository.chunks = self.chunksadded to master, 28 tests incheck_cmd_test.pyfail, most of them inRepository.close()with theF_PENDINGAssertionErrorabove, among themtest_check_formaton an intact repo and the existingtest_verify_data,test_missing_file_chunkandtest_missing_archive_item_chunk.Full test suite: 3082 passed, 1142 skipped.
Checklist
master(or maintenance branch if only applicable there)toxor the relevant test subset)