|
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). |
@@ -2236,9 +2237,27 @@ class ArchiveChecker: |
2236 | 2237 | def __init__(self): |
2237 | 2238 | self.error_found = False |
2238 | 2239 | 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. |
2241 | 2241 | 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) |
2242 | 2261 |
|
2243 | 2262 | def note_dropped_objects(self): |
2244 | 2263 | # The chunk index rebuild skipped repository content to get past a corrupt object header. |
@@ -2295,7 +2314,8 @@ def check( |
2295 | 2314 | self.repo_objs = RepoObj(self.key) |
2296 | 2315 | validate = object_validator(self.repo_objs) |
2297 | 2316 | # 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. |
2299 | 2319 | self.repository.flush() |
2300 | 2320 | if not repair and self.repository.is_chunk_index_loaded: |
2301 | 2321 | # without --repair, use the loaded index. |
@@ -2423,9 +2443,14 @@ def verify_data(self): |
2423 | 2443 | # failed twice -> remove this defect chunk. delete rewrites its pack without it, |
2424 | 2444 | # keeping the other chunks, and removes it from self.chunks, so rebuild_archives |
2425 | 2445 | # 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) |
2428 | 2450 | 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) |
2429 | 2454 | else: |
2430 | 2455 | logger.warning("chunk %s not deleted, did not consistently fail.", bin_to_hex(defect_chunk)) |
2431 | 2456 | else: |
@@ -2513,7 +2538,7 @@ def valid_archive(obj): |
2513 | 2538 | self.error_found = True |
2514 | 2539 | if self.repair: |
2515 | 2540 | 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) |
2517 | 2542 | else: |
2518 | 2543 | logger.warning(f"Would create archives directory entry for {name} {archive_id_hex}.") |
2519 | 2544 |
|
@@ -2569,7 +2594,7 @@ def add_reference(id_, size, cdata): |
2569 | 2594 | # with --repair, store a chunk the repository does not have; put() adds it to self.chunks. |
2570 | 2595 | if self.repair and id_ not in self.chunks: |
2571 | 2596 | assert cdata is not None |
2572 | | - self.repository.put(id_, cdata) |
| 2597 | + self.record_stored(self.repository.put(id_, cdata)) |
2573 | 2598 | self.chunks_modified = True |
2574 | 2599 |
|
2575 | 2600 | def verify_file_chunks(archive_name, item): |
@@ -2782,51 +2807,125 @@ def valid_item(obj): |
2782 | 2807 | logger.debug(f"archive id new: {bin_to_hex(new_archive_id)}") |
2783 | 2808 | cdata = self.repo_objs.format(new_archive_id, {}, data, ro_type=ROBJ_ARCHIVE_META) |
2784 | 2809 | 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) |
2786 | 2811 | if archive_id != new_archive_id: |
2787 | 2812 | self.manifest.archives.delete_by_id(archive_id) |
2788 | 2813 | finally: |
2789 | 2814 | pi.finish() |
2790 | 2815 | report_missing_chunks() |
2791 | 2816 |
|
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" |
2810 | 2884 | ) |
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 |
2819 | 2890 | ) |
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" |
2825 | 2908 | ) |
| 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 |
2826 | 2927 | # the stored index matches the packs: clear the invalid marker. |
2827 | 2928 | 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() |
2830 | 2929 |
|
2831 | 2930 |
|
2832 | 2931 | class ArchiveRecreater: |
|
0 commit comments