diff --git a/app/core/database.py b/app/core/database.py index 72e3ed2f..f43239e9 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -10,12 +10,39 @@ This module provides classes and functions for: import logging -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event from sqlalchemy.orm import declarative_base, sessionmaker from .config import settings -engine = create_engine(settings.database_url) + +def _configure_sqlite_connection(dbapi_connection, connection_record): + """Configure SQLite connection for better concurrency. + + - WAL mode: Allows concurrent reads during writes + - busy_timeout: Wait up to 30 seconds if database is locked + - synchronous=NORMAL: Balance between safety and performance + """ + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA journal_mode=WAL") + cursor.execute("PRAGMA busy_timeout=30000") + cursor.execute("PRAGMA synchronous=NORMAL") + cursor.close() + + +# Create engine with SQLite-specific configuration +engine_kwargs = {} + +# Add SQLite-specific settings for better concurrent access +if settings.database_url.startswith("sqlite"): + engine_kwargs["connect_args"] = {"check_same_thread": False} + +engine = create_engine(settings.database_url, **engine_kwargs) + +# Configure SQLite pragmas on connection +if settings.database_url.startswith("sqlite"): + event.listen(engine, "connect", _configure_sqlite_connection) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/app/core/logging.py b/app/core/logging.py index 660ba5d7..18757755 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -23,71 +23,87 @@ class DatabaseLogHandler(logging.Handler): Custom logging handler that stores WARNING, ERROR, and CRITICAL logs in database. Runs asynchronously to avoid blocking application performance. + Uses retry logic for SQLite database locking issues. """ + MAX_RETRIES = 3 + RETRY_DELAY = 0.1 # 100ms delay between retries + def __init__(self): super().__init__() self.setLevel(logging.WARNING) # Only log WARNING and above to database def emit(self, record): - """Emit a log record to the database.""" - try: - from app.core.database import SessionLocal - from models.database.admin import ApplicationLog - - # Skip if no database session available - db = SessionLocal() - if not db: - return + """Emit a log record to the database with retry logic for SQLite locking.""" + import time + for attempt in range(self.MAX_RETRIES): try: - # Extract exception information if present - exception_type = None - exception_message = None - stack_trace = None + from app.core.database import SessionLocal + from models.database.admin import ApplicationLog - if record.exc_info: - exception_type = record.exc_info[0].__name__ if record.exc_info[0] else None - exception_message = str(record.exc_info[1]) if record.exc_info[1] else None - stack_trace = "".join(traceback.format_exception(*record.exc_info)) + # Skip if no database session available + db = SessionLocal() + if not db: + return - # Extract context from record (if middleware added it) - user_id = getattr(record, "user_id", None) - vendor_id = getattr(record, "vendor_id", None) - request_id = getattr(record, "request_id", None) - context = getattr(record, "context", None) + try: + # Extract exception information if present + exception_type = None + exception_message = None + stack_trace = None - # Create log entry - log_entry = ApplicationLog( - timestamp=datetime.fromtimestamp(record.created, tz=UTC), - level=record.levelname, - logger_name=record.name, - module=record.module, - function_name=record.funcName, - line_number=record.lineno, - message=record.getMessage(), - exception_type=exception_type, - exception_message=exception_message, - stack_trace=stack_trace, - request_id=request_id, - user_id=user_id, - vendor_id=vendor_id, - context=context, - ) + if record.exc_info: + exception_type = record.exc_info[0].__name__ if record.exc_info[0] else None + exception_message = str(record.exc_info[1]) if record.exc_info[1] else None + stack_trace = "".join(traceback.format_exception(*record.exc_info)) - db.add(log_entry) - db.commit() + # Extract context from record (if middleware added it) + user_id = getattr(record, "user_id", None) + vendor_id = getattr(record, "vendor_id", None) + request_id = getattr(record, "request_id", None) + context = getattr(record, "context", None) - except Exception as e: - # If database logging fails, don't crash the app - # Just print to stderr - print(f"Failed to write log to database: {e}", file=sys.stderr) - finally: - db.close() + # Create log entry + log_entry = ApplicationLog( + timestamp=datetime.fromtimestamp(record.created, tz=UTC), + level=record.levelname, + logger_name=record.name, + module=record.module, + function_name=record.funcName, + line_number=record.lineno, + message=record.getMessage(), + exception_type=exception_type, + exception_message=exception_message, + stack_trace=stack_trace, + request_id=request_id, + user_id=user_id, + vendor_id=vendor_id, + context=context, + ) - except Exception: - # Silently fail - logging should never crash the app - pass + db.add(log_entry) + db.commit() + return # Success, exit retry loop + + except Exception as e: + db.rollback() + # Check if it's a database locked error + if "database is locked" in str(e).lower(): + if attempt < self.MAX_RETRIES - 1: + time.sleep(self.RETRY_DELAY * (attempt + 1)) + continue + # For other errors or final attempt, silently skip + # Don't print to stderr to avoid log spam during imports + pass + finally: + db.close() + + except Exception: + # Silently fail - logging should never crash the app + pass + + break # Exit retry loop on non-recoverable errors def get_log_level_from_db():