- app/tx/reconcile.py called the payout retry "a future payout-retry routine — still an open gap". It shipped as B-26: clearing payout_txid leaves the round in exactly the state _retry_payout_if_due picks up, so an abandoned payout rebuilds itself and the log line next to it is an alert, not the recovery path. Reading it the old way, an operator would go hand-fix a round the scheduler was already retrying. - app/db/base.py sized the SQLite busy timeout against "five concurrent background tasks" and then listed only the non-listener ones; the lifespan starts six. - The third item (app/auth/routes.py citing B-31 where it meant B-33) was already correct in the tree; the test pins it so it stays that way. tests/unit/test_code_comments.py derives the task count from the lifespan's own create_task calls rather than restating it, so the comment fails the next time a task is added or removed instead of quietly going stale again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
48 lines
1.9 KiB
Python
48 lines
1.9 KiB
Python
from sqlalchemy import event
|
|
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
|
from sqlalchemy.orm import DeclarativeBase
|
|
|
|
from app.config import settings
|
|
|
|
# How long a writer waits for a lock held by another writer before SQLite raises
|
|
# "database is locked" (B-39). A few seconds is enough to ride out this app's own
|
|
# six concurrent background tasks (Electrum listener, scheduler, confirmation
|
|
# poller, RBF bumper, two reconcilers) plus HTTP handlers briefly overlapping a
|
|
# write.
|
|
_SQLITE_BUSY_TIMEOUT_MS = 5000
|
|
|
|
|
|
def _register_sqlite_pragmas(target_engine: AsyncEngine) -> None:
|
|
"""Without WAL, SQLite's default (rollback-journal) mode lets a writer block
|
|
every reader for the duration of its transaction, and a second writer arriving
|
|
while one is already active fails immediately rather than waiting at all —
|
|
realistic under this app's concurrency, and nothing previously handled it.
|
|
WAL lets readers and writers proceed without blocking each other, and
|
|
busy_timeout gives a second writer a real window to wait for the first
|
|
instead of an instant `OperationalError`.
|
|
|
|
No-op for any dialect other than sqlite (e.g. a future PostgreSQL
|
|
DATABASE_URL), which neither needs nor understands these pragmas.
|
|
"""
|
|
if target_engine.dialect.name != "sqlite":
|
|
return
|
|
|
|
@event.listens_for(target_engine.sync_engine, "connect")
|
|
def _set_sqlite_pragmas(dbapi_connection, connection_record) -> None:
|
|
cursor = dbapi_connection.cursor()
|
|
try:
|
|
cursor.execute("PRAGMA journal_mode=WAL")
|
|
cursor.execute("PRAGMA synchronous=NORMAL")
|
|
cursor.execute(f"PRAGMA busy_timeout={_SQLITE_BUSY_TIMEOUT_MS}")
|
|
finally:
|
|
cursor.close()
|
|
|
|
|
|
engine = create_async_engine(settings.database_url)
|
|
_register_sqlite_pragmas(engine)
|
|
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|