Skip to content

fix(database): share one SQLite writer per owner and take write locks up front - #748

Open
bmc08gt wants to merge 8 commits into
mainfrom
fix/database-per-owner
Open

fix(database): share one SQLite writer per owner and take write locks up front#748
bmc08gt wants to merge 8 commits into
mainfrom
fix/database-per-owner

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Bugsnag 6a4fde3, "Failed to persist conversation state [replace-feed]" with database is locked (code: 5). The event's log stream shows one cold launch running completeLogin five times for the same owner. Each call built a SessionContainer with its own Database, so several writer connections shared one SQLite file and their history syncs raced.

replaceConversationFeed runs a transaction that reads the doomed conversation ids before deleting them. Under BEGIN DEFERRED that first read takes a WAL snapshot, and when the delete then needs the write lock while another connection holds it, SQLite returns SQLITE_BUSY at once because upgrading a snapshot cannot wait. The busy handler never runs. On top of that, SQLite.swift's busyTimeout is in seconds, so the existing 2000 armed a 33-minute wait rather than 2 seconds on the paths where the handler does run.

Changes:

  • Container owns a new DatabaseStore that opens one Database per owner and hands the same instance back on every later request. SessionAuthenticator.createSessionContainer reads from it; the old initializeDatabase and createApplicationSupportIfNeeded helpers move into the store unchanged.
  • replaceConversationFeed, persistMessages, and the shared Database.transaction helper run BEGIN IMMEDIATE, so a read-then-write transaction takes the write lock first and waits on the busy handler instead of failing.
  • busyTimeout is 2 on both connections.
  • The two login-path logs move from debug to info with the attempt count in metadata, so the next release report shows how many times login ran.
  • New Regression_6a4fde3 suite. 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; on main it 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's beginTransaction is BEGIN EXCLUSIVE, so its read-then-write DAO transactions never hit the deferred snapshot upgrade.

Two things this does not resolve. Why one launch called completeLogin five times is still unknown, which is what the info log is for. And logout() still does not tear down the previous SessionContainer, 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.

@bmc08gt bmc08gt self-assigned this Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant