|
30 | 30 | import borg |
31 | 31 | import borg.helpers.errors |
32 | 32 | from .. import xattr, helpers, platform |
33 | | -from ..archive import Archive, ChunkBuffer, ChunksProcessor |
| 33 | +from ..archive import Archive, ChunkBuffer, ChunksProcessor, MetadataCollector |
34 | 34 | from ..archiver import Archiver, parse_storage_quota, PURE_PYTHON_MSGPACK_WARNING |
35 | 35 | from ..cache import Cache, LocalCache |
36 | 36 | from ..chunker import has_seek_hole |
@@ -1325,6 +1325,45 @@ def test_create_erroneous_file_read_retry_rolls_back_chunks(self): |
1325 | 1325 | for chunk_id, refs in item_chunk_refs.items(): |
1326 | 1326 | assert refcounts[chunk_id] == refs |
1327 | 1327 |
|
| 1328 | + def test_create_ext_attrs_error_retry_rolls_back_chunks(self): |
| 1329 | + # a read error that happens AFTER chunking (here: while reading the extended attributes) must |
| 1330 | + # also roll back the chunk increfs, so a retry does not end up with bad (too high) refcounts. |
| 1331 | + # the chunker reads the file fine; the failure is injected into stat_ext_attrs instead. |
| 1332 | + from collections import Counter |
| 1333 | + chunk_size = 1000 |
| 1334 | + # distinct content per block, so each block maps to its own (unique) chunk id: |
| 1335 | + self.create_regular_file('file', contents=b'A' * chunk_size + b'B' * chunk_size) |
| 1336 | + self.cmd('init', '--encryption=repokey', self.repository_location) |
| 1337 | + |
| 1338 | + orig_stat_ext_attrs = MetadataCollector.stat_ext_attrs |
| 1339 | + failed = [] |
| 1340 | + |
| 1341 | + def failing_stat_ext_attrs(self, st, path, fd=None): |
| 1342 | + # fail exactly once, on the first regular file we read the extended attributes for: |
| 1343 | + if stat.S_ISREG(st.st_mode) and not failed: |
| 1344 | + failed.append(True) |
| 1345 | + raise borg.helpers.errors.BackupOSError( |
| 1346 | + 'extended stat (xattrs)', OSError(errno.EIO, 'simulated I/O error', path)) |
| 1347 | + return orig_stat_ext_attrs(self, st, path, fd=fd) |
| 1348 | + |
| 1349 | + with patch.object(MetadataCollector, 'stat_ext_attrs', failing_stat_ext_attrs): |
| 1350 | + out = self.cmd('create', f'--chunker-params=fixed,{chunk_size}', '--list', |
| 1351 | + self.repository_location + '::test', 'input') |
| 1352 | + assert 'retry: 1 of ' in out |
| 1353 | + assert failed # the error was actually injected |
| 1354 | + assert 'E input/file' not in out # it was backed up ok on the retry |
| 1355 | + |
| 1356 | + with Repository(self.repository_path, exclusive=True) as repository: |
| 1357 | + manifest, key = Manifest.load(repository, Manifest.NO_OPERATION_CHECK) |
| 1358 | + archive = Archive(repository, key, manifest, 'test') |
| 1359 | + item = [item for item in archive.iter_items() if item.path == 'input/file'][0] |
| 1360 | + item_chunk_refs = Counter(chunk.id for chunk in item.chunks) |
| 1361 | + with Cache(repository, key, manifest) as cache: |
| 1362 | + refcounts = {id: entry.refcount for id, entry in cache.chunks.iteritems()} |
| 1363 | + # a leaked chunk from the failed attempt would show up here as a too high refcount: |
| 1364 | + for chunk_id, refs in item_chunk_refs.items(): |
| 1365 | + assert refcounts[chunk_id] == refs |
| 1366 | + |
1328 | 1367 | def test_create_erroneous_file_with_part_files(self): |
1329 | 1368 | # if we have already written part files (checkpoints) for a file, a later read error must |
1330 | 1369 | # NOT trigger a retry: re-reading the file from the start would create duplicate / inconsistent |
@@ -4560,6 +4599,10 @@ def test_create_changed_file_retry_rolls_back_chunks(self): |
4560 | 4599 | def test_create_erroneous_file_with_part_files(self): |
4561 | 4600 | pass |
4562 | 4601 |
|
| 4602 | + @unittest.skip('patches objects') |
| 4603 | + def test_create_ext_attrs_error_retry_rolls_back_chunks(self): |
| 4604 | + pass |
| 4605 | + |
4563 | 4606 | @unittest.skip('test_basic_functionality seems incompatible with fakeroot and/or the binary.') |
4564 | 4607 | def test_basic_functionality(self): |
4565 | 4608 | pass |
@@ -5024,6 +5067,10 @@ def test_create_changed_file_retry_rolls_back_chunks(self): |
5024 | 5067 | def test_create_erroneous_file_read_retry_rolls_back_chunks(self): |
5025 | 5068 | pass |
5026 | 5069 |
|
| 5070 | + @unittest.skip('only works locally') # inspects cache chunk refcounts via a local Cache/Repository |
| 5071 | + def test_create_ext_attrs_error_retry_rolls_back_chunks(self): |
| 5072 | + pass |
| 5073 | + |
5027 | 5074 | @unittest.skip('only works locally') |
5028 | 5075 | def test_config(self): |
5029 | 5076 | pass |
|
0 commit comments