Skip to content

Commit

Permalink
Automatic rebuild cache on exception, fixes #5213 (#8257)
Browse files Browse the repository at this point in the history
Try to rebuild cache if an exception is raised, fixes #5213

For now, we catch FileNotFoundError and FileIntegrityError.

Write cache config without manifest to prevent override of manifest_id.
This is needed in order to have an empty manifest_id.
This empty id triggers the re-syncing of the chunks cache by calling sync() inside LocalCache.__init__()

Adapt and extend test_cache_chunks to new behaviour:

- a cache wipe is expected now.
- borg detects the corrupt cache and wipes/rebuilds the cache.
- check if the in-memory and on-disk cache is as expected (a rebuilt chunks cache).
  • Loading branch information
Aztorius committed Jul 7, 2024
1 parent add9caf commit 0bd41ba
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 13 deletions.
29 changes: 23 additions & 6 deletions src/borg/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,12 @@ def __init__(self, repository, key, manifest, path=None, sync=True, warn_if_unen
self.security_manager.assert_access_unknown(warn_if_unencrypted, manifest, key)
self.create()

self.open()
try:
self.open()
except (FileNotFoundError, FileIntegrityError):
self.wipe_cache()
self.open()

try:
self.security_manager.assert_secure(manifest, key, cache_config=self.cache_config)

Expand Down Expand Up @@ -920,19 +925,31 @@ def check_cache_compatibility(self):
return True

def wipe_cache(self):
logger.warning("Discarding incompatible cache and forcing a cache rebuild")
archive_path = os.path.join(self.path, 'chunks.archive.d')
logger.warning("Discarding incompatible or corrupted cache and forcing a cache rebuild")
archive_path = os.path.join(self.path, "chunks.archive.d")
if os.path.isdir(archive_path):
shutil.rmtree(os.path.join(self.path, 'chunks.archive.d'))
os.makedirs(os.path.join(self.path, 'chunks.archive.d'))
self.chunks = ChunkIndex()
with SaveFile(os.path.join(self.path, files_cache_name()), binary=True):
with IntegrityCheckedFile(path=os.path.join(self.path, "chunks"), write=True) as fd:
self.chunks.write(fd)
self.cache_config.integrity["chunks"] = fd.integrity_data
with IntegrityCheckedFile(path=os.path.join(self.path, files_cache_name()), write=True) as fd:
pass # empty file
self.cache_config.manifest_id = ''
self.cache_config._config.set('cache', 'manifest', '')
self.cache_config.integrity[files_cache_name()] = fd.integrity_data
self.cache_config.manifest_id = ""
self.cache_config._config.set("cache", "manifest", "")
if not self.cache_config._config.has_section("integrity"):
self.cache_config._config.add_section("integrity")
for file, integrity_data in self.cache_config.integrity.items():
self.cache_config._config.set("integrity", file, integrity_data)
# This is needed to pass the integrity check later on inside CacheConfig.load()
self.cache_config._config.set("integrity", "manifest", "")

self.cache_config.ignored_features = set()
self.cache_config.mandatory_features = set()
with SaveFile(self.cache_config.config_path) as fd:
self.cache_config._config.write(fd)

def update_compatibility(self):
operation_to_features_map = self.manifest.get_all_mandatory_features()
Expand Down
30 changes: 23 additions & 7 deletions src/borg/testsuite/archiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ..crypto.key import KeyfileKeyBase, RepoKey, KeyfileKey, Passphrase, TAMRequiredError, ArchiveTAMRequiredError
from ..crypto.keymanager import RepoIdMismatch, NotABorgKeyFile
from ..crypto.file_integrity import FileIntegrityError
from ..hashindex import ChunkIndex
from ..helpers import Location, get_security_dir
from ..helpers import Manifest, MandatoryFeatureUnsupported, ArchiveInfo
from ..helpers import init_ec_warnings
Expand Down Expand Up @@ -4361,15 +4362,30 @@ def corrupt(self, file, amount=1):
fd.seek(-amount, io.SEEK_END)
fd.write(corrupted)

@pytest.mark.allow_cache_wipe
def test_cache_chunks(self):
self.corrupt(os.path.join(self.cache_path, 'chunks'))
self.create_src_archive("test")
chunks_path = os.path.join(self.cache_path, 'chunks')
chunks_before_corruption = set(ChunkIndex(path=chunks_path).iteritems())
self.corrupt(chunks_path)

if self.FORK_DEFAULT:
out = self.cmd('info', self.repository_location, exit_code=2)
assert 'failed integrity check' in out
else:
with pytest.raises(FileIntegrityError):
self.cmd('info', self.repository_location)
assert not self.FORK_DEFAULT # test does not support forking

chunks_in_memory = None
sync_chunks = LocalCache.sync

def sync_wrapper(cache):
nonlocal chunks_in_memory
sync_chunks(cache)
chunks_in_memory = set(cache.chunks.iteritems())

with patch.object(LocalCache, "sync", sync_wrapper):
out = self.cmd("info", self.repository_location)

assert chunks_in_memory == chunks_before_corruption
assert "forcing a cache rebuild" in out
chunks_after_repair = set(ChunkIndex(path=chunks_path).iteritems())
assert chunks_after_repair == chunks_before_corruption

def test_cache_files(self):
self.cmd('create', self.repository_location + '::test', 'input')
Expand Down

0 comments on commit 0bd41ba

Please sign in to comment.