mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 12:50:42 -07:00
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.
92 lines
2.7 KiB
Python
92 lines
2.7 KiB
Python
"""Engine creation, initialization, and session management."""
|
|
|
|
import logging
|
|
import uuid
|
|
|
|
from sqlalchemy import create_engine, event
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from .. import config
|
|
from .migrations import run_migrations
|
|
from .models import (
|
|
AudioChannel,
|
|
Base,
|
|
EffectPreset,
|
|
Generation,
|
|
GenerationVersion,
|
|
ProfileChannelMapping,
|
|
VoiceProfile,
|
|
)
|
|
from .seed import backfill_generation_versions, seed_builtin_presets
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Initialized by init_db()
|
|
engine = None
|
|
SessionLocal = None
|
|
_db_path = None
|
|
|
|
|
|
def init_db() -> None:
|
|
"""Initialize the database engine, run migrations, create tables, and seed data."""
|
|
global engine, SessionLocal, _db_path
|
|
|
|
_db_path = config.get_db_path()
|
|
_db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
engine = create_engine(
|
|
f"sqlite:///{_db_path}",
|
|
# 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)
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
# Create default audio channel if it doesn't exist
|
|
db = SessionLocal()
|
|
try:
|
|
default_channel = db.query(AudioChannel).filter(AudioChannel.is_default).first()
|
|
if not default_channel:
|
|
default_channel = AudioChannel(
|
|
id=str(uuid.uuid4()),
|
|
name="Default",
|
|
is_default=True,
|
|
)
|
|
db.add(default_channel)
|
|
|
|
for profile in db.query(VoiceProfile).all():
|
|
db.add(ProfileChannelMapping(
|
|
profile_id=profile.id,
|
|
channel_id=default_channel.id,
|
|
))
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
|
|
backfill_generation_versions(SessionLocal, Generation, GenerationVersion)
|
|
seed_builtin_presets(SessionLocal, EffectPreset)
|
|
|
|
|
|
def get_db():
|
|
"""Yield a database session (FastAPI dependency)."""
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|