fix: do not block statement cleanup on a busy connection - #361
warmwaffles wants to merge 3 commits into
Conversation
statement_type_destructor took the connection mutex and called sqlite3_finalize. A connection in the busy handler holds that mutex, and SQLite's db mutex, until busy_timeout expires. Cleanup then stalled the process that held the write lock, so the waiting BEGIN IMMEDIATE failed with "database is locked". The destructor now finalizes only when the connection mutex is free. Otherwise it queues the statement. The next locked call, or connection close, finalizes it. Fixes: #360
|
Linter is failing, I'll fix that in a bit. Just want to validate that this resolves the issue. |
|
Excited about this getting in! |
|
Tested
Before the fix, 16 of 25 runs on 0.34.0–0.40.0 stalled for about 2.9 s and the waiter failed with The resource-lifetime path appears sound: the queue node is allocated during prepare; destruction either obtains the connection mutex or enqueues the statement; each statement retains the connection resource until that is complete; and connection destruction drains before Two remaining edge cases:
I have not measured the second case. This investigation (reproduction runs, source review, and this write-up) was done with Claude Opus 5.5 and GPT-5.6 Sol (via Codex). |
|
Awesome news. I'll take a look into the two remaining points and see what's possible. |
`release/2` still took the owning connection mutex. A caller holding the write lock could stall a dirty scheduler for the whole `busy_timeout`, the same failure as a destructor wait. A statement queued during that wait was also left until the next locked call. If it had been stepped, it kept a WAL read mark after the busy call returned. Release now queues instead of waiting, and the unlock path finalizes queued statements before the mutex is free. Fixes: #360
Credo allows two levels of nesting. The poll's cond contained an if, which failed mix lint.
|
@andreasronge I believe this should address all of your points above. |
|
Thanks, and sorry, just being a meat proxy here for my AIs :) This covers both points. On Two minor things from
Reviewed with Claude Opus 5.5 and GPT-6.1 Sol (via Codex). |
|
Tested 329a94f locally: 180s run, 10 connections, 20 short-lived writer processes, I didn't see any stalls. For comparison, I did attempt to get some idea of memory use with I also pushed the fix to a test device (aarch64) and can report back after it has had some time to run. |
statement_type_destructortook the connection mutex and calledsqlite3_finalize. A connection in the busy handler holds that mutex, and SQLite's db mutex, untilbusy_timeoutexpires. Cleanup then stalled the process that held the write lock, so the waitingBEGIN IMMEDIATEfailed with "database is locked".The destructor now finalizes only when the connection mutex is free. Otherwise it queues the statement. The next locked call, or connection close, finalizes it.
Grok 4.7 assisted with creating this fix. Most difficult part was making a test that demonstrated the failure repeatedly.
Fixes: #360