db: thread-local SQLite connections + busy_timeout

The module-level _conn was being hit from FastAPI handlers, the retention
daemon thread, and asyncio.to_thread workers simultaneously. Sharing a
single sqlite3.Connection across threads is unsafe (cursors collide)
even with check_same_thread=False and WAL. The writer _lock didn't cover
readers, so a reader cursor could race a writer mid-statement.

Switch to threading.local(): each thread gets its own Connection via
_get_conn(). WAL handles concurrent readers/writer at the DB level;
busy_timeout=5000 absorbs short-lived "database is locked" when two
threads both try to BEGIN IMMEDIATE. The write-serialising _lock stays
— it keeps multi-statement writer blocks atomic and avoids busy-loop on
concurrent writers.

External access via db._conn replaced with a new db.has_running_application
helper (the only caller outside db.py).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
EiSiMo 2026-04-21 19:01:27 +02:00
parent 6bd7a4306a
commit 617c76cb54
2 changed files with 88 additions and 72 deletions

View file

@ -274,11 +274,7 @@ def _filter_summary(f) -> str:
def _has_running_application(user_id: int) -> bool:
row = db._conn.execute(
"SELECT 1 FROM applications WHERE user_id = ? AND finished_at IS NULL LIMIT 1",
(user_id,),
).fetchone()
return row is not None
return db.has_running_application(user_id)
def _finish_apply_background(app_id: int, user_id: int, flat_id: str, url: str,