Skip to content

Commit ad05ba8

Browse files
Merge pull request #9203 from ThomasWaldmann/fix-compact-1.4
compact: fix dealing with mismatching hints (1.4-maint)
2 parents 3d32703 + 32946a9 commit ad05ba8

File tree

3 files changed

+121
-6
lines changed

3 files changed

+121
-6
lines changed

src/borg/repository.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,8 @@ def complete_xfer(intermediate=True):
796796
for segment in unused:
797797
logger.debug('complete_xfer: deleting unused segment %d', segment)
798798
count = self.segments.pop(segment)
799-
assert count == 0, 'Corrupted segment reference count - corrupted index or hints'
799+
if count != 0:
800+
logger.warning('Corrupted segment reference count %d (expected 0) for segment %d - corrupted index or hints', count, segment)
800801
self.io.delete_segment(segment)
801802
del self.compact[segment]
802803
unused = []
@@ -807,7 +808,8 @@ def complete_xfer(intermediate=True):
807808
for segment, freeable_space in sorted(self.compact.items()):
808809
if not self.io.segment_exists(segment):
809810
logger.warning('segment %d not found, but listed in compaction data', segment)
810-
del self.compact[segment]
811+
self.compact.pop(segment, None)
812+
self.segments.pop(segment, None)
811813
pi.show()
812814
continue
813815
segment_size = self.io.segment_size(segment)
@@ -907,7 +909,8 @@ def complete_xfer(intermediate=True):
907909
if not self.shadow_index[key]:
908910
# shadowed segments list is empty -> remove it
909911
del self.shadow_index[key]
910-
assert segments[segment] == 0, 'Corrupted segment reference count - corrupted index or hints'
912+
if segments[segment] != 0:
913+
logger.warning('Corrupted segment reference count %d (expected 0) for segment %d - corrupted index or hints', segments[segment], segment)
911914
unused.append(segment)
912915
pi.show()
913916
pi.finish()

src/borg/testsuite/issue_8535.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import os
2+
import pytest
3+
from borg.constants import * # noqa: F403
4+
from borg.helpers import msgpack
5+
from borg.testsuite.archiver import exec_cmd
6+
7+
8+
@pytest.fixture
9+
def borg_env(tmp_path, monkeypatch):
10+
monkeypatch.setenv('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING', 'YES')
11+
monkeypatch.setenv('BORG_DELETE_I_KNOW_WHAT_I_AM_DOING', 'YES')
12+
monkeypatch.setenv('BORG_PASSPHRASE', 'waytooeasyonlyfortests')
13+
monkeypatch.setenv('BORG_SELFTEST', 'disabled')
14+
15+
# Set up directories
16+
keys_path = tmp_path / 'keys'
17+
cache_path = tmp_path / 'cache'
18+
input_path = tmp_path / 'input'
19+
20+
monkeypatch.setenv('BORG_KEYS_DIR', str(keys_path))
21+
monkeypatch.setenv('BORG_CACHE_DIR', str(cache_path))
22+
23+
keys_path.mkdir()
24+
cache_path.mkdir()
25+
input_path.mkdir()
26+
27+
# Create test file
28+
(input_path / 'file1').write_bytes(b'X' * 1024 * 80)
29+
30+
cwd = os.getcwd()
31+
os.chdir(tmp_path)
32+
yield {
33+
'repo': str(tmp_path / 'repository'),
34+
'input': str(input_path),
35+
}
36+
os.chdir(cwd)
37+
38+
39+
def cmd(*args, **kw):
40+
kw.setdefault('fork', True)
41+
ret, output = exec_cmd(*args, **kw)
42+
if ret != 0:
43+
print(output)
44+
assert ret == 0
45+
return output
46+
47+
48+
def test_missing_segment_in_hints(borg_env):
49+
"""Test that compact handles missing segment files gracefully."""
50+
repo = borg_env['repo']
51+
52+
cmd('init', '--encryption=none', repo)
53+
cmd('create', repo + '::archive1', 'input')
54+
cmd('delete', repo + '::archive1')
55+
56+
# Find hints
57+
hints_files = sorted([f for f in os.listdir(repo) if f.startswith('hints.') and not f.endswith('.tmp')],
58+
key=lambda x: int(x.split('.')[1]))
59+
hints_file = os.path.join(repo, hints_files[-1])
60+
61+
with open(hints_file, 'rb') as f:
62+
hints = msgpack.unpack(f)
63+
64+
# Find data segment
65+
target_segment = None
66+
for seg in hints[b'compact'].keys():
67+
segment_file = os.path.join(repo, 'data', str(seg // 10000), str(seg))
68+
if os.path.exists(segment_file) and os.path.getsize(segment_file) > 100:
69+
target_segment = seg
70+
break
71+
72+
assert target_segment is not None
73+
74+
# Delete segment file
75+
segment_file = os.path.join(repo, 'data', str(target_segment // 10000), str(target_segment))
76+
os.unlink(segment_file)
77+
78+
# Compact should succeed
79+
cmd('compact', repo)
80+
81+
# Verify hints updated
82+
hints_files = sorted([f for f in os.listdir(repo) if f.startswith('hints.') and not f.endswith('.tmp')],
83+
key=lambda x: int(x.split('.')[1]))
84+
new_hints_file = os.path.join(repo, hints_files[-1])
85+
86+
with open(new_hints_file, 'rb') as f:
87+
new_hints = msgpack.unpack(f)
88+
89+
assert target_segment not in new_hints[b'compact']
90+
assert target_segment not in new_hints[b'segments']
91+
92+
93+
def test_index_corruption_with_old_hints(borg_env):
94+
"""Test that compact handles corrupted index (with old hints) gracefully."""
95+
repo = borg_env['repo']
96+
97+
cmd('init', '--encryption=none', repo)
98+
cmd('create', repo + '::archive1', 'input')
99+
100+
# Corrupt index
101+
index_files = sorted([f for f in os.listdir(repo) if f.startswith('index.') and not f.endswith('.tmp')],
102+
key=lambda x: int(x.split('.')[1]))
103+
index_path = os.path.join(repo, index_files[-1])
104+
105+
with open(index_path, 'wb') as f:
106+
f.write(b'corrupted')
107+
108+
# Compact should succeed (with fix)
109+
cmd('compact', repo)

src/borg/testsuite/repository.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -716,10 +716,13 @@ def test_subtly_corrupted_hints_without_integrity(self):
716716
with self.repository:
717717
self.repository.append_only = False
718718
self.repository.put(H(3), b'1234')
719-
# Do a compaction run. Fails, since the corrupted refcount was not detected and leads to an assertion failure.
720-
with pytest.raises(AssertionError) as exc_info:
719+
# Do a compaction run.
720+
# The corrupted refcount is detected and logged as a warning, but compaction proceeds.
721+
with self.assertLogs('borg.repository', level='WARNING') as cm:
721722
self.repository.commit(compact=True)
722-
assert 'Corrupted segment reference count' in str(exc_info.value)
723+
assert any('Corrupted segment reference count' in msg for msg in cm.output)
724+
# We verify that the repository is still consistent.
725+
assert self.repository.check()
723726

724727

725728
class RepositoryCheckTestCase(RepositoryTestCaseBase):

0 commit comments

Comments
 (0)