repository: add salvage_pack, keeping only the authenticated objects of a corrupt pack, refs #10026 - #10419
repository: add salvage_pack, keeping only the authenticated objects of a corrupt pack, refs #10026#10419mr-raj12 wants to merge 3 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #10419 +/- ##
==========================================
+ Coverage 88.39% 88.41% +0.02%
==========================================
Files 103 103
Lines 19061 19163 +102
Branches 2968 2988 +20
==========================================
+ Hits 16849 16943 +94
- Misses 1540 1548 +8
Partials 672 672 ☔ View full report in Codecov by Harness. |
|
check the names, some are a bit off. |
ThomasWaldmann
left a comment
There was a problem hiding this comment.
Review of fd40682b. The 21 new tests pass locally. Items 1 and 2 should be fixed before this gets wired into check --repair; the rest is naming and polish.
1. Read errors from non-local backends are not caught as SALVAGE_READ_ERROR
salvage_pack catches only OSError. For rest:// (the ssh:// transport), sftp and s3, a read failure surfaces as borgstore's BackendError / BackendConnectionError, which are not OSError subclasses. The rest backend turns a server-side I/O error into BackendError(response.text). Nothing gets corrupted, since all reads happen before the first write, but the caller gets an exception instead of a status. Once this is wired into check --repair, one unreadable pack would abort the whole repair on a remote repo, while a local repo just skips the pack. Suggestion: also catch StoreBackendError, but let StoreObjectNotFound (a subclass of it) through, as the docstring promises.
2. chunks=None means the opposite of what it means in the sibling methods
In compact_pack and transform_pack, chunks=None means "use self.chunks". Here it means "do not update the index", and the old pack is still deleted. Every self.chunks entry for the pack is then left pointing at a deleted pack. test_salvage_pack_without_chunks_leaves_the_index_alone enshrines that. Please either follow the siblings (None -> self.chunks) or drop the None option. If a caller really needs "no index update", make it an explicit flag.
3. before_old_pack_delete is called when nothing is deleted
When the kept bytes hash to pack_id (e.g. the damage is appended bytes), store_store overwrites the old pack and the delete is skipped, but the callback is still called. compact_pack calls it only if new_pack_id != pack_id. The effect is harmless (at worst an unneeded "chunk index invalid" marker), but the name and the sibling's behaviour say otherwise. Either match compact_pack or pick a name that fits. No test covers the callback in the same-id case.
4. The authenticated_no_key refusal is in the wrong layer
repository.py now imports crypto.key to read AUTHENTICATED_NO_KEY. Apart from that, the repository layer never deals with keys. repoobj.py already has its own AUTHENTICATED_NO_KEY. Having object_authenticator() raise under the workaround would protect every future caller, keep salvage_pack key-agnostic, and remove the docstring paragraph that describes the "accepts any object" behaviour. Also, the workaround only affects the authenticated-* key classes, so refusing for every mode is broader than needed (harmless, though).
5. Names
pack_store_cacheis a bool but reads like the cache itself. Something likeuses_pack_store_cachewould be clearer.SALVAGE_UNSTABLEalso covers "the loaded bytes hash to the pack's name", which is not instability. Use a separate status, or a name that covers both cases.- validate vs. authenticate: when
object_validator(header + metadata slot) fails,iter_headersalready logs "object does not authenticate". Nowobject_authenticatoris the whole-object check. The naming should make the header+meta vs. whole-object difference clear. - Test helpers:
offsets_endreturns the position of an object's last byte, not an end offset.store_salvage_packstores a pack and damages it; it does not salvage anything.
6. Nits
- Do the
store_hash(pack_contents).digest() == pack_idcheck right after the first load. As it is, it runs after the authenticate loop (wasted work), and an intact read where nothing authenticates reportsSALVAGE_NOTHING_AUTHENTICATES. - On posixfs, the second load will usually come from the page cache, so it catches in-memory or transfer corruption rather than flaky media. The docstring could say what the second read actually guards against.
test_salvage_pack_refuses_without_write_permissiondoes not assert that the store and the index are unchanged.
What looks good
- The order of changes (store the new pack, callback, index update, delete the old pack) is crash-safe.
- The index update handles duplicate ids and
F_PENDINGentries correctly. - Adding unindexed objects as
F_USEDwith size 0 follows the precedent inarchive.py. - The
_pack_cacheentry is dropped even when the new pack gets the old name.
Generated by Claude Code
…of a corrupt pack, refs borgbackup#10026
… pack is deleted, refs borgbackup#10026
04a33b6 to
88e5b04
Compare
|
Found another nit: except (OSError, StoreBackendError) also swallows borgstore's PermissionDenied and BackendMustBeOpen. BackendMustBeOpen means a bug in the calling code, but it would now show up as SALVAGE_READ_ERROR with only a warning. Can you catch more specific exception classes? Also: please rebase. |
|
ping @mr-raj12 |
1 similar comment
|
ping @mr-raj12 |
Description
Adds
Repository.salvage_packandrepoobj.whole_object_authenticator. Not wired intocheck --repairyet, refs #10026.salvage_packreplaces a pack whose content no longer matches its store hash name by a pack holding only the objects that authenticate (tags of both slots verified), then updates the chunk index (self.chunksunlesschunksis given):F_USED, size 0)F_PENDINGentries stay as they areThe store and the index change only for
SALVAGE_DONE. Other outcomes:SALVAGE_INTACT: the store hash matches the nameSALVAGE_READS_DIFFER: the loaded bytes hash to the name although the store hash did not, or a second load differs from the firstSALVAGE_NOTHING_AUTHENTICATES: no object authenticatesSALVAGE_READ_ERROR:OSErroror a store backend error while reading; a missing pack raisesStoreObjectNotFoundAll reads happen before the first store change. The order of changes is: store the new pack, call
before_old_pack_delete, update the index, delete the old pack. If the kept bytes are the undamaged pack, the new pack has the old name, sobefore_old_pack_deleteand the delete are skipped.salvage_packraisesErrorwithBORG_STORE_CACHE(two loads may return the same cached copy).whole_object_authenticatorraisesErrorfor anauthenticated-*key withBORG_WORKAROUNDS=authenticated_no_key(tags not verified).The
checklog message for a gap object failingobject_validatoris now "object header or metadata does not authenticate".Checklist
master(or maintenance branch if only applicable there)toxor the relevant test subset)