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:
@@ -165,22 +165,61 @@ async def get_db(
|
||||
|
||||
Yields:
|
||||
An open :class:`aiosqlite.Connection` for the request.
|
||||
|
||||
Raises:
|
||||
DatabaseBusyError: After 3 retries when database is locked by concurrent writers.
|
||||
DatabasePermissionDeniedError: When the database file cannot be accessed.
|
||||
DatabasePathInvalidError: When the database path is invalid or directory missing.
|
||||
DatabaseCorruptedError: When the database file is corrupted.
|
||||
DatabaseUnavailableError: For any other unexpected database error.
|
||||
"""
|
||||
from app.db import open_db # noqa: PLC0415
|
||||
from app.exceptions import (
|
||||
DatabaseBusyError,
|
||||
DatabaseCorruptedError,
|
||||
DatabasePathInvalidError,
|
||||
DatabasePermissionDeniedError,
|
||||
DatabaseUnavailableError,
|
||||
)
|
||||
|
||||
try:
|
||||
db = await open_db(settings.database_path)
|
||||
except Exception as exc:
|
||||
log.error("database_open_failed", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Database is not available.",
|
||||
) from exc
|
||||
db = None
|
||||
retries = 3
|
||||
retry_delay = 0.1
|
||||
last_exc = None
|
||||
|
||||
for attempt in range(1, retries + 1):
|
||||
try:
|
||||
db = await open_db(settings.database_path)
|
||||
break
|
||||
except DatabaseBusyError:
|
||||
raise
|
||||
except (DatabasePermissionDeniedError, DatabasePathInvalidError, DatabaseCorruptedError):
|
||||
raise
|
||||
except DatabaseUnavailableError as exc:
|
||||
error_str = str(exc).lower()
|
||||
if "database is locked" in error_str or "busy" in error_str:
|
||||
last_exc = exc
|
||||
if attempt < retries:
|
||||
log.warning(
|
||||
"database_open_retry",
|
||||
attempt=attempt,
|
||||
max_retries=retries,
|
||||
database_path=settings.database_path,
|
||||
)
|
||||
import asyncio
|
||||
await asyncio.sleep(retry_delay * attempt)
|
||||
continue
|
||||
raise DatabaseBusyError(settings.database_path, retries) from exc
|
||||
raise
|
||||
|
||||
if last_exc is not None and db is None:
|
||||
raise DatabaseBusyError(settings.database_path, retries)
|
||||
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
await db.close()
|
||||
if db is not None:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def get_http_session(
|
||||
|
||||
Reference in New Issue
Block a user