Repository navigation
check: survive I/O errors while reading repository objects - #10194
ThomasWaldmann wants to merge 4 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10194 +/- ##
==========================================
- Coverage 89.30% 89.30% -0.01%
==========================================
Files 104 104
Lines 20027 20125 +98
Branches 3144 3157 +13
==========================================
+ Hits 17886 17973 +87
- Misses 1482 1488 +6
- Partials 659 664 +5 ☔ View full report in Codecov by Harness. |
|
Two follow-ups pushed:
One scope note I should have put in the description from the start: this catches |
28b4ed7 to
f3e45ca
Compare
bb67b77 to
309d89b
Compare
…gbackup#3509 An OSError from the store (failing disk, flaky network filesystem, ...) escaped up to the top-level handler, so borg check died with a "Local Exception" traceback and the results collected so far were lost. Reads of repository objects now raise Repository.StoreReadError, keeping the original OSError as __cause__. It is deliberately distinct from ObjectNotFound and IntegrityError: borg never saw the content, so it can tell neither that the object is missing nor that it is corrupt. The repository check reports such an object, goes on and fails at the end, so one run lists everything that is affected. No result is recorded for it, so a later check verifies it again rather than remembering it as corrupt. The archives check reports the affected archive and continues with the next one. Because the data may well be readable again once the underlying problem is fixed, nothing acts on an unreadable object: --repair refuses to repair, the index is not rebuilt from packs that could not be read, and --verify-data leaves those chunks in place instead of deleting them.
Listing the archives reads each archive's metadata chunk, so an unreadable pack holding one made the archives check die before its per-archive loop even started. The FreeBSD CI job hit this because the pack it picked happened to hold archive metadata. Archives.list() gets an opt-in tolerate_read_errors: with it, an archive whose metadata could not be read becomes a placeholder entry, so the check can report it and go on with the other archives. Only the archives check opts in. Everywhere else the read error propagates, because acting on a partially readable repository is how a transient I/O error turns into data loss: prune would see the placeholder's 1970 timestamp and treat the archive as the oldest one. The --find-lost-archives scan now also skips an unreadable chunk instead of aborting; it only looks for archive metadata there, so skipping one can at worst miss a lost archive. The verify-data test now targets the pack holding file content, so it tests what it means to test regardless of how chunks land in packs.
…backup#3509 Instead of one try around the whole per-archive body, catch the read error at the two places that can raise it: reading the archive metadata block, and reading the item metadata stream. Both are precise about what failed, neither can swallow a read error from somewhere unexpected, and the body stays where it was, so the diff is readable. Also add tests for the branches the first two commits left untested: an unreadable namespace listing, an unreadable index object, --repair stopping at an unreadable pack while rebuilding the index, --repair stopping at unreadable archive metadata (with the archive left intact), --find-lost-archives skipping an unreadable chunk, and the one that matters most - a defect chunk whose re-read fails is kept rather than deleted, which is how a flaky disk would otherwise talk borg into throwing data away. The failure injection is now one helper taking a per-read predicate and an "after N reads" counter, so a test can fail one object, one range, one namespace, or only the second read of something.
…gbackup#3509 _load_config() carefully tells "the store has no repository config" (a lost config, something that never was a borg 2 repository, or an interrupted repo-create) from "the config is there". A read failure is neither, but it escaped as a raw OSError, so borg died with a traceback on it. It now raises StoreReadError. This replaces the get_manifest() wrapping of the previous revision of this commit: repository version 5 folded the manifest object into config/config, so reading that object is now the place where an I/O error must not be mistaken for "missing". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
309d89b to
18f415b
Compare
Fixes #3509.
An
OSErrorfrom the store (failing disk, flaky network filesystem, ...) escaped up to the top-level handler, soborg checkdied with a "Local Exception" traceback and the results collected so far were lost. The issue is from the segment-file days, but master has the same hole in the new code paths:Reads of repository objects now raise
Repository.StoreReadError(rc 35), keeping the originalOSErroras__cause__. It is deliberately distinct fromObjectNotFoundandIntegrityError: borg never saw the content, so it can tell neither that the object is missing nor that it is corrupt. This covers all read paths: whole-pack loads, range reads, batched reads (gather_many, used for archive metadata and item metadata streams), namespace listings and the repository config.Following the three points from the issue:
OSErroritself stays as__cause__.--repairrefuses to repair (newRepository.RepairUnsafe, rc 36), neither the chunk index is rebuilt nor are corrupt packs salvaged while some objects could not be read, and--verify-dataleaves those chunks in place instead of deleting them. Previously--verify-data --repaircould only reach the delete path via anIntegrityError, so this is about keeping the two apart as the code grows.Commands other than
borg checkstop with the read error instead of working with a partially readable repository: e.g. prune must not see a placeholder for an archive whose metadata could not be read. Onlyborg checklists such an archive with a placeholder entry, so it can go on with the other archives.Same repository as above, with the fix:
(The archive's name is stored in its unreadable metadata, so the check can only show the placeholder and the archive id.)
A listing that fails (
store.list()on an unreadable directory) is still fatal — we do not know what to check then — but it ends the check with that one-line message instead of a traceback. Likewise, an unreadable repository config ends a command with that message, instead of looking like a missing repository.Fifteen regression tests cover it: the basics (reported and the check continues, no tracker record written,
--repairrefuses), each read path (packs, index objects, namespace listings, archive metadata, the archives listing, the repository config), and the cases that would otherwise lose data - a defect chunk whose re-read fails is kept,--repairdoes not salvage corrupt packs while another pack is unreadable, and--repairstops at unreadable archive metadata and leaves the archive intact. They patch the posixfs backend to raiseEIOrather than using file permissions, so they also work when the tests run as root.