-
Notifications
You must be signed in to change notification settings - Fork 444
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Handle database corruption exception at the SQLite connection level
- Loading branch information
Showing
20 changed files
with
400 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
from __future__ import annotations | ||
|
||
import logging | ||
import sqlite3 | ||
from contextlib import contextmanager | ||
from pathlib import Path | ||
from typing import Union | ||
|
||
logger = logging.getLogger('db_corruption_handling') | ||
|
||
|
||
class DatabaseIsCorrupted(Exception): | ||
pass | ||
|
||
|
||
@contextmanager | ||
def handling_malformed_db_error(db_filepath: Path): | ||
# Used in all methods of Connection and Cursor classes where the database corruption error can occur | ||
try: | ||
yield | ||
except Exception as e: | ||
if _is_malformed_db_exception(e): | ||
_mark_db_as_corrupted(db_filepath) | ||
raise DatabaseIsCorrupted(str(db_filepath)) from e | ||
raise | ||
|
||
|
||
def handle_db_if_corrupted(db_filename: Union[str, Path]): | ||
# Checks if the database is marked as corrupted and handles it by removing the database file and the marker file | ||
db_path = Path(db_filename) | ||
marker_path = get_corrupted_db_marker_path(db_path) | ||
if marker_path.exists(): | ||
_handle_corrupted_db(db_path) | ||
|
||
|
||
def get_corrupted_db_marker_path(db_filepath: Path) -> Path: | ||
return Path(str(db_filepath) + '.is_corrupted') | ||
|
||
|
||
def _is_malformed_db_exception(exception): | ||
return isinstance(exception, sqlite3.DatabaseError) and 'malformed' in str(exception) | ||
|
||
|
||
def _mark_db_as_corrupted(db_filepath: Path): | ||
# Creates a new `*.is_corrupted` marker file alongside the database file | ||
marker_path = get_corrupted_db_marker_path(db_filepath) | ||
marker_path.touch() | ||
|
||
|
||
def _handle_corrupted_db(db_path: Path): | ||
# Removes the database file and the marker file | ||
if db_path.exists(): | ||
logger.warning(f'Database file was marked as corrupted, removing it: {db_path}') | ||
db_path.unlink() | ||
|
||
marker_path = get_corrupted_db_marker_path(db_path) | ||
if marker_path.exists(): | ||
logger.warning(f'Removing the corrupted database marker: {marker_path}') | ||
marker_path.unlink() |
95 changes: 95 additions & 0 deletions
95
src/tribler/core/utilities/db_corruption_handling/sqlite_replacement.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
from __future__ import annotations | ||
|
||
import sqlite3 | ||
import sys | ||
from pathlib import Path | ||
from sqlite3 import DataError, DatabaseError, Error, IntegrityError, InterfaceError, InternalError, NotSupportedError, \ | ||
OperationalError, ProgrammingError, Warning, sqlite_version_info # pylint: disable=unused-import, redefined-builtin | ||
|
||
from tribler.core.utilities.db_corruption_handling.base import handling_malformed_db_error | ||
|
||
|
||
# This module serves as a replacement to the sqlite3 module and handles the case when the database is corrupted. | ||
# It provides the `connect` function that should be used instead of `sqlite3.connect` and the `Cursor` and `Connection` | ||
# classes that replaces `sqlite3.Cursor` and `sqlite3.Connection` classes respectively. If the `connect` function or | ||
# any Connectoin or Cursor method is called and the database is corrupted, the database is marked as corrupted and | ||
# the DatabaseIsCorrupted exception is raised. It should be handled by terminating the Tribler Core with the exit code | ||
# EXITCODE_DATABASE_IS_CORRUPTED (99). After the Core restarts, the `handle_db_if_corrupted` function checks the | ||
# presense of the database corruption marker and handles it by removing the database file and the corruption marker. | ||
# After that, the database is recreated upon the next attempt to connect to it. | ||
|
||
|
||
def connect(db_filename: str, **kwargs) -> sqlite3.Connection: | ||
# Replaces the sqlite3.connect function | ||
kwargs['factory'] = Connection | ||
with handling_malformed_db_error(Path(db_filename)): | ||
return sqlite3.connect(db_filename, **kwargs) | ||
|
||
|
||
def _add_method_wrapper_that_handles_malformed_db_exception(cls, method_name: str): | ||
# Creates a wrapper for the given method that handles the case when the database is corrupted | ||
|
||
def wrapper(self, *args, **kwargs): | ||
with handling_malformed_db_error(self._db_filepath): # pylint: disable=protected-access | ||
return getattr(super(cls, self), method_name)(*args, **kwargs) | ||
|
||
wrapper.__name__ = method_name | ||
wrapper.is_wrapped = True # for testing purposes | ||
setattr(cls, method_name, wrapper) | ||
|
||
|
||
class Cursor(sqlite3.Cursor): | ||
# Handles the case when the database is corrupted in all relevant methods. | ||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self._db_filepath = self.connection._db_filepath | ||
|
||
|
||
for method_name_ in ['execute', 'executemany', 'executescript', 'fetchall', 'fetchmany', 'fetchone', '__next__']: | ||
_add_method_wrapper_that_handles_malformed_db_exception(Cursor, method_name_) | ||
|
||
|
||
|
||
class ConnectionBase(sqlite3.Connection): | ||
# This class simplifies testing of the Connection class by allowing mocking of base class methods. | ||
# Direct mocking of sqlite3.Connection methods is not possible because they are C functions. | ||
|
||
if sys.version_info < (3, 11): | ||
def blobopen(self, *args, **kwargs) -> Blob: | ||
raise NotImplementedError | ||
|
||
|
||
class Connection(ConnectionBase): | ||
# Handles the case when the database is corrupted in all relevant methods. | ||
def __init__(self, db_filepath: str, *args, **kwargs): | ||
super().__init__(db_filepath, *args, **kwargs) | ||
self._db_filepath = Path(db_filepath) | ||
|
||
def cursor(self, factory=None) -> Cursor: | ||
return super().cursor(factory or Cursor) | ||
|
||
def iterdump(self): | ||
# Not implemented because it is not used in Tribler. | ||
# Can be added later with an iterator class that handles the malformed db error during the iteration | ||
raise NotImplementedError | ||
|
||
def blobopen(self, *args, **kwargs) -> Blob: # Works for Python >= 3.11 | ||
with handling_malformed_db_error(self._db_filepath): | ||
blob = super().blobopen(*args, **kwargs) | ||
return Blob(blob, self._db_filepath) | ||
|
||
|
||
for method_name_ in ['commit', 'execute', 'executemany', 'executescript', 'backup', '__enter__', '__exit__', | ||
'serialize', 'deserialize']: | ||
_add_method_wrapper_that_handles_malformed_db_exception(Connection, method_name_) | ||
|
||
|
||
class Blob: # For Python >= 3.11. Added now, so we do not forgot to add it later when upgrading to 3.11. | ||
def __init__(self, blob, db_filepath: Path): | ||
self._blob = blob | ||
self._db_filepath = db_filepath | ||
|
||
|
||
for method_name_ in ['close', 'read', 'write', 'seek', '__len__', '__enter__', '__exit__', '__getitem__', | ||
'__setitem__']: | ||
_add_method_wrapper_that_handles_malformed_db_exception(Blob, method_name_) |
Empty file.
Oops, something went wrong.