|
53 | 53 | from .patterns import PathPrefixPattern, FnmatchPattern, IECommand |
54 | 54 | from .item import Item, ArchiveItem, ItemDiff |
55 | 55 | 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 |
57 | 58 | from .repoobj import RepoObj, object_validator |
58 | 59 |
|
59 | 60 | # macOS: SF_DATALESS marks dataless placeholder files (e.g. cloud files not materialized locally). |
@@ -2212,9 +2213,24 @@ class ArchiveChecker: |
2212 | 2213 | def __init__(self): |
2213 | 2214 | self.error_found = False |
2214 | 2215 | self.key = None |
2215 | | - # True once repair drops a defect chunk or writes a new one, i.e. once the chunks index no |
2216 | | - # longer matches the packs. |
| 2216 | + # True once repair wrote a pack: it stored a chunk or deleted a defect chunk. |
2217 | 2217 | self.chunks_modified = False |
| 2218 | + # ids of the existing packs repair stored (put(), flush()) or wrote by rewriting a pack (delete()). |
| 2219 | + self.written_packs = set() |
| 2220 | + |
| 2221 | + def record_stored(self, results): |
| 2222 | + """Add the pack ids in results to written_packs. |
| 2223 | +
|
| 2224 | + results: the (chunk_id, pack_id, obj_offset, obj_size) tuples Repository.put() or .flush() returns |
| 2225 | + for the packs it stored, or None if it stored no pack. |
| 2226 | + """ |
| 2227 | + if results: |
| 2228 | + self.written_packs.update(pack_id for _, pack_id, _, _ in results) |
| 2229 | + |
| 2230 | + def create_archive_entry(self, name, id, ts): |
| 2231 | + """Store the pack writer buffer, record the packs it wrote, create the archives directory entry.""" |
| 2232 | + self.record_stored(self.repository.flush()) |
| 2233 | + self.manifest.archives.create(name, id, ts) |
2218 | 2234 |
|
2219 | 2235 | def note_dropped_objects(self): |
2220 | 2236 | # The chunk index rebuild skipped repository content to get past a corrupt object header. |
@@ -2273,6 +2289,7 @@ def check( |
2273 | 2289 | validate = object_validator(self.repo_objs) |
2274 | 2290 | else: |
2275 | 2291 | validate = None |
| 2292 | + assert not repair or validate is not None # a repair validates every object it indexes |
2276 | 2293 | # store the chunks buffered in the pack writer, so the index below has their pack locations |
2277 | 2294 | # (pack id, offset and size in the pack). |
2278 | 2295 | self.repository.flush() |
@@ -2405,9 +2422,14 @@ def verify_data(self): |
2405 | 2422 | # failed twice -> remove this defect chunk. delete rewrites its pack without it, |
2406 | 2423 | # keeping the other chunks, and removes it from self.chunks, so rebuild_archives |
2407 | 2424 | # reports the file it belongs to. update_index=False: finish() stores the index |
2408 | | - # rebuilt from the packs and clears the invalid marker delete() writes. |
2409 | | - self.repository.delete(defect_chunk, update_index=False, validate=validate) |
| 2425 | + # and clears the invalid marker delete() writes. |
| 2426 | + # new_pack_id holds the other objects of the old pack, None if there were none. |
| 2427 | + old_pack_id = self.chunks[defect_chunk].pack_id |
| 2428 | + new_pack_id, _ = self.repository.delete(defect_chunk, update_index=False, validate=validate) |
2410 | 2429 | self.chunks_modified = True |
| 2430 | + self.written_packs.discard(old_pack_id) |
| 2431 | + if new_pack_id is not None: |
| 2432 | + self.written_packs.add(new_pack_id) |
2411 | 2433 | else: |
2412 | 2434 | logger.warning("chunk %s not deleted, did not consistently fail.", bin_to_hex(defect_chunk)) |
2413 | 2435 | else: |
@@ -2495,7 +2517,7 @@ def valid_archive(obj): |
2495 | 2517 | self.error_found = True |
2496 | 2518 | if self.repair: |
2497 | 2519 | logger.warning(f"Creating archives directory entry for {name} {archive_id_hex}.") |
2498 | | - self.manifest.archives.create(name, archive_id, archive.time) |
| 2520 | + self.create_archive_entry(name, archive_id, archive.time) |
2499 | 2521 | else: |
2500 | 2522 | logger.warning(f"Would create archives directory entry for {name} {archive_id_hex}.") |
2501 | 2523 |
|
@@ -2551,7 +2573,7 @@ def add_reference(id_, size, cdata): |
2551 | 2573 | # with --repair, store a chunk the repository does not have; put() adds it to self.chunks. |
2552 | 2574 | if self.repair and id_ not in self.chunks: |
2553 | 2575 | assert cdata is not None |
2554 | | - self.repository.put(id_, cdata) |
| 2576 | + self.record_stored(self.repository.put(id_, cdata)) |
2555 | 2577 | self.chunks_modified = True |
2556 | 2578 |
|
2557 | 2579 | def verify_file_chunks(archive_name, item): |
@@ -2761,43 +2783,108 @@ def valid_item(obj): |
2761 | 2783 | logger.debug(f"archive id new: {bin_to_hex(new_archive_id)}") |
2762 | 2784 | cdata = self.repo_objs.format(new_archive_id, {}, data, ro_type=ROBJ_ARCHIVE_META) |
2763 | 2785 | add_reference(new_archive_id, len(data), cdata) |
2764 | | - self.manifest.archives.create(info.name, new_archive_id, info.ts) |
| 2786 | + self.create_archive_entry(info.name, new_archive_id, info.ts) |
2765 | 2787 | if archive_id != new_archive_id: |
2766 | 2788 | self.manifest.archives.delete_by_id(archive_id) |
2767 | 2789 | finally: |
2768 | 2790 | pi.finish() |
2769 | 2791 | report_missing_chunks() |
2770 | 2792 |
|
| 2793 | + def verify_written_packs(self): |
| 2794 | + """Read the object headers of the packs in written_packs and make the chunks index match them. |
| 2795 | +
|
| 2796 | + put() and delete() compute the index entries of the packs they write without reading the packs. |
| 2797 | + This compares the (chunk_id, obj_offset, obj_size) of each object header in a written pack, read |
| 2798 | + with a validator, with the index entries that name the pack. Each difference is a check finding, |
| 2799 | + logged and fixed in the index: |
| 2800 | +
|
| 2801 | + - an index entry names an object the pack does not hold: the entry is removed. |
| 2802 | + - the pack holds an object whose chunk id is not indexed: the object is indexed. |
| 2803 | + - the pack does not exist: its index entries are removed. |
| 2804 | +
|
| 2805 | + An object whose chunk id is indexed at another location is a superseded duplicate, not a finding: a |
| 2806 | + pack delete() wrote can hold one, in a byte range compact_pack copied with no index entry covering it. |
| 2807 | + """ |
| 2808 | + pack_ids = sorted(self.written_packs) |
| 2809 | + if not pack_ids: |
| 2810 | + return |
| 2811 | + logger.info(f"Re-reading the packs written by the repair: {len(pack_ids)}.") |
| 2812 | + # (chunk_id, obj_offset, obj_size) of the index entries, per written pack. |
| 2813 | + indexed = {pack_id: set() for pack_id in pack_ids} |
| 2814 | + for chunk_id, entry in self.chunks.iteritems(): |
| 2815 | + entries = indexed.get(entry.pack_id) |
| 2816 | + if entries is not None: |
| 2817 | + entries.add((chunk_id, entry.obj_offset, entry.obj_size)) |
| 2818 | + validate = object_validator(self.repo_objs) |
| 2819 | + for pack_id in pack_ids: |
| 2820 | + # PackReader reads from the store, which does not refresh the repository lock. |
| 2821 | + self.repository._lock_refresh() |
| 2822 | + pack_hex = bin_to_hex(pack_id) |
| 2823 | + expected = indexed.pop(pack_id) |
| 2824 | + reader = PackReader(self.repository.store, pack_id) |
| 2825 | + # iter_headers() yields nothing for a missing pack: the store reports size 0 for it. |
| 2826 | + if not self.repository.store.info(reader.key).exists: |
| 2827 | + self.error_found = True |
| 2828 | + logger.error(f"pack {pack_hex}: written by the repair, but it is missing. Removing its index entries.") |
| 2829 | + for chunk_id, _, _ in expected: |
| 2830 | + del self.chunks[chunk_id] |
| 2831 | + continue |
| 2832 | + found = list(reader.iter_headers(validate=validate, on_drop=self.note_dropped_objects)) |
| 2833 | + not_found = sorted(expected.difference(found)) |
| 2834 | + for chunk_id, _, _ in not_found: |
| 2835 | + del self.chunks[chunk_id] |
| 2836 | + # the loop indexes each unindexed object, so of several unindexed copies of a chunk, the first |
| 2837 | + # is indexed and the others are superseded duplicates. |
| 2838 | + unindexed = [] |
| 2839 | + for obj in found: |
| 2840 | + chunk_id, obj_offset, obj_size = obj |
| 2841 | + if obj in expected: |
| 2842 | + continue |
| 2843 | + if chunk_id in self.chunks: |
| 2844 | + logger.debug( |
| 2845 | + f"pack {pack_hex}: {bin_to_hex(chunk_id)} at offset {obj_offset}, {obj_size} bytes: " |
| 2846 | + "superseded duplicate" |
| 2847 | + ) |
| 2848 | + continue |
| 2849 | + unindexed.append(obj) |
| 2850 | + # size=0: the object header does not hold the plaintext size. |
| 2851 | + self.chunks[chunk_id] = ChunkIndexEntry( |
| 2852 | + flags=ChunkIndex.F_USED, size=0, pack_id=pack_id, obj_offset=obj_offset, obj_size=obj_size |
| 2853 | + ) |
| 2854 | + if not (not_found or unindexed): |
| 2855 | + continue |
| 2856 | + self.error_found = True |
| 2857 | + logger.error( |
| 2858 | + f"pack {pack_hex}: the chunks index does not match the pack. Indexed objects not in the pack: " |
| 2859 | + f"{len(not_found)}, objects in the pack with an unindexed chunk id: {len(unindexed)}. " |
| 2860 | + "Fixed the index." |
| 2861 | + ) |
| 2862 | + for chunk_id, obj_offset, obj_size in not_found: |
| 2863 | + logger.debug( |
| 2864 | + f"pack {pack_hex}: {bin_to_hex(chunk_id)} at offset {obj_offset}, {obj_size} bytes: not in pack" |
| 2865 | + ) |
| 2866 | + for chunk_id, obj_offset, obj_size in unindexed: |
| 2867 | + logger.debug( |
| 2868 | + f"pack {pack_hex}: {bin_to_hex(chunk_id)} at offset {obj_offset}, {obj_size} bytes: not indexed" |
| 2869 | + ) |
| 2870 | + |
2771 | 2871 | def finish(self): |
2772 | 2872 | if self.repair: |
2773 | | - # flush chunks re-added during repair so their packs are on the store and out of the pack |
2774 | | - # writer buffer (close() requires an empty buffer, #10055) before we (re)build the index. |
2775 | | - self.repository.flush() |
| 2873 | + # store the pack writer buffer before the index is written (close() requires an empty buffer, #10055). |
| 2874 | + self.record_stored(self.repository.flush()) |
2776 | 2875 | if self.chunks_modified: |
2777 | | - # the packs changed: rebuild the index from them and store it. The index/ fragments lack |
2778 | | - # the chunks this repair stored, so the index is invalid until the rebuilt one is stored. |
2779 | | - # Free the current index first, so only one index is in memory. |
| 2876 | + # the index/ fragments do not have the chunks this repair stored. |
2780 | 2877 | write_chunkindex_invalid(self.repository) |
2781 | | - self.repository.invalidate_chunk_index() |
2782 | | - self.chunks = None |
2783 | | - logger.info("Rebuilding and writing the repository chunks index.") |
2784 | | - build_chunkindex_from_repo( |
2785 | | - self.repository, |
2786 | | - slow_rebuild=True, |
2787 | | - validate=object_validator(self.repo_objs), |
2788 | | - on_drop=self.note_dropped_objects, |
2789 | | - write_immediately=True, |
2790 | | - ) |
2791 | | - else: |
2792 | | - # the packs are unchanged, so the index still matches them: persist it as is. |
2793 | | - logger.info("Writing the rebuilt repository chunks index.") |
2794 | | - write_chunkindex_to_repo( |
2795 | | - self.repository, self.chunks, incremental=False, clear=False, force_write=True, delete_other=True |
2796 | | - ) |
| 2878 | + self.verify_written_packs() |
| 2879 | + logger.info("Writing the rebuilt repository chunks index.") |
| 2880 | + write_chunkindex_to_repo( |
| 2881 | + self.repository, self.chunks, incremental=False, clear=False, force_write=True, delete_other=True |
| 2882 | + ) |
| 2883 | + # close() persists the in-memory index: drop it, the stored one is current. |
| 2884 | + self.repository.invalidate_chunk_index() |
| 2885 | + self.chunks = None |
2797 | 2886 | # the stored index matches the packs: clear the invalid marker. |
2798 | 2887 | delete_chunkindex_invalid(self.repository) |
2799 | | - # drop the in-memory index so close() does not persist it over the index just written. |
2800 | | - self.repository.invalidate_chunk_index() |
2801 | 2888 |
|
2802 | 2889 |
|
2803 | 2890 | class ArchiveRecreater: |
|
0 commit comments