From b7b7d62b7d7d8c51b6b52aefcf504672fd45e1cf Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sun, 26 Jul 2026 23:17:20 -0700 Subject: [PATCH] perf(db): enable WAL and busy timeout for SQLite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generation worker and request handlers race on the database — enough that orphan-recovery code exists in two places to clean up after 'database is locked' failures. WAL lets readers proceed during a write, synchronous=NORMAL is the recommended pairing, and the 30s sqlite3 timeout waits on a locked database instead of raising immediately. --- backend/database/session.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) 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()),