Skip to content

Commit 96ead8a

Browse files
Merge pull request #10379 from mr-raj12/check-finish-narrow-walk-8466
check --repair: re-read only the packs the repair wrote, refs #8466
2 parents c27f341 + 4d970f4 commit 96ead8a

8 files changed

Lines changed: 562 additions & 84 deletions

File tree

‎docs/internals/data-structures.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ cache/
8686
a marker object: while it is present, the chunks index in ``index/`` is considered
8787
invalid, because its fragments may be missing entries or point at deleted packs.
8888
It is written before deleting index fragments, before a single-object delete removes
89-
the old pack, and before ``borg check --repair`` rebuilds the index after changing
90-
the packs. It is removed after the last fragment is deleted or once the complete
91-
current index is stored.
89+
the old pack, and by ``borg check --repair`` after storing packs, before it re-reads
90+
them and stores the index. It is removed after the last fragment is deleted or once
91+
the complete current index is stored.
9292

9393
Note that this ``cache/`` namespace is inside the repository (and thus shared by
9494
all clients); it is not the client-local cache described in

‎docs/internals/packs.rst‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,12 @@ A deletion that could drop entries -- dropping the index entirely, or the full r
323323
above -- is guarded by a marker object, ``cache/chunkindex-invalid``, written before
324324
the first deletion and removed after the last one. A single-object delete writes the
325325
marker just before it removes the old pack, and ``borg check --repair`` writes it
326-
before rebuilding the index after changing the packs; both remove it once the index
327-
is stored. While the marker is present, the fragments may be missing entries or point
328-
at deleted packs, so they are not merged; the index is rebuilt from the pack files on
329-
the next load instead. A consolidation needs no marker: the entries of the small
330-
fragments it deletes are already contained in the merged fragments it wrote before
331-
deleting them.
326+
after storing packs, before it re-reads them and stores the index; both remove it
327+
once the index is stored. While the marker is present, the fragments may be missing
328+
entries or point at deleted packs, so they are not merged; the index is rebuilt from
329+
the pack files on the next load instead. A consolidation needs no marker: the entries
330+
of the small fragments it deletes are already contained in the merged fragments it
331+
wrote before deleting them.
332332

333333
If the entire ``index/`` namespace is lost or corrupt, the ChunkIndex can be rebuilt
334334
by scanning pack files directly; see :ref:`pack-recovery`.

‎src/borg/archive.py‎

Lines changed: 141 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
from .patterns import PathPrefixPattern, FnmatchPattern, IECommand
5454
from .item import Item, ArchiveItem, ItemDiff
5555
from .platform import acl_get, acl_set, set_flags, get_flags, set_times, swidth
56-
from .repository import Repository
56+
from .hashindex import ChunkIndex, ChunkIndexEntry
57+
from .repository import Repository, PackReader
5758
from .repoobj import RepoObj, object_validator
5859

5960
# macOS: SF_DATALESS marks dataless placeholder files (e.g. cloud files not materialized locally).
@@ -2236,9 +2237,27 @@ class ArchiveChecker:
22362237
def __init__(self):
22372238
self.error_found = False
22382239
self.key = None
2239-
# True once repair drops a defect chunk or writes a new one, i.e. once the chunks index no
2240-
# longer matches the packs.
2240+
# True once repair changed the packs: it stored a chunk or deleted a defect chunk.
22412241
self.chunks_modified = False
2242+
# ids of the packs repair wrote: stored by put() and flush(), or written by delete() rewriting a pack.
2243+
self.written_packs = set()
2244+
2245+
def record_stored(self, results):
2246+
"""Add the pack ids in results to written_packs.
2247+
2248+
results: (chunk_id, pack_id, obj_offset, obj_size) tuples of the objects in the stored packs, as
2249+
Repository.put() and flush() return them, or None.
2250+
"""
2251+
if results:
2252+
self.written_packs.update(pack_id for _, pack_id, _, _ in results)
2253+
2254+
def create_archive_entry(self, name, id, ts):
2255+
"""Store the pack writer buffer, record the packs it wrote, create the archives directory entry.
2256+
2257+
Archives.create() stores the pack writer buffer too, but does not return the packs it wrote.
2258+
"""
2259+
self.record_stored(self.repository.flush())
2260+
self.manifest.archives.create(name, id, ts)
22422261

22432262
def note_dropped_objects(self):
22442263
# The chunk index rebuild skipped repository content to get past a corrupt object header.
@@ -2295,7 +2314,8 @@ def check(
22952314
self.repo_objs = RepoObj(self.key)
22962315
validate = object_validator(self.repo_objs)
22972316
# store the chunks buffered in the pack writer, so the index below has their pack locations
2298-
# (pack id, offset and size in the pack).
2317+
# (pack id, offset and size in the pack). The result is ignored: written_packs holds only the packs
2318+
# the repair writes.
22992319
self.repository.flush()
23002320
if not repair and self.repository.is_chunk_index_loaded:
23012321
# without --repair, use the loaded index.
@@ -2423,9 +2443,14 @@ def verify_data(self):
24232443
# failed twice -> remove this defect chunk. delete rewrites its pack without it,
24242444
# keeping the other chunks, and removes it from self.chunks, so rebuild_archives
24252445
# reports the file it belongs to. update_index=False: finish() stores the index
2426-
# rebuilt from the packs and clears the invalid marker delete() writes.
2427-
self.repository.delete(defect_chunk, update_index=False, validate=validate)
2446+
# and clears the invalid marker delete() writes.
2447+
old_pack_id = self.chunks[defect_chunk].pack_id
2448+
# new_pack_id: the pack holding the other objects of the old pack, None if there were none.
2449+
new_pack_id, _ = self.repository.delete(defect_chunk, update_index=False, validate=validate)
24282450
self.chunks_modified = True
2451+
self.written_packs.discard(old_pack_id) # delete() removed the old pack
2452+
if new_pack_id is not None:
2453+
self.written_packs.add(new_pack_id)
24292454
else:
24302455
logger.warning("chunk %s not deleted, did not consistently fail.", bin_to_hex(defect_chunk))
24312456
else:
@@ -2513,7 +2538,7 @@ def valid_archive(obj):
25132538
self.error_found = True
25142539
if self.repair:
25152540
logger.warning(f"Creating archives directory entry for {name} {archive_id_hex}.")
2516-
self.manifest.archives.create(name, archive_id, archive.time)
2541+
self.create_archive_entry(name, archive_id, archive.time)
25172542
else:
25182543
logger.warning(f"Would create archives directory entry for {name} {archive_id_hex}.")
25192544

@@ -2569,7 +2594,7 @@ def add_reference(id_, size, cdata):
25692594
# with --repair, store a chunk the repository does not have; put() adds it to self.chunks.
25702595
if self.repair and id_ not in self.chunks:
25712596
assert cdata is not None
2572-
self.repository.put(id_, cdata)
2597+
self.record_stored(self.repository.put(id_, cdata))
25732598
self.chunks_modified = True
25742599

25752600
def verify_file_chunks(archive_name, item):
@@ -2782,51 +2807,125 @@ def valid_item(obj):
27822807
logger.debug(f"archive id new: {bin_to_hex(new_archive_id)}")
27832808
cdata = self.repo_objs.format(new_archive_id, {}, data, ro_type=ROBJ_ARCHIVE_META)
27842809
add_reference(new_archive_id, len(data), cdata)
2785-
self.manifest.archives.create(info.name, new_archive_id, info.ts)
2810+
self.create_archive_entry(info.name, new_archive_id, info.ts)
27862811
if archive_id != new_archive_id:
27872812
self.manifest.archives.delete_by_id(archive_id)
27882813
finally:
27892814
pi.finish()
27902815
report_missing_chunks()
27912816

2792-
def finish(self):
2793-
if self.repair:
2794-
# flush chunks re-added during repair so their packs are on the store and out of the pack
2795-
# writer buffer (close() requires an empty buffer, #10055) before we (re)build the index.
2796-
self.repository.flush()
2797-
if self.chunks_modified:
2798-
# the packs changed: rebuild the index from them and store it. The index/ fragments lack
2799-
# the chunks this repair stored, so the index is invalid until the rebuilt one is stored.
2800-
# Free the current index first, so only one index is in memory.
2801-
write_chunkindex_invalid(self.repository)
2802-
self.repository.invalidate_chunk_index()
2803-
self.chunks = None
2804-
# Runs to completion, also after a Ctrl-C: delete_chunkindex_invalid() below declares
2805-
# the stored index to match the packs, which holds only once every pack was indexed.
2806-
if sig_int:
2807-
logger.warning(
2808-
"Rebuilding and writing the repository chunks index. "
2809-
"This reads every pack and can not be interrupted."
2817+
def verify_written_packs(self):
2818+
"""Read the object headers of the packs in written_packs and make the chunks index match them.
2819+
2820+
put() and delete() compute the index entries of the packs they write from the data they write.
2821+
This compares the (chunk_id, obj_offset, obj_size) of each object header in a written pack, read
2822+
with a validator, with the index entries that name the pack. Each difference sets error_found, is
2823+
logged and is fixed in the index:
2824+
2825+
- an index entry names an object the pack does not hold: the entry is removed.
2826+
- the pack holds an object whose chunk id is not indexed: the object is indexed.
2827+
- the pack does not exist: its index entries are removed.
2828+
2829+
A superseded duplicate is an object whose chunk id is indexed at another location. It is logged at
2830+
debug level. A pack delete() wrote holds one if compact_pack copied a byte range that no index entry
2831+
covers.
2832+
"""
2833+
pack_ids = sorted(self.written_packs)
2834+
if not pack_ids:
2835+
return
2836+
logger.info(f"Re-reading {len(pack_ids)} pack(s) written by the repair.")
2837+
# (chunk_id, obj_offset, obj_size) of the index entries, per written pack.
2838+
indexed = {pack_id: set() for pack_id in pack_ids}
2839+
for chunk_id, entry in self.chunks.iteritems():
2840+
entries = indexed.get(entry.pack_id)
2841+
if entries is not None:
2842+
entries.add((chunk_id, entry.obj_offset, entry.obj_size))
2843+
validate = object_validator(self.repo_objs)
2844+
# pass 1 removes the index entries of every pack before pass 2 indexes any object, so whether an object
2845+
# is unindexed does not depend on the order the packs are read in.
2846+
found_in = {} # pack_id -> (chunk_id, obj_offset, obj_size) of the objects in the pack
2847+
not_found_in = {} # pack_id -> sorted index entries naming an object the pack does not hold
2848+
for pack_id in pack_ids:
2849+
# PackReader reads from the store, which does not refresh the repository lock.
2850+
self.repository._lock_refresh()
2851+
expected = indexed[pack_id]
2852+
key = "packs/" + bin_to_hex(pack_id)
2853+
info = self.repository.store.info(key)
2854+
if not info.exists:
2855+
self.error_found = True
2856+
logger.error(
2857+
f"pack {bin_to_hex(pack_id)}: written by the repair, but it is missing. Removing its index entries."
2858+
)
2859+
for chunk_id, _, _ in expected:
2860+
del self.chunks[chunk_id]
2861+
continue
2862+
reader = PackReader(self.repository.store, pack_id, pack_size=info.size)
2863+
found = list(reader.iter_headers(validate=validate, on_drop=self.note_dropped_objects))
2864+
not_found = sorted(expected.difference(found))
2865+
for chunk_id, _, _ in not_found:
2866+
del self.chunks[chunk_id]
2867+
found_in[pack_id] = found
2868+
not_found_in[pack_id] = not_found
2869+
# pass 2 indexes each unindexed object, so of several unindexed copies of a chunk, the first in pack id and
2870+
# offset order is indexed and the others are superseded duplicates.
2871+
for pack_id, found in found_in.items():
2872+
pack_hex = bin_to_hex(pack_id)
2873+
expected = indexed[pack_id]
2874+
not_found = not_found_in[pack_id]
2875+
unindexed = []
2876+
for obj in found:
2877+
chunk_id, obj_offset, obj_size = obj
2878+
if obj in expected:
2879+
continue
2880+
if chunk_id in self.chunks:
2881+
logger.debug(
2882+
f"pack {pack_hex}: {bin_to_hex(chunk_id)} at offset {obj_offset}, {obj_size} bytes: "
2883+
"superseded duplicate"
28102884
)
2811-
else:
2812-
logger.info("Rebuilding and writing the repository chunks index.")
2813-
build_chunkindex_from_repo(
2814-
self.repository,
2815-
slow_rebuild=True,
2816-
validate=object_validator(self.repo_objs),
2817-
on_drop=self.note_dropped_objects,
2818-
write_immediately=True,
2885+
continue
2886+
unindexed.append(obj)
2887+
# size=0: the object header does not hold the plaintext size.
2888+
self.chunks[chunk_id] = ChunkIndexEntry(
2889+
flags=ChunkIndex.F_USED, size=0, pack_id=pack_id, obj_offset=obj_offset, obj_size=obj_size
28192890
)
2820-
else:
2821-
# the packs are unchanged, so the index still matches them: persist it as is.
2822-
logger.info("Writing the rebuilt repository chunks index.")
2823-
write_chunkindex_to_repo(
2824-
self.repository, self.chunks, incremental=False, clear=False, force_write=True, delete_other=True
2891+
if not (not_found or unindexed):
2892+
continue
2893+
self.error_found = True
2894+
# an object the pack holds whose index entry has a wrong offset or size counts in both numbers,
2895+
# unless pass 2 indexed another copy of it first.
2896+
logger.error(
2897+
f"pack {pack_hex}: the chunks index does not match the pack. Indexed objects not in the pack: "
2898+
f"{len(not_found)}, objects in the pack with an unindexed chunk id: {len(unindexed)}. "
2899+
"Fixed the index."
2900+
)
2901+
for chunk_id, obj_offset, obj_size in not_found:
2902+
logger.debug(
2903+
f"pack {pack_hex}: {bin_to_hex(chunk_id)} at offset {obj_offset}, {obj_size} bytes: not in pack"
2904+
)
2905+
for chunk_id, obj_offset, obj_size in unindexed:
2906+
logger.debug(
2907+
f"pack {pack_hex}: {bin_to_hex(chunk_id)} at offset {obj_offset}, {obj_size} bytes: not indexed"
28252908
)
2909+
2910+
def finish(self):
2911+
if self.repair:
2912+
# store the pack writer buffer before the index is written (close() requires an empty buffer, #10055).
2913+
self.record_stored(self.repository.flush())
2914+
if self.chunks_modified or self.written_packs:
2915+
# the index/ fragments lack the pack changes of this repair.
2916+
write_chunkindex_invalid(self.repository)
2917+
# Runs to completion, also after a Ctrl-C: delete_chunkindex_invalid() below declares the
2918+
# stored index to match the packs, which holds only once every written pack was re-read.
2919+
self.verify_written_packs()
2920+
logger.info("Writing the repository chunks index.")
2921+
write_chunkindex_to_repo(
2922+
self.repository, self.chunks, incremental=False, clear=False, force_write=True, delete_other=True
2923+
)
2924+
# close() persists the in-memory index: drop it, the stored one is current.
2925+
self.repository.invalidate_chunk_index()
2926+
self.chunks = None
28262927
# the stored index matches the packs: clear the invalid marker.
28272928
delete_chunkindex_invalid(self.repository)
2828-
# drop the in-memory index so close() does not persist it over the index just written.
2829-
self.repository.invalidate_chunk_index()
28302929

28312930

28322931
class ArchiveRecreater:

‎src/borg/archiver/check_cmd.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,9 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser):
222222
``borg check`` rebuilds the chunk index from the packs when ``--repair`` is given or when
223223
the stored index cannot be used. Ctrl-C stops that rebuild after the current object and
224224
discards the partial index: it lacks chunks that are still in the repository, so the check
225-
would report them as lost. After a ``--repair`` that stored or deleted chunks, borg rebuilds and
226-
stores the chunk index once more; that rebuild always runs to completion, also after a
227-
Ctrl-C, and reads every pack.
225+
would report them as lost. After a ``--repair`` that stored or deleted chunks, borg re-reads the
226+
packs the repair wrote, makes the chunk index match them and stores it; that always runs to
227+
completion, also after a Ctrl-C.
228228
229229
About repair mode
230230
+++++++++++++++++

‎src/borg/cache.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ def write_chunkindex_invalid(repository):
609609
"""Store the invalid marker, cache/chunkindex-invalid.
610610
611611
Store it before deleting index/ fragments whose entries no other fragment holds, before deleting a pack
612-
the fragments point at, and before rebuilding the index after pack changes the fragments do not record.
612+
the fragments point at, and after storing packs the fragments do not record.
613613
While it is present, build_chunkindex_from_repo rebuilds the index from the packs instead of merging the
614614
fragments.
615615
"""

0 commit comments

Comments
 (0)