perf(db): enable WAL and busy timeout for SQLite

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.
This commit is contained in:
Jamie Pine
2026-07-26 23:17:20 -07:00
parent c5f9d3b0f3
commit b7b7d62b7d
+18 -5
View File
@@ -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()),