Repository navigation
Conversation
database/sql begins SQLite transactions deferred. Under WAL, a deferred transaction that reads and then writes can't upgrade to a write transaction once another connection has committed, and SQLite returns SQLITE_BUSY at once without waiting for `busy_timeout`. The job scheduler's transaction reads eligible jobs before updating them, so on a pool of more than one connection it can fail intermittently. Add `BeginReadThenWrite` to the executor. The SQLite driver begins it with `BEGIN IMMEDIATE`, taking the write lock up front where `busy_timeout` applies; the Postgres drivers treat it as `Begin`. The job scheduler begins its transaction with it, and `JobSchedule` uses it when it begins its own. River's other transactions write first and stay deferred, so they don't hold the write lock across reads. database/sql can't choose the kind of `BEGIN`, and `_txlock` is a DSN option of some drivers that Turso doesn't support, so the immediate transaction replaces the deferred one on River's dedicated connection. Add concurrent read-then-write and job scheduling tests to the shared driver tests and run SQLite, libSQL, and Turso tests on a pool of four connections with a busy timeout. With `BeginReadThenWrite` deferred, 135 of 160 and 43 of 160 of their operations fail with SQLITE_BUSY. A pool of one connection remains the recommended configuration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
csquared
force-pushed
the
sqlite-begin-immediate
branch
from
November 11, 2026 15:33
fe7c9d2 to
1b5fb09
Compare
Contributor
|
TY Chris! Would you mind signing our CLA please: https://github.com/riverqueue/rivercla |
… paths Mutation testing the previous commit's changed lines found gaps the existing tests let through: - Changing the job scheduler back to `Begin` passed every test. Add `TestJobScheduler/BeginsReadThenWrite`, which runs the scheduler on an executor whose `Begin` fails, so it must use `BeginReadThenWrite`. - Removing either `tx.Rollback()` in `beginImmediate` after a failed `ROLLBACK` or `BEGIN IMMEDIATE` hung the connection's discard forever. Add `TestExecutorBegin/FailedBeginImmediateDiscardsConnection` for both. - Setting the test pool back to one connection passed the concurrency tests without any contention. `ConcurrentReadThenWrite` now first checks that two transactions can be open at once. - Add `InSubtransactionBeginsSubtransaction`. `ConcurrentReadThenWrite` also flaked on a loaded host: with eight workers queued for the write lock, SQLite doesn't serve waiters in order, and one in 160 operations could wait past the test pool's 5s `busy_timeout`. Under the same load (one -race process, CPUs oversubscribed, load ~24) it failed 6 of 100 runs at 5000ms and 0 of 100 at 30000ms, so the test pool's `busy_timeout` is now 30s. It bounds how long a waiter may wait, not how long the tests take when nothing contends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
|
Added a second commit with tests from mutation testing the first one's changed lines: the scheduler's use of 🤖 Generated with Claude Code |
Author
|
For sure!
Also doing a few hundred rounds of mutation tests to convince myself this
isn't a hallucination :)
…On Wed, Nov 11, 2026 at 10:52 AM Brandur ***@***.***> wrote:
*brandur* left a comment (riverqueue/river#1597)
<#1597 (comment)>
TY Chris! Would you mind signing our CLA please:
https://github.com/riverqueue/rivercla
—
Reply to this email directly, view it on GitHub
<#1597?email_source=notifications&email_token=AABKLENQZGQN6RVIQS45CET5YSKNJA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTMOBQGE4DKMRUGM2KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-6801852434>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABKLEMB5P26ODYIBWV3RC35YSKNJAVCNFSNUABFKJSXA33TNF2G64TZHM3TCNRSGI4DONZSHNEXG43VMU5TMMZTGE3TKMRSGE32C5QC>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/AABKLEPKWMAYLYNK6J2EZFL5YSKNJA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTMOBQGE4DKMRUGM2KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/AABKLENI5TNP7T22MTAIM4T5YSKNJA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTMOBQGE4DKMRUGM2KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
Contributor
|
@csquared Awesome. Just lemme know when you're finished up (and sign that CLA) and I'll take a look! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With the SQLite driver on a pool of more than one connection, the job scheduler can fail intermittently with
database is locked (5) (SQLITE_BUSY), even withbusy_timeoutset.database/sql begins SQLite transactions deferred. Under WAL, a deferred transaction that reads and then writes can't upgrade to a write transaction once another connection has committed in between, and SQLite returns SQLITE_BUSY at once instead of waiting for
busy_timeout. The scheduler's transaction reads eligible jobs before updating them, so it hits exactly this.Change
BeginReadThenWriteon the executor, for a transaction that reads before it writes. The SQLite driver begins it withBEGIN IMMEDIATE, taking the write lock up front wherebusy_timeoutapplies. The Postgres drivers treat it asBegin.JobScheduleuses it when it begins its own.BEGIN, and_txlockis a DSN option of only some drivers (Turso doesn't support it), so the immediate transaction replaces the deferred one on River's dedicated connection.A pool of one connection is still the recommended configuration; this makes larger pools wait for the write lock instead of failing.
Tests
ConcurrentReadThenWritefirst checks the pool really allows two open transactions, so it can't pass without contention.BeginReadThenWritechanged to begin deferred, 135 of 160 and 43 of 160 of those operations fail with SQLITE_BUSY. With it immediate, all pass.TestJobScheduler/BeginsReadThenWriteruns the scheduler on an executor whoseBeginfails, so the scheduler must begin withBeginReadThenWrite.TestExecutorBegin/FailedBeginImmediateDiscardsConnectioncovers a failedROLLBACKand a failedBEGIN IMMEDIATE: the connection is discarded instead of hanging.Begin, dropping eitherRollbackinbeginImmediate, and a one-connection test pool all passed before and fail now.busy_timeoutis now 30s instead of 5s. Under deliberate load (one-raceprocess, CPUs oversubscribed, load ~24),ConcurrentReadThenWritefailed 6 of 100 runs at 5s, each time with one of 160 operations waiting past the timeout, since SQLite doesn't serve waiters in order. It failed 0 of 100 at 30s.-race: the SQLite, libSQL and Turso driver suites,riversqlite,internal/maintenance, and the pgx and database/sql driver suites against Postgres 17.🤖 Generated with Claude Code