Skip to content

Commit 9ed75ca

Browse files
Item: remove .chunks_healthy, fixes #8559
Well, it's not totally removed, some code in Item, Archive and borg transfer --from-borg1 needs to stay in place, so that we can pick the CORRECT chunks list that is in .chunks_healthy for all-zero-replacement-chunk-patched items when transferring archives from borg1 to borg2 repos. FUSE fs read: IOError or all-zero result Other reads: TODO
1 parent 84744ac commit 9ed75ca

12 files changed

+88
-230
lines changed

Diff for: src/borg/archive.py

+13-90
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def unpack_many(self, ids, *, filter=None, preload=False):
281281
item = Item(internal_dict=_item)
282282
if "chunks" in item:
283283
item.chunks = [ChunkListEntry(*e) for e in item.chunks]
284-
if "chunks_healthy" in item:
284+
if "chunks_healthy" in item: # legacy
285285
item.chunks_healthy = [ChunkListEntry(*e) for e in item.chunks_healthy]
286286
if filter and not filter(item):
287287
continue
@@ -744,7 +744,6 @@ def same_item(item, st):
744744
# if a previous extraction was interrupted between setting the mtime and setting non-default flags.
745745
return True
746746

747-
has_damaged_chunks = "chunks_healthy" in item
748747
if dry_run or stdout:
749748
with self.extract_helper(item, "", hlm, dry_run=dry_run or stdout) as hardlink_set:
750749
if not hardlink_set:
@@ -771,8 +770,6 @@ def same_item(item, st):
771770
item_size, item_chunks_size
772771
)
773772
)
774-
if has_damaged_chunks:
775-
raise BackupError("File has damaged (all-zero) chunks. Try running borg check --repair.")
776773
return
777774

778775
dest = self.cwd
@@ -827,8 +824,6 @@ def make_parent(path):
827824
raise BackupError(
828825
f"Size inconsistency detected: size {item_size}, chunks size {item_chunks_size}"
829826
)
830-
if has_damaged_chunks:
831-
raise BackupError("File has damaged (all-zero) chunks. Try running borg check --repair.")
832827
return
833828
with backup_io:
834829
# No repository access beyond this point.
@@ -1141,10 +1136,6 @@ def chunk_processor(chunk):
11411136
return chunk_entry
11421137

11431138
item.chunks = []
1144-
# if we rechunkify, we'll get a fundamentally different chunks list, thus we need
1145-
# to get rid of .chunks_healthy, as it might not correspond to .chunks any more.
1146-
if self.rechunkify and "chunks_healthy" in item:
1147-
del item.chunks_healthy
11481139
for chunk in chunk_iter:
11491140
chunk_entry = chunk_processor(chunk)
11501141
item.chunks.append(chunk_entry)
@@ -1761,13 +1752,10 @@ def verify_data(self):
17611752
if defect_chunks:
17621753
if self.repair:
17631754
# if we kill the defect chunk here, subsequent actions within this "borg check"
1764-
# run will find missing chunks and replace them with all-zero replacement
1765-
# chunks and flag the files as "repaired".
1766-
# if another backup is done later and the missing chunks get backed up again,
1767-
# a "borg check" afterwards can heal all files where this chunk was missing.
1755+
# run will find missing chunks.
17681756
logger.warning(
1769-
"Found defect chunks. They will be deleted now, so affected files can "
1770-
"get repaired now and maybe healed later."
1757+
"Found defect chunks and will delete them now. "
1758+
"Reading files referencing these chunks will result in an I/O error."
17711759
)
17721760
for defect_chunk in defect_chunks:
17731761
# remote repo (ssh): retry might help for strange network / NIC / RAM errors
@@ -1787,10 +1775,7 @@ def verify_data(self):
17871775
else:
17881776
logger.warning("chunk %s not deleted, did not consistently fail.", bin_to_hex(defect_chunk))
17891777
else:
1790-
logger.warning(
1791-
"Found defect chunks. With --repair, they would get deleted, so affected "
1792-
"files could get repaired then and maybe healed later."
1793-
)
1778+
logger.warning("Found defect chunks. With --repair, they would get deleted.")
17941779
for defect_chunk in defect_chunks:
17951780
logger.debug("chunk %s is defect.", bin_to_hex(defect_chunk))
17961781
log = logger.error if errors else logger.info
@@ -1901,80 +1886,18 @@ def add_reference(id_, size, cdata):
19011886
self.repository.put(id_, cdata)
19021887

