Skip to content

fix(auth): prevent DB connection-pool-closed crash on concurrent login#1071

Merged
bmc08gt merged 2 commits into
code/cashfrom
fix/db-connection-pool-closed-race
Jul 12, 2026
Merged

fix(auth): prevent DB connection-pool-closed crash on concurrent login#1071
bmc08gt merged 2 commits into
code/cashfrom
fix/db-connection-pool-closed-race

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a rare crash: IllegalStateException: Cannot perform this operation because the connection pool has been closed. (Bugsnag 6a51b2b4)

How it happened

A cold start on degraded network races two soft-logins:

  1. App startupFlipcashAppAuthManager.init() begins soft-login fix: reset sheet state on collapse #1.
  2. An FCM push arriving mid-startupNotificationService.onMessageReceivedauthenticateIfNeeded { ... } calls AuthManager.init() again because userManager.accountCluster is still null (login fix: reset sheet state on collapse #1 hadn't established it yet).

Login #2 re-runs FlipcashDatabase.init(), which unconditionally close()d and rebuilt the per-user DB — tearing down the connection pool out from under an active Room query (CashScreenViewModel's balance Flow, already collecting once login #1 reached Ready).

It's rare because it needs a cold start on slow network + a push landing in a ~150ms window + the Cash Flow already collecting.

Changes

  • FlipcashDatabase.init() — now @Synchronized and idempotent: early-returns when an instance for the same DB already exists. Keyed on the DB name, not isOpen() — Room opens the file lazily, so isOpen() is false in exactly the race window (this is why PersistenceProvider.openDatabase's existing isOpen() guard didn't catch it). Account-switch (different entropy) still closes + rebuilds.
  • AuthManager.init() — serialized with a Mutex and short-circuits when already LoggedIn, so the push handler can't launch a second concurrent soft-login.

Tests

  • FlipcashDatabaseInitTest (new): same entropy → same instance kept alive; different entropy → rebuild.
  • AuthManagerTest (+2): already-LoggedIn → no lookup/openDatabase; unauthenticated → soft-login proceeds.

Both affected suites pass locally.

Risk

Low. Account-switch still rebuilds the DB. The auth guard only skips a redundant re-login when already logged in; explicit login() (seed input) and logoutAndSwitchAccount are unaffected.


Update — also fixes the logout variant (SQLITE_MISUSE, Bugsnag 6a52e88b)

A second report of the same defect family surfaced — android.database.SQLException: Error code: 21, message: connection is closed — from the same culprit (CashScreenViewModel's Room flow) but a different trigger: logout instead of login re-init.

On logout, AuthManager.resetStateForUser() called persistence.close() while the just-dismissed Cash screen's stateIn(WhileSubscribed(5000)) flows were still active (up to 5s), so an in-flight Room query hit the closed connection.

Now that FlipcashDatabase.init() is idempotent + @Synchronized, it owns the DB lifecycle, so this commit:

  • resetStateForUser() no longer closes the DB — a same-user re-login reuses the open instance; a different user's login rebuilds it.
  • PersistenceProvider.openDatabase() no longer short-circuits on isOpen(). With the DB now left open after logout, that guard would hand a different user the previous user's database — so it always delegates to the idempotent, name-keyed init().

Adds PersistenceProviderTest (same-user reuse + different-user swap-while-open / account isolation); updates AuthManagerTest to assert the DB is not closed on logout.

A cold start with degraded network could race two soft-logins: app
startup (FlipcashApp.init) and an FCM push handler (NotificationService)
both call AuthManager.init() when no account cluster is established yet.
The second login re-runs FlipcashDatabase.init(), which unconditionally
closed and rebuilt the per-user database — tearing down the connection
pool out from under an active Room query (e.g. CashScreenViewModel's
balance Flow) and throwing:

  IllegalStateException: Cannot perform this operation because the
  connection pool has been closed.

Fixes:
- FlipcashDatabase.init() is now @synchronized and idempotent: it
  early-returns when an instance for the same DB already exists. Keyed
  on the DB name (not isOpen(), which is false while Room lazily opens
  the file — exactly the race window). Account-switch still rebuilds.
- AuthManager.init() is serialized with a Mutex and short-circuits when
  already LoggedIn, so a second concurrent soft-login can't churn the
  DB lifecycle.

Adds FlipcashDatabaseInitTest and AuthManager init-guard tests.
@github-actions github-actions Bot added type: fix Bug fix area: auth Login, session, access keys, identity and removed type: fix Bug fix labels Jul 11, 2026
A second connection-closed crash was reported (SQLException code 21
"connection is closed") from CashScreenViewModel's Room flow — same
family as the connection-pool crash this branch fixes, but a different
trigger: logout instead of login re-init.

On logout, resetStateForUser() called persistence.close() while the
just-dismissed Cash screen's stateIn(WhileSubscribed(5000)) flows were
still active (up to 5s), so an in-flight Room query hit the closed
connection and threw.

Now that FlipcashDatabase.init() is idempotent + @synchronized, it owns
the DB lifecycle end to end, so:

- resetStateForUser() no longer closes the DB. A same-user re-login
  reuses the open instance; a different user's login rebuilds it.
- PersistenceProvider.openDatabase() no longer short-circuits on
  isOpen(): with the DB now left open after logout, that guard would
  hand a different user the previous user's database. It always
  delegates to the (idempotent, name-keyed) init() instead.

Adds PersistenceProviderTest covering same-user reuse and different-user
swap-while-open (account isolation). Updates AuthManagerTest to assert
the DB is not closed on logout.
@github-actions github-actions Bot added the type: fix Bug fix label Jul 12, 2026
@bmc08gt bmc08gt merged commit c8e7d4d into code/cash Jul 12, 2026
3 checks passed
@bmc08gt bmc08gt deleted the fix/db-connection-pool-closed-race branch July 12, 2026 02:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth Login, session, access keys, identity type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant