Repository navigation
sqlitedb: recover a poisoned handle inside a write operation by replaying it - #93
Merged
Merged
Conversation
…ying it Closes the last deviation recorded for DID-matlab da83fa3. MATLAB reopens the connection before retrying a READONLY statement, because a poisoned handle (SQLITE_READONLY_DBMOVED) is cleared by reopening and never by waiting. mksqlite autocommits, so there the failed statement is all there is to re-run. In SQLiteDB it is part of a transaction whose earlier statements die with the handle. The four write operations (_do_add_branch, _do_add_doc, _do_remove_doc, _do_delete_branch) now run through a journaling cursor. It records each statement's SQL, parameters, rowcount, an INSERT's lastrowid and every row read. On a READONLY at a statement or at COMMIT, _recover_transaction rolls back the dead transaction, waits on the same backoff schedule, reopens and replays the journal. Each statement is checked against what it did the first time before the failed statement runs or the transaction commits. Only SQL is replayed: files already copied, or deleted with delete_original, are not needed again. A replay that differs (another writer took the next doc_idx, a different file is at the path, a statement fails) is refused, nothing is written, and the original error is raised. The tests reproduce a real DBMOVED by replacing the database file under the open connection. Doing that showed SQLite reports it at the first write of a transaction rather than at COMMIT, so a COMMIT-only recovery would have missed the real case. It also showed that a swap after the first write is not detected at all, which no retry can see. Adding documents is about 4% slower (1,500 documents: 6.1 s -> 6.4 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Dn4gCNCU2jUGdmfPVCAGK
This was referenced Oct 24, 2026
Merged
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.
Closes the last deviation #90 recorded for DID-matlab da83fa3: "a genuinely poisoned handle (DBMOVED) is not recovered at COMMIT".
Why it isn't a one-line port
MATLAB reopens the connection before retrying a READONLY statement, because a poisoned handle (
SQLITE_READONLY_DBMOVED) clears on reopen and never by waiting. mksqlite commits every statement on its own, so the failed statement is all MATLAB has to re-run.SQLiteDBruns each add, remove or branch operation as one transaction, and a poisoned handle loses the transaction's earlier statements with it.Re-running the whole operation isn't safe:
_do_add_doccopies ingested files, and withdelete_originaldeletes the source, before it commits, so a second run would fail on the missing source. What is safe to re-run is the operation's SQL.The change
_do_add_branch,_do_add_doc,_do_remove_doc,_do_delete_branch) now run through a recording cursor (_JournalCursor, under@_journaled). For each statement it records:lastrowid, since later statements are built from it;_recover_transaction:doc_idx, a different file is now at the path, or a statement fails on replay. Nothing is written, and the caller gets the original error. A divergence isn't retried.do_run_sql_querykeeps its own retry for statements outside a write operation._rollback()), so the caller sees the real error rather than anAttributeError.Found while porting
Cost
Adding 1,500 documents takes 6.4 s, against 6.1 s on
main, about 4% slower. The recorded statements are kept for one operation and dropped at commit.Tests (
tests/test_sqlitedb_transient_errors.py, 13 new)add_docs,add_branchandremove_docs. The recording ends with the operation, so the next one replays only its own statements.delete_originalisn't needed again, and the document's file still opens afterwards;Locally:
black --check,ruff check(0.16.5),pytest(794 passed), andcheck_bridge_coverage.py(0 problems).Generated by Claude Code