diff --git a/backend/database/session.py b/backend/database/session.py index de4cccd9..a8bbf118 100644 --- a/backend/database/session.py +++ b/backend/database/session.py @@ -3,20 +3,20 @@ import logging import uuid -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from .. import config +from .migrations import run_migrations from .models import ( - Base, AudioChannel, + Base, EffectPreset, Generation, GenerationVersion, ProfileChannelMapping, VoiceProfile, ) -from .migrations import run_migrations from .seed import backfill_generation_versions, seed_builtin_presets logger = logging.getLogger(__name__) @@ -36,9 +36,22 @@ def init_db() -> None: engine = create_engine( f"sqlite:///{_db_path}", - connect_args={"check_same_thread": False}, + # timeout is sqlite3's busy handler: wait up to 30s on a locked + # database instead of raising "database is locked" immediately. + connect_args={"check_same_thread": False, "timeout": 30}, ) + @event.listens_for(engine, "connect") + def _set_sqlite_pragmas(dbapi_connection, _connection_record): + # WAL lets readers proceed while a writer holds the lock, which is + # the main source of lock racing between the generation worker and + # request handlers. synchronous=NORMAL is the recommended pairing + # (durable across app crashes, fsyncs only on checkpoint). + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA journal_mode=WAL") + cursor.execute("PRAGMA synchronous=NORMAL") + cursor.close() + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) run_migrations(engine) @@ -47,7 +60,7 @@ def init_db() -> None: # Create default audio channel if it doesn't exist db = SessionLocal() try: - default_channel = db.query(AudioChannel).filter(AudioChannel.is_default == True).first() + default_channel = db.query(AudioChannel).filter(AudioChannel.is_default).first() if not default_channel: default_channel = AudioChannel( id=str(uuid.uuid4()),