From 00cd8ca252d58aedf2a7b82e25684e2d7f7b0337 Mon Sep 17 00:00:00 2001 From: m17hr1l Date: Sun, 7 Jun 2026 11:57:07 +0200 Subject: [PATCH] =?UTF-8?q?db:=20NullPool=20+=20WAL=20+=20busy=5Ftimeout?= =?UTF-8?q?=20=E2=80=94=20fixes=20QueuePool=20exhaustion=20under=20federat?= =?UTF-8?q?ion+classify=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/psyc/db.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/psyc/db.py b/src/psyc/db.py index a6e66b0..d358882 100644 --- a/src/psyc/db.py +++ b/src/psyc/db.py @@ -17,11 +17,13 @@ from sqlalchemy import ( Table, Text, create_engine, + event, func, insert, select, ) from sqlalchemy.dialects.sqlite import insert as sqlite_insert +from sqlalchemy.pool import NullPool from psyc import DATA_DIR, log from psyc.models import Case @@ -209,10 +211,31 @@ _engine: Optional[Engine] = None def engine(db_path: Path = DB_PATH) -> Engine: + """Lazy-init the SQLite engine. + + Uses NullPool — SQLite doesn't benefit from connection pooling (it's a + file, opens are cheap) and the default QueuePool starved the classify + + federation + cockpit-request workers under real load. WAL journal mode + + a 30s busy timeout let readers and a writer share the file safely. + """ global _engine if _engine is None: db_path.parent.mkdir(parents=True, exist_ok=True) - _engine = create_engine(f"sqlite:///{db_path}", future=True) + _engine = create_engine( + f"sqlite:///{db_path}", + future=True, + poolclass=NullPool, + connect_args={"check_same_thread": False, "timeout": 30}, + ) + + @event.listens_for(_engine, "connect") + def _sqlite_pragmas(dbapi_conn, _connection_record): # noqa: D401 + cur = dbapi_conn.cursor() + cur.execute("PRAGMA journal_mode=WAL") + cur.execute("PRAGMA synchronous=NORMAL") + cur.execute("PRAGMA busy_timeout=30000") + cur.close() + return _engine