From ee8eb3a83f0c4dd4afc2b9be427112b19ba24cfe Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 14 May 2026 12:10:34 -0500 Subject: [PATCH] Enable SQLite WAL journal mode and 5 s busy timeout Switch from the default DELETE/ROLLBACK journal to WAL so concurrent readers (SSE status polls, history queries) are not blocked while the generation worker holds a write transaction. Set a 5-second busy timeout to eliminate "database is locked" errors under brief write contention. Both PRAGMAs are applied via a custom creator function so every connection in the pool gets the settings at open time, not just the first one. --- backend/database/session.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/backend/database/session.py b/backend/database/session.py index de4cccd9..f67864f1 100644 --- a/backend/database/session.py +++ b/backend/database/session.py @@ -1,6 +1,7 @@ """Engine creation, initialization, and session management.""" import logging +import sqlite3 as _sqlite3 import uuid from sqlalchemy import create_engine @@ -21,6 +22,22 @@ logger = logging.getLogger(__name__) + +def _make_connection(db_path: str) -> _sqlite3.Connection: + """Open a SQLite connection with WAL journal mode and a 5-second busy timeout. + + WAL allows concurrent readers while a write is in progress (the default + DELETE journal blocks all readers). This matters for voicebox because SSE + status polls and history queries run concurrently with the generation worker + writing to the same database. The busy timeout prevents "database is + locked" errors when two writers briefly contend on the same write slot. + """ + conn = _sqlite3.connect(db_path, check_same_thread=False) + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA busy_timeout=5000") + return conn + + # Initialized by init_db() engine = None SessionLocal = None @@ -37,6 +54,14 @@ def init_db() -> None: engine = create_engine( f"sqlite:///{_db_path}", connect_args={"check_same_thread": False}, + # Each connection enables WAL journal mode and sets a 5-second busy + # timeout. WAL allows concurrent readers during a write (the default + # DELETE/ROLLBACK journal blocks all readers), which matters for + # voicebox because SSE status polls and history queries run + # concurrently with the generation worker writing to the same db. + # busy_timeout prevents "database is locked" errors when two + # connections briefly contend on the same write slot. + creator=lambda: _make_connection(str(_db_path)), ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)