fix(database): share one SQLite writer per owner and take write locks up front - #748
Open
bmc08gt wants to merge 8 commits into
Open
fix(database): share one SQLite writer per owner and take write locks up front#748bmc08gt wants to merge 8 commits into
bmc08gt wants to merge 8 commits into
Conversation
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.
Bugsnag 6a4fde3, "Failed to persist conversation state [replace-feed]" with
database is locked (code: 5). The event's log stream shows one cold launch runningcompleteLoginfive times for the same owner. Each call built aSessionContainerwith its ownDatabase, so several writer connections shared one SQLite file and their history syncs raced.replaceConversationFeedruns a transaction that reads the doomed conversation ids before deleting them. UnderBEGIN DEFERREDthat first read takes a WAL snapshot, and when the delete then needs the write lock while another connection holds it, SQLite returnsSQLITE_BUSYat once because upgrading a snapshot cannot wait. The busy handler never runs. On top of that, SQLite.swift'sbusyTimeoutis in seconds, so the existing2000armed a 33-minute wait rather than 2 seconds on the paths where the handler does run.Changes:
Containerowns a newDatabaseStorethat opens oneDatabaseper owner and hands the same instance back on every later request.SessionAuthenticator.createSessionContainerreads from it; the oldinitializeDatabaseandcreateApplicationSupportIfNeededhelpers move into the store unchanged.replaceConversationFeed,persistMessages, and the sharedDatabase.transactionhelper runBEGIN IMMEDIATE, so a read-then-write transaction takes the write lock first and waits on the busy handler instead of failing.busyTimeoutis2on both connections.Regression_6a4fde3suite. The rival-writer test holds an immediate transaction on a second connection to the same file for 200 ms and asserts the feed replacement lands; onmainit fails in under 10 ms with the production message.This matches what Android already does:
FlipcashDatabase.init()is a synchronized singleton that returns the existing instance for the same database name, and Room'sbeginTransactionisBEGIN EXCLUSIVE, so its read-then-write DAO transactions never hit the deferred snapshot upgrade.Two things this does not resolve. Why one launch called
completeLoginfive times is still unknown, which is what the info log is for. Andlogout()still does not tear down the previousSessionContainer, so an in-flight write from a stale container now lands on the shared live store rather than failing; that is pre-existing and left for a separate change.Triage brief and plan are in
.claude/plans/2026-09-09-bugsnag-6a4fde3*.md.