check: keep pack check results, add --max-age to reuse them - #9925
check: keep pack check results, add --max-age to reuse them#9925mr-raj12 wants to merge 11 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #9925 +/- ##
==========================================
+ Coverage 86.14% 86.27% +0.12%
==========================================
Files 96 96
Lines 17326 17468 +142
Branches 2649 2675 +26
==========================================
+ Hits 14925 15070 +145
+ Misses 1663 1660 -3
Partials 738 738 ☔ View full report in Codecov by Harness. |
|
I think we could keep all entries except the ones for packs that do not exist any more. See also #9898. The condition for skipping a pack would then change from "it is in checked-packs AND it was OK" to "it is in checked-packs AND it was OK AND the result is recent enough". |
Implemented as a new --max-age INTERVAL option (e.g. 4w). Skip condition is now: in checked-packs AND it was OK AND the record is younger than max_age. Intact records are kept across cycles, only records of packs no longer listed in packs/ are pruned at cycle end. Corrupt records are always re-verified. |
234a018 to
7e5325a
Compare
|
CI: /home/runner/work/borg/borg/docs/misc/asciinema/README.rst: WARNING: document isn't included in any toctree [toc.not_included] I'll fix that, was a collateral damage of my asciinema updates. Update: #9938 merged, please rebase on current master. |
7e5325a to
57f2394
Compare
|
review by claude opus 5: Reviewed at The design is sound and matches what was asked for in the earlier comment: retention is unconditional, the skip condition became "recorded AND ok AND recent enough", pruning is by "no longer listed in packs/". The Two real defects, then smaller stuff. 1. Partial check reports "Corrupt packs" and "no problems found", exit code 0
if index_errors == 0: # the packs were checked, so the corrupt records are from this checkThat comment is false on the
Two coherent ways out, and I'd take the second:
Either way 2. A future timestamp makes a pack unverifiable forever
if max_age and time.time() - entry.timestamp < max_age:If One-line fix, treating a future record as stale: age = time.time() - entry.timestamp
if max_age and 0 <= age < max_age:
continueThe failure here is silent rather than noisy, which is what makes it worse than the usual clock-skew annoyances. Smaller points
The summary hides how much was skipped. With Unbounded corrupt-id log line ( Nested condition reads oddly now ( if entry is not None and entry.result and max_age and 0 <= time.time() - entry.timestamp < max_age:
continue # recorded intact recently; corrupt records are always re-verifiedStale test name. Doc wording nit, Not a PR issue: |
|
For 2. add and use corrupt-id log line: 1 id per line? |
e7676d1 to
74fe294
Compare
|
updated!, records are kept unconditionally now, nothing drops a good entry except the pack going away from packs/. |
|
review by claude opus 5 Re-reviewed at The headline:
|
|
fixed both, future-skew tolerance is now capped at min(MAX_CLOCK_SKEW, max_age), so a small window won't accept more future skew than past age (and added a test). The permanent corrupt-pack failure is intentional and now documented in the check() docstring: it clears on re-verify, compact, or repair (#8572) |
Only the intact-pack records are cycle progress; the corrupt ones are kept for repair until the pack verifies intact or is gone from packs/, and their ids are now reported in the check summary. Refs borgbackup#9696.
…ls their reuse Records are pruned only for packs no longer listed in packs/, so --max-duration now requires --max-age to make progress.
…ge, log corrupt packs one per line
…l checks, show reused-result count
…rt partial break skips corrupt pack
…istent corrupt-pack failure
Parse --max-age with the calendar-aware relative time marker (m, y measured against now), tolerate MAX_CLOCK_SKEW at both ends of the reuse window, and correct the stale --repository-only comment.
16fac1b to
ff6bb8b
Compare
|
review by claude opus 5 Reviewed at Tests: 107 passed / 2 skipped, 1. Starvation: still open
Re-confirmed on this head. One unit of work per run, One note on the evidence: the 8-pack repro from the last review now passes on this head, but only by accident. The widened window below adds two hours of slack, which breaks the exact-day boundary tie in that day-granularity simulation and lets the scan advance one slot. Move the repo one unit further past the edge and the cycle re-establishes itself, as above. The 2h of slack shifts where the cliff sits; it does not remove it. The epilog still promises "until every pack has a result younger than The suggested fix is unchanged: sort 2.
|
…ument --max-age markers
|
review by claude opus 5 Reviewed at No new defects this pass. There is one simplification the fix makes possible, which is the most interesting thing here. The fixes hold up
Run 12 of a 13-pack repo is exactly one full pass — no wasted work, and the corruption is caught and then reported on every run after. The coverage probe shows strict round-robin, one distinct pack per run with no repeats. Scoping the sort to The
|
…ng in docs The sort provides partial-check progress on its own, so --max-duration no longer requires --max-age; bare --max-duration works as documented again. Credit the least-recently-checked ordering (not --max-age) for resumption in the epilog and data-structures.rst, and rework the partial-ordering test to use intact packs so it exercises the skip path together with the sort.
Refs #9696.
cache/checked-packsmaps pack id -> (timestamp, result) and holds the results of the repository pack check. Before this branch its contents were throwaway: a completed check calledtracker.clear(), which empties the table and deletes the store object, and the next check cleared it again before starting. The records existed only to let a--max-durationcheck resume where the previous one stopped.That made two things impossible. A cheap cron check could not hand its findings to a later repair, so corrupt pack ids only ever appeared as log lines in whichever run happened to find them. And a check could not reuse a result that was still perfectly good, which matters on remote or cloud storage where hashing every pack takes hours.
This branch keeps the records instead and lets the user decide, at check time, how old a result may be.
Retention. Records are kept in all cases.
tracker.clear()at the end of a check becomestracker.prune(pack_ids): drop the records whose pack id is no longer listed inpacks/, then store the rest. Pruning runs after the pack listing has been scanned, on both the completed and the interrupted path.Reuse. The new
borg check --max-age INTERVALskips a pack when its recorded result is intact and younger than INTERVAL. Without--max-age(the default) every pack is verified, and the results are still recorded. Records of corrupt packs are always re-verified regardless of age, so a repair never acts on a stale failure.--max-agecannot be combined with--repairor--archives-only.Partial checks. Progress comes from the record timestamps: a partial check verifies the least-recently-checked packs first, so repeated
--max-durationruns cover the whole repository whether or not--max-ageis set. Bare--max-durationkeeps working as documented today, so there is no breaking change and no sign-off to make.--max-ageis a pure optimization on top: with--max-duration=3600 --max-age=1w, a daily one hour check skips packs whose result is younger than a week, until the whole repository has been covered once per week.The summary names the corrupt packs, guarded on
index_errors == 0so the ids only appear when the pack check actually ran.No format change and no version bump. Old blobs load unchanged, and an older borg reading a newer blob sees
result=0entries it re-verifies anyway. A single store object was preferred over a separatecache/corrupt-packs, becauseresultalready encodes the distinction and two files can drift out of sync where one cannot.docs/internals/data-structures.rstdescribes the retention rule and how--max-ageand--max-durationinteract.Tests in
repository_test.pyandcheck_cmd_test.pycover: intact and corrupt records surviving a completed check; a plain check recording results that a later--max-agecheck reuses; a check without--max-ageverifying every pack while keeping the records; a carried-over corrupt record being re-verified and replaced once the pack is intact; selective pruning of records whose pack is no longer listed; corrupt records surviving across consecutive partial checks; a partial check verifying the least-recently-checked packs first while skipping packs younger than--max-age; the summary naming the corrupt ids; and the argument rules for--max-age(rejected with--repairand--archives-only) and--max-duration(requires--repository-only). black and ruff are clean.Not in here: machine-readable output,
--repairconsuming the recorded list, clearing records after a successful repair. Those depend on howborg check --repairand a separate repair command end up being split, which is still open.