diff --git a/src/tribler/core/upgrade/tests/test_upgrader.py b/src/tribler/core/upgrade/tests/test_upgrader.py index e6b1a369c92..66dc7081a42 100644 --- a/src/tribler/core/upgrade/tests/test_upgrader.py +++ b/src/tribler/core/upgrade/tests/test_upgrader.py @@ -3,7 +3,7 @@ import time from pathlib import Path from typing import Set -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from ipv8.keyvault.private.libnaclkey import LibNaCLSK @@ -15,8 +15,10 @@ from tribler.core.tests.tools.common import TESTS_DATA_DIR from tribler.core.upgrade.db8_to_db10 import calc_progress from tribler.core.upgrade.tags_to_knowledge.tags_db import TagDatabase -from tribler.core.upgrade.upgrade import TriblerUpgrader, cleanup_noncompliant_channel_torrents +from tribler.core.upgrade.upgrade import TriblerUpgrader, catch_db_is_corrupted_exception, \ + cleanup_noncompliant_channel_torrents from tribler.core.utilities.configparser import CallbackConfigParser +from tribler.core.utilities.pony_utils import DatabaseIsCorrupted from tribler.core.utilities.utilities import random_infohash @@ -55,6 +57,35 @@ def _copy(source_name, target): shutil.copyfile(source, target) +def test_catch_db_is_corrupted_exception_with_exception(): + upgrader = Mock(_db_is_corrupted_exception=None) + upgrader_method = Mock(side_effect=DatabaseIsCorrupted()) + decorated_method = catch_db_is_corrupted_exception(upgrader_method) + + # Call the decorated method and expect it to catch the exception + decorated_method(upgrader) + upgrader_method.assert_called_once() + + # Check if the exception was caught and stored + upgrader_method.assert_called_once() + assert isinstance(upgrader._db_is_corrupted_exception, DatabaseIsCorrupted) + upgrader._logger.exception.assert_called_once() + + +def test_catch_db_is_corrupted_exception_without_exception(): + upgrader = Mock(_db_is_corrupted_exception=None) + upgrader_method = Mock() + decorated_method = catch_db_is_corrupted_exception(upgrader_method) + + # Call the decorated method and expect it to run without exceptions + decorated_method(upgrader) + + # Check if the method was called and no exception was stored + upgrader_method.assert_called_once() + assert upgrader._db_is_corrupted_exception is None + upgrader._logger.exception.assert_not_called() + + def test_upgrade_pony_db_complete(upgrader, channels_dir, state_dir, trustchain_keypair, mds_path): # pylint: disable=W0621 """ diff --git a/src/tribler/core/upgrade/upgrade.py b/src/tribler/core/upgrade/upgrade.py index 76e1db17ced..6912b42adc5 100644 --- a/src/tribler/core/upgrade/upgrade.py +++ b/src/tribler/core/upgrade/upgrade.py @@ -71,6 +71,19 @@ def cleanup_noncompliant_channel_torrents(state_dir): def catch_db_is_corrupted_exception(upgrader_method): + # This decorator applied for TriblerUpgrader methods. It suppresses and remembers the DatabaseIsCorrupted exception. + # As a result, if one upgrade method raises an exception, the following upgrade methods are still executed. + + # The reason for this is the following: it is possible that one upgrade methods upgrades a database A, while + # the next upgrade method upgrades a database B. If a corruption detected in the database A, the database B still + # need to be upgraded. So we want to temporarily suppress DatabaseIsCorrupted exception until all upgrades are + # executed. + + # If an upgrade found the database to be corrupted, the database is marked as corrupted. Then, the next upgrade + # will rename the corrupted database file (this is handled by the get_db_version call) and immediately return + # because there is no database to upgrade. So, if one upgrade function detects the database corruption, all the + # following upgrade functions for this specific database will skip the actual upgrade. As a result, a new + # database with the current DB version will be created on the Tribler Core start. @wraps(upgrader_method) def new_method(*args, **kwargs): @@ -81,7 +94,7 @@ def new_method(*args, **kwargs): self._logger.exception(exc) if not self._db_is_corrupted_exception: - self._db_is_corrupted_exception = exc + self._db_is_corrupted_exception = exc # Suppress and remember the exception to re-raise it later return new_method @@ -125,6 +138,9 @@ def run(self): self.upgrade_pony_db_14to15() if self._db_is_corrupted_exception: + # The current code is executed in the worker's thread. After all upgrade methods are executed, + # we re-raise the delayed exception, and then it is received and handled in the main thread + # by the UpgradeManager.on_worker_finished signal handler. raise self._db_is_corrupted_exception # pylint: disable=raising-bad-type def remove_old_logs(self) -> Tuple[List[Path], List[Path]]: