Repository navigation
Conversation
|
The following comment was made by an LLM, it may be inaccurate: Based on my search, I found one potentially related PR: PR #44558: fix(db): serialize database init and migrations across processes
This is loosely related to the current PR since both handle concurrency in shared SQLite databases, but they solve different problems (migration serialization vs. statement lock retry). The current PR (47567) is the primary fix for runtime statement lock contention with exponential backoff retry logic. No other duplicate PRs found addressing the same sqlite lock timeout retry mechanism. |
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
803cb7a to
ad6219d
Compare
Statements failing with SQLITE_BUSY under concurrent processes now retry with jittered exponential backoff (up to 30s) instead of surfacing LockTimeoutError, which killed prompts and message updates when multiple opencode processes shared one database.
ad6219d to
57f1bda
Compare
Why this mattersMulti-process / multi-client OpenCode hits SQLite EvidenceBaseline on current
Live result: PR tip
Confirms this PR is the leaf. Happy to help land as-is. Architecture (proposal)Keep merge scope = lock-timeout retry only. Durable follow-on: one shared SQLite busy policy (busy_timeout + which errors retry vs |
|
Thanks for verifying the repro on I agree with your follow-up proposal. A single shared busy policy at the effect-SQL client boundary, with a busy_timeout default and an explicit list of which statement errors retry versus |
(cherry picked from anomalyco#47567)
anomalyco#47567's retryLocked wraps every LockTimeoutError, but a held DEFERRED WAL transaction whose read snapshot has been invalidated by another connection's commit returns SQLITE_BUSY_SNAPSHOT on the next write — retrying the same statement will keep failing the same way until a rollback + restart of the whole transaction, so the 30 s budget only delays the inevitable. Only retry in autocommit; inside an explicit transaction surface the error immediately so the caller can decide. Bun's bun:sqlite Database exposes inTransaction; node:sqlite's DatabaseSync exposes isTransaction. Each driver captures its native handle in the make closure and consults the right property in the retry's `while` predicate.
|
@woodsonl one case the retry can't recover from, since you mentioned the error classification is the tricky part. Effect's Repro with a.run("BEGIN") // deferred
a.query("SELECT v FROM t WHERE id = 1").get() // takes a read snapshot
b.run("UPDATE t SET v = v + 1 WHERE id = 1") // b commits
a.run("UPDATE t SET v = v + 10 WHERE id = 1") // SQLITE_BUSY_SNAPSHOT (517), 0 ms
a.run("UPDATE t SET v = v + 10 WHERE id = 1") // SQLITE_BUSY_SNAPSHOT again, 0 ms
a.run("ROLLBACK")
a.run("BEGIN")
a.run("UPDATE t SET v = v + 10 WHERE id = 1") // okI'm carrying a fix for this on my fork: 54f5929. |
#47567's retryLocked wraps every LockTimeoutError, but a held DEFERRED WAL transaction whose read snapshot has been invalidated by another connection's commit returns SQLITE_BUSY_SNAPSHOT on the next write — retrying the same statement will keep failing the same way until a rollback + restart of the whole transaction, so the 30 s budget only delays the inevitable. Only retry in autocommit; inside an explicit transaction surface the error immediately so the caller can decide. Bun's bun:sqlite Database exposes inTransaction; node:sqlite's DatabaseSync exposes isTransaction. Each driver captures its native handle in the make closure and consults the right property in the retry's `while` predicate.
|
Thanks for the detailed repro. I checked I also agree with surfacing plain I cherry-picked 54f5929 (4291616) with your authorship intact. The new test passes, and the core suite is green with typecheck clean. Thanks for including the test as well; the deferred-transaction setup is clean. |
|
This branch has been upgraded to V2. We're no longer taking PRs for V1. If you think this is still relevant, please port it over and open it for V2. — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊 |
When multiple opencode processes share one database, statements that cannot grab the SQLite write lock within busy_timeout fail with database is locked and kill the running prompt. The statement only needs the lock, so retry autocommit statements classified as LockTimeoutError (SQLITE_BUSY/SQLITE_LOCKED) with jittered exponential backoff (50 ms doubling to a 250 ms cap, 30 s total) in the bun and node sqlite drivers. The async sleep between attempts lets the competing process finish its write. Only autocommit statements are retried. Inside a transaction the same error repeats until the transaction is rolled back and restarted (SQLITE_BUSY_SNAPSHOT on a deferred transaction whose read snapshot went stale), so it surfaces at once for the caller to handle. Ports anomalyco#47567 to the V2 tree. The in-transaction fail-fast and its test originate from iceteaSA's work on that PR.
Issue for this PR
Closes #47566
Type of change
What does this PR do?
When multiple opencode processes share one database, statements that can't grab the SQLite write lock within
busy_timeoutfail withdatabase is locked. Callers treat that as fatal (Effect.orDie), so a competing process's write kills an unrelated prompt.The statement is fine — it just needs the lock. This PR retries statements whose failure is classified as
LockTimeoutError(SQLITE_BUSY/SQLITE_LOCKED) with jittered exponential backoff (50ms → 250ms cap, 30s total), in both the bun and node sqlite drivers. The async sleep between attempts yields the thread so the other process's write can finish.How did you verify your code works?
packages/core/test/sqlite-lock-retry.test.ts: opens a second connection holdingBEGIN IMMEDIATE, setsbusy_timeout = 0on the client, then updates through the effect client while the lock is held and releases it 100ms later. Asserts the update lands. Fails ondev(lock error propagates), passes here.core and opencode package suites pass (
OPENCODE_CLIENT=cli);bun turbo typecheckclean; lint unchanged from dev steady state (one pre-existing octal-escape error in session-ui, same count on base).Checklist