19031888
def verify_file_chunks(archive_name, item):
1904-
"""Verifies that all file chunks are present.
1905-
1906-
Missing file chunks will be replaced with new chunks of the same length containing all zeros.
1907-
If a previously missing file chunk re-appears, the replacement chunk is replaced by the correct one.
1908-
"""
1909-
1910-
def replacement_chunk(size):
1911-
chunk = Chunk(None, allocation=CH_ALLOC, size=size)
1912-
chunk_id, data = cached_hash(chunk, self.key.id_hash)
1913-
cdata = self.repo_objs.format(chunk_id, {}, data, ro_type=ROBJ_FILE_STREAM)
1914-
return chunk_id, size, cdata
1915-
1889+
"""Verifies that all file chunks are present. Missing file chunks will be logged."""
19161890
offset = 0
1917-
chunk_list = []
1918-
chunks_replaced = False
1919-
has_chunks_healthy = "chunks_healthy" in item
1920-
chunks_current = item.chunks
1921-
chunks_healthy = item.chunks_healthy if has_chunks_healthy else chunks_current
1922-
if has_chunks_healthy and len(chunks_current) != len(chunks_healthy):
1923-
# should never happen, but there was issue #3218.
1924-
logger.warning(f"{archive_name}: {item.path}: Invalid chunks_healthy metadata removed!")
1925-
del item.chunks_healthy
1926-
has_chunks_healthy = False
1927-
chunks_healthy = chunks_current
1928-
for chunk_current, chunk_healthy in zip(chunks_current, chunks_healthy):
1929-
chunk_id, size = chunk_healthy
1891+
for chunk in item.chunks:
1892+
chunk_id, size = chunk
19301893
if chunk_id not in self.chunks:
1931-
# a chunk of the healthy list is missing
1932-
if chunk_current == chunk_healthy:
1933-
logger.error(
1934-
"{}: {}: New missing file chunk detected (Byte {}-{}, Chunk {}). "
1935-
"Replacing with all-zero chunk.".format(
1936-
archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id)
1937-
)
1894+
logger.error(
1895+
"{}: {}: Missing file chunk detected (Byte {}-{}, Chunk {}).".format(
1896+
archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id)
19381897
)
1939-
self.error_found = chunks_replaced = True
1940-
chunk_id, size, cdata = replacement_chunk(size)
1941-
add_reference(chunk_id, size, cdata)
1942-
else:
1943-
logger.info(
1944-
"{}: {}: Previously missing file chunk is still missing (Byte {}-{}, Chunk {}). "
1945-
"It has an all-zero replacement chunk already.".format(
1946-
archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id)
1947-
)
1948-
)
1949-
chunk_id, size = chunk_current
1950-
if chunk_id not in self.chunks:
1951-
logger.warning(
1952-
"{}: {}: Missing all-zero replacement chunk detected (Byte {}-{}, Chunk {}). "
1953-
"Generating new replacement chunk.".format(
1954-
archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id)
1955-
)
1956-
)
1957-
self.error_found = chunks_replaced = True
1958-
chunk_id, size, cdata = replacement_chunk(size)
1959-
add_reference(chunk_id, size, cdata)
1960-
else:
1961-
if chunk_current == chunk_healthy:
1962-
pass # normal case, all fine.
1963-
else:
1964-
logger.info(
1965-
"{}: {}: Healed previously missing file chunk! (Byte {}-{}, Chunk {}).".format(
1966-
archive_name, item.path, offset, offset + size, bin_to_hex(chunk_id)
1967-
)
1968-
)
1969-
chunk_list.append([chunk_id, size]) # list-typed element as chunks_healthy is list-of-lists
1898+
)
1899+
self.error_found = True
19701900
offset += size
1971-
if chunks_replaced and not has_chunks_healthy:
1972-
# if this is first repair, remember the correct chunk IDs, so we can maybe heal the file later
1973-
item.chunks_healthy = item.chunks
1974-
if has_chunks_healthy and chunk_list == chunks_healthy:
1975-
logger.info(f"{archive_name}: {item.path}: Completely healed previously damaged file!")
1976-
del item.chunks_healthy
1977-
item.chunks = chunk_list
19781901
if "size" in item:
19791902
item_size = item.size
19801903
item_chunks_size = item.get_size(from_chunks=True)

Diff for: src/borg/archiver/check_cmd.py

+1-22
Original file line numberDiff line numberDiff line change
@@ -168,28 +168,7 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser):
168168
169169
2. When checking the consistency and correctness of archives, repair mode might
170170
remove whole archives from the manifest if their archive metadata chunk is
171-
corrupt or lost. On a chunk level (i.e. the contents of files), repair mode
172-
will replace corrupt or lost chunks with a same-size replacement chunk of
173-
zeroes. If a previously zeroed chunk reappears, repair mode will restore
174-
this lost chunk using the new chunk.
175-
176-
Most steps taken by repair mode have a one-time effect on the repository, like
177-
removing a lost archive from the repository. However, replacing a corrupt or
178-
lost chunk with an all-zero replacement will have an ongoing effect on the
179-
repository: When attempting to extract a file referencing an all-zero chunk,
180-
the ``extract`` command will distinctly warn about it. The FUSE filesystem
181-
created by the ``mount`` command will reject reading such a "zero-patched"
182-
file unless a special mount option is given.
183-
184-
As mentioned earlier, Borg might be able to "heal" a "zero-patched" file in
185-
repair mode, if all its previously lost chunks reappear (e.g. via a later
186-
backup). This is achieved by Borg not only keeping track of the all-zero
187-
replacement chunks, but also by keeping metadata about the lost chunks. In
188-
repair mode Borg will check whether a previously lost chunk reappeared and will
189-
replace the all-zero replacement chunk by the reappeared chunk. If all lost
190-
chunks of a "zero-patched" file reappear, this effectively "heals" the file.
191-
Consequently, if lost chunks were repaired earlier, it is advised to run
192-
``--repair`` a second time after creating some new backups.
171+
corrupt or lost. Borg will also report files that reference missing chunks.
193172
194173
If ``--repair --find-lost-archives`` is given, previously lost entries will
195174
be recreated in the archive directory. This is only possible before

Diff for: src/borg/archiver/compact_cmd.py

+8-25
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..cache import write_chunkindex_to_repo_cache, build_chunkindex_from_repo
77
from ..constants import * # NOQA
88
from ..hashindex import ChunkIndex, ChunkIndexEntry
9-
from ..helpers import set_ec, EXIT_WARNING, EXIT_ERROR, format_file_size, bin_to_hex
9+
from ..helpers import set_ec, EXIT_ERROR, format_file_size, bin_to_hex
1010
from ..helpers import ProgressIndicatorPercent
1111
from ..manifest import Manifest
1212
from ..remote import RemoteRepository
@@ -39,9 +39,7 @@ def garbage_collect(self):
3939
logger.info("Starting compaction / garbage collection...")
4040
self.chunks = self.get_repository_chunks()
4141
logger.info("Computing object IDs used by archives...")
42-
(self.missing_chunks, self.reappeared_chunks, self.total_files, self.total_size, self.archives_count) = (
43-
self.analyze_archives()
44-
)
42+
(self.missing_chunks, self.total_files, self.total_size, self.archives_count) = self.analyze_archives()
4543
self.report_and_delete()
4644
self.save_chunk_index()
4745
logger.info("Finished compaction / garbage collection...")
@@ -73,28 +71,24 @@ def save_chunk_index(self):
7371
self.chunks.clear() # we already have updated the repo cache in get_repository_chunks
7472
self.chunks = None # nothing there (cleared!)
7573

76-
def analyze_archives(self) -> Tuple[Set, Set, int, int, int]:
77-
"""Iterate over all items in all archives, create the dicts id -> size of all used/wanted chunks."""
74+
def analyze_archives(self) -> Tuple[Set, int, int, int]:
75+
"""Iterate over all items in all archives, create the dicts id -> size of all used chunks."""
7876

79-
def use_it(id, *, wanted=False):
77+
def use_it(id):
8078
entry = self.chunks.get(id)
8179
if entry is not None:
8280
# the chunk is in the repo, mark it used.
8381
self.chunks[id] = entry._replace(flags=entry.flags | ChunkIndex.F_USED)
84-
if wanted:
85-
# chunk id is from chunks_healthy list: a lost chunk has re-appeared!
86-
reappeared_chunks.add(id)
8782
else:
8883
# with --stats: we do NOT have this chunk in the repository!
8984
# without --stats: we do not have this chunk or the chunks index is incomplete.
9085
missing_chunks.add(id)
9186

9287
missing_chunks: set[bytes] = set()
93-
reappeared_chunks: set[bytes] = set()
9488
archive_infos = self.manifest.archives.list(sort_by=["ts"])
9589
num_archives = len(archive_infos)
9690
pi = ProgressIndicatorPercent(
97-
total=num_archives, msg="Computing used/wanted chunks %3.1f%%", step=0.1, msgid="compact.analyze_archives"
91+
total=num_archives, msg="Computing used chunks %3.1f%%", step=0.1, msgid="compact.analyze_archives"
9892
)
9993
total_size, total_files = 0, 0
10094
for i, info in enumerate(archive_infos):
@@ -114,25 +108,14 @@ def use_it(id, *, wanted=False):
114108
for id, size in item.chunks:
115109
total_size += size # original, uncompressed file content size
116110
use_it(id)
117-
if "chunks_healthy" in item:
118-
# we also consider the chunks_healthy chunks as referenced - do not throw away
119-
# anything that borg check --repair might still need.
120-
for id, size in item.chunks_healthy:
121-
use_it(id, wanted=True)
122111
pi.finish()
123-
return missing_chunks, reappeared_chunks, total_files, total_size, num_archives
112+
return missing_chunks, total_files, total_size, num_archives
124113

125114
def report_and_delete(self):
126-
run_repair = " Run borg check --repair!"
127-
128115
if self.missing_chunks:
129-
logger.error(f"Repository has {len(self.missing_chunks)} missing objects." + run_repair)
116+
logger.error(f"Repository has {len(self.missing_chunks)} missing objects!")
130117
set_ec(EXIT_ERROR)
131118

132-
if self.reappeared_chunks:
133-
logger.warning(f"{len(self.reappeared_chunks)} previously missing objects re-appeared!" + run_repair)
134-
set_ec(EXIT_WARNING)
135-
136119
logger.info("Cleaning archives directory from soft-deleted archives...")
137120
archive_infos = self.manifest.archives.list(sort_by=["ts"], deleted=True)
138121
for archive_info in archive_infos:

Diff for: src/borg/archiver/mount_cmds.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser
104104
105105
- ``versions``: when used with a repository mount, this gives a merged, versioned
106106
view of the files in the archives. EXPERIMENTAL, layout may change in future.
107-
- ``allow_damaged_files``: by default damaged files (where missing chunks were
108-
replaced with runs of zeros by ``borg check --repair``) are not readable and
109-
return EIO (I/O error). Set this option to read such files.
107+
- ``allow_damaged_files``: by default damaged files (where chunks are missing)
108+
will return EIO (I/O error) when trying to read the related parts of the file.
109+
Set this option to replace the missing parts with all-zero bytes.
110110
- ``ignore_permissions``: for security reasons the ``default_permissions`` mount
111111
option is internally enforced by borg. ``ignore_permissions`` can be given to
112112
not enforce ``default_permissions``.

Diff for: src/borg/archiver/recreate_cmd.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,10 @@ def build_parser_recreate(self, subparsers, common_parser, mid_common_parser):
9595
at least the entire deduplicated size of the archives using the previous
9696
chunker params.
9797
98-
If you recently ran borg check --repair and it had to fix lost chunks with all-zero
99-
replacement chunks, please first run another backup for the same data and re-run
100-
borg check --repair afterwards to heal any archives that had lost chunks which are
101-
still generated from the input data.
102-
103-
Important: running borg recreate to re-chunk will remove the chunks_healthy
104-
metadata of all items with replacement chunks, so healing will not be possible
105-
any more after re-chunking (it is also unlikely it would ever work: due to the
106-
change of chunking parameters, the missing chunk likely will never be seen again
107-
even if you still have the data that produced it).
98+
If your most recent borg check found missing chunks, please first run another
99+
backup for the same data, before doing any rechunking. If you are lucky, that
100+
will re-create the missing chunks. Optionally, do another borg check, to see
101+
if the chunks are still missing).
108102
"""
109103
)
110104
subparser = subparsers.add_parser(

0 commit comments

Comments
 (0)