- 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>
create_async_engine had no connect_args and there was no PRAGMA
anywhere in the repo. 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
with "database is locked" rather than waiting at all - realistic given
five concurrent background tasks (scheduler, confirmation poller, RBF
bumper, two reconcilers) plus every HTTP handler share one file, and
nothing previously handled that error.
app/db/base.py now registers a "connect" event on the engine that sets
journal_mode=WAL, synchronous=NORMAL and a 5-second busy_timeout on
every new DBAPI connection - applied only when the dialect is sqlite,
so a future PostgreSQL DATABASE_URL is unaffected. WAL lets readers and
writers proceed without blocking each other, and busy_timeout gives a
second writer a real window to wait instead of failing instantly.
Left out: an explicit application-level retry wrapper for "database is
locked" in the background loops, the other half of the proposed fix -
busy_timeout already gives SQLite itself several seconds to resolve
writer-vs-writer contention before ever raising, and every background
loop already catches and logs an unhandled exception before its next
scheduled tick, which is itself a retry, just not an immediate one.
Suite grows from 211 to 214 tests. BUGS.md moves B-39 to Previously
fixed.