Skip to content

Fix the ASGI pool concurrency test racing on WAL conversion - #850

Merged
ajslater merged 1 commit into
developfrom
fix/asgi-db-test-wal-race
Sep 13, 2026
Merged

ajslater merged 1 commit into
developfrom
fix/asgi-db-test-wal-race

Conversation

@ajslater

Copy link
Copy Markdown
Owner

Fixes the intermittent test_pool_bounds_concurrency failure that broke the
v2.3.2 CI run on develop.

Not a product bug

The traceback points at Django's get_new_connection, inside the loop that
runs init_command, not at any query:

for init_command in self.init_commands:
    if init_command := init_command.strip():
>       conn.execute(init_command)
E       sqlite3.OperationalError: database is locked

The first pragma is journal_mode=wal. Converting a file into WAL takes an
exclusive lock, and SQLite answers with SQLITE_BUSY straight away rather
than waiting out the configured 120 s busy timeout. The alias fixture hands
the pool a database file nothing has ever opened, so the two workers convert
it concurrently and one loses.

Isolated repro, two threads opening one fresh file with the codex pragmas and
a 120 s timeout:

file state races failed
brand new 22 / 60
already in WAL 0 / 60

The test itself failed 3 times in 8 local runs before this change.

The database codex serves is never in that state. codex_startup runs
migrate on a single connection before run() starts the librarian or the
server, so the file is in WAL before a second connection exists, and asserting
WAL on a WAL file needs no exclusive lock.

The fix

Create the fixture's file in WAL, which is the state it was always meant to
mirror. One sqlite3.connect plus one pragma, before the alias is yielded.

Verified: 30 consecutive runs of test_pool_bounds_concurrency clean, 5
consecutive runs of the whole file clean, 1123 pytest and 500 vitest passing,
make lint and make ty clean.

🤖 Generated with Claude Code

test_pool_bounds_concurrency failed intermittently with "database is
locked", raised from the connection's init_command rather than from any
query. The pragmas include journal_mode=wal, and the fixture handed the
pool a database file that had never been opened. Converting a file into
WAL takes an exclusive lock, and SQLite answers that with SQLITE_BUSY
immediately instead of waiting out the 120 second busy timeout, so the
two pool workers opening that new file at the same instant raced and one
lost. Reproduced at 22 failures in 60 attempts in isolation, and 3 in 8
runs of the test itself.

The database codex serves is never in that state: startup migrates it on
one connection well before the worker pool or the librarian exists, and
asserting WAL on a file already in WAL needs no exclusive lock. So put
the fixture's file in WAL when it is created, which is the state it was
always meant to mirror. 30 runs of the test, clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ajslater
ajslater merged commit f92bbf0 into develop Sep 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant