Add granular DB error types with retry logic
New exceptions: DatabaseBusyError, DatabasePermissionDeniedError, DatabasePathInvalidError, DatabaseCorruptedError, DatabaseUnavailableError. open_db creates parent directory if missing. Catches all aiosqlite errors and maps to specific exception types. get_db retries up to 3x on locked database with backoff. Propagates specific exceptions instead of generic HTTPException. Tests for all new error types and retry behavior.
This commit is contained in:
@@ -475,14 +475,75 @@ async def init_db(db: aiosqlite.Connection) -> None:
|
||||
async def open_db(database_path: str) -> aiosqlite.Connection:
|
||||
"""Open a new application SQLite connection with the standard settings.
|
||||
|
||||
Creates the parent directory if it does not exist.
|
||||
|
||||
Args:
|
||||
database_path: Path to the BanGUI SQLite database.
|
||||
|
||||
Returns:
|
||||
A configured :class:`aiosqlite.Connection` instance.
|
||||
|
||||
Raises:
|
||||
DatabasePathInvalidError: If the directory cannot be created or is inaccessible.
|
||||
DatabasePermissionDeniedError: If aiosqlite.connect raises PermissionError.
|
||||
DatabaseCorruptedError: If the database file is corrupted.
|
||||
DatabaseUnavailableError: For any other unexpected error.
|
||||
"""
|
||||
await _cleanup_wal_files(database_path)
|
||||
db = await aiosqlite.connect(database_path)
|
||||
from app.exceptions import (
|
||||
DatabaseCorruptedError,
|
||||
DatabasePathInvalidError,
|
||||
DatabasePermissionDeniedError,
|
||||
DatabaseUnavailableError,
|
||||
)
|
||||
|
||||
db_dir = Path(database_path).parent
|
||||
if not db_dir.exists():
|
||||
try:
|
||||
db_dir.mkdir(parents=True, exist_ok=True)
|
||||
except PermissionError as exc:
|
||||
log.error("database_open_failed", error=str(exc), database_path=database_path)
|
||||
raise DatabasePathInvalidError(database_path) from exc
|
||||
except OSError as exc:
|
||||
log.error("database_open_failed", error=str(exc), database_path=database_path)
|
||||
raise DatabaseUnavailableError(database_path, str(exc)) from exc
|
||||
|
||||
try:
|
||||
db = await aiosqlite.connect(database_path)
|
||||
except PermissionError as exc:
|
||||
log.error("database_open_failed", error=str(exc), database_path=database_path)
|
||||
raise DatabasePermissionDeniedError(database_path) from exc
|
||||
except aiosqlite.OperationalError as exc:
|
||||
error_msg = str(exc).lower()
|
||||
sqlite_code = getattr(exc, "sqlite_errorcode", None)
|
||||
log.error(
|
||||
"database_open_failed",
|
||||
error=str(exc),
|
||||
sqlite_errorcode=sqlite_code,
|
||||
database_path=database_path,
|
||||
)
|
||||
if "database is locked" in error_msg or "busy" in error_msg:
|
||||
raise DatabaseUnavailableError(database_path, str(exc)) from exc
|
||||
if "unable to open database file" in error_msg:
|
||||
raise DatabasePathInvalidError(database_path) from exc
|
||||
raise DatabaseUnavailableError(database_path, str(exc)) from exc
|
||||
except aiosqlite.DatabaseError as exc:
|
||||
log.error(
|
||||
"database_open_failed",
|
||||
error=str(exc),
|
||||
database_path=database_path,
|
||||
)
|
||||
raise DatabaseCorruptedError(database_path) from exc
|
||||
except OSError as exc:
|
||||
log.error("database_open_failed", error=str(exc), database_path=database_path)
|
||||
raise DatabaseUnavailableError(database_path, str(exc)) from exc
|
||||
except Exception as exc:
|
||||
log.error("database_open_failed", error=str(exc), database_path=database_path)
|
||||
raise DatabaseUnavailableError(database_path, str(exc)) from exc
|
||||
|
||||
db.row_factory = aiosqlite.Row
|
||||
await _configure_connection(db)
|
||||
try:
|
||||
await _configure_connection(db)
|
||||
except Exception:
|
||||
await db.close()
|
||||
raise
|
||||
return db
|
||||
|
||||
Reference in New Issue
Block a user