Summary
During an account-switch flow (sign out, then sign in with a different user), we call disconnectAndClear() followed by connect() with a fresh connector.
On one real-device occurrence, connect() never started the sync client: no Starting Rust sync iteration log, no error, no isolate exit — total silence. Every subsequent disconnect() / connect() call (triggered by further auth transitions) also produced no sync activity, consistent with them queueing forever on syncConnectMutex behind the stuck connect().
Only killing and restarting the app recovered. The same flow works fine most of the time — this looks like a race.
Environment
powersync 2.2.0 (Flutter, Rust sync client, Sync Streams)
- Android, real device (Samsung, arm64), debug build
- Supabase auth/backend,
PowerSyncBackendConnector recreated on each connect()
- Attachment queue (
@experimental attachments API) active with 62 synced attachments, periodic sync every 30 s
- We diffed
connection_manager.dart / native_powersync_database.dart between 2.2.0 and 2.3.2: the connect/disconnect/mutex mechanics are
identical, so we believe 2.3.2 is affected as well.
Timeline (from logcat, single app process)
Context: for ~20 minutes before the incident the device had no DNS (Failed host lookup), with the Rust client retrying every 5 s — the sync recovered right before the account switch (checkpoint applied).
21:32:03 Starting Rust sync iteration
21:32:08 Validated and applied checkpoint (old account, recovery after DNS outage)
21:32:10 supabase signedOut -> our code calls db.disconnect()
21:32:10 Ending Rust sync iteration. Immediate restart: false
21:32:11 Sync Isolate exit
21:32:30 supabase signedIn (DIFFERENT user id) -> our code calls:
await db.disconnectAndClear(); // completes: local db cleared
await db.connect(connector: fresh); // <- never starts the sync client
21:32:30 Found 0 active attachments (attachment queue reacting to the cleared table)
—— from here on: NOTHING from the sync client, forever ——
21:45:16 signedOut -> db.disconnect() // no "Ending ...", no "Isolate exit": hangs silently
21:45:38 signedIn (same user) -> disconnectAndClear() + connect() // silent too
The attachment queue's periodic timer kept running normally the whole time (so the main isolate event loop was healthy). db.currentStatus stayed
frozen at the state set by disconnectAndClear() (connected: false, connecting: false, hasSynced: false) — that is how our UI-level watchdog
now detects the condition.
What we could rule out
disconnectAndClear() completed (the cleared attachments table triggered watch queries; our code after it ran — the new owner id was persisted).
- The previous sync isolate exited cleanly 19 s before the failing
connect() (Sync Isolate exit logged at 21:32:11).
- No exception surfaced from
connect() into our try/catch around the auth-transition handler (or it was swallowed — see next point): nothing
was reported through our error pipeline either.
- The app was NOT hot-restarted between those events (single process).
Candidate spots we noticed while reading 2.2.0 sources
We could not pin down the exact mechanism, but while reading connection_manager.dart / native_powersync_database.dart / sync_isolate_protocol.dart we noticed a few places that could produce a silent never-completing connect():
ConnectionManager.connect() holds syncConnectMutex across connectInternal(), which awaits hasInitPort.future. If the freshly spawned isolate never sends init, connect() never returns and every later connect()/disconnect() queues on the mutex forever — matching the observed "everything silent from this point on".
_abortCurrentSync()'s else branch (disconnector.aborted == true) awaits onCompletion but does not reset _abortActiveSync to null; the following assert(_abortActiveSync == null) in connect() would throw in debug builds (an AssertionError escaping through the caller rather than a clean state).
_RemoteMutex.lock() ignores its abortTrigger parameter, so a sync isolate stuck acquiring the remote sync/crud mutex cannot be aborted: openedStreamingSync?.abort() would hang in shutdown(), the isolate never exits, and disconnect() then hangs in waitForShutdown() while holding syncConnectMutex.
- Mutex grant/release messages (
receiveMessages) and the exit notification (receiveExit) arrive on different ports with no ordering guarantee; handleChildIsolateExit() completing held grants relies on those orderings being benign.
Given the DNS-outage recovery, an abort 2 s after a checkpoint, and an attachment queue writing concurrently, a race between those pieces seems
plausible.
Repro
Not deterministic — the same account-switch flow passed twice under instrumentation the previous day. Happy to run an instrumented build or
provide the full logcat if that helps.
Workaround we ship meanwhile
App-level watchdog: after asking for a reconnection, if the status stream stays frozen with no sign of life (connected/connecting/downloading/
errors all absent) for 20 s while the first sync is pending, we tell the user to restart the app.
Summary
During an account-switch flow (sign out, then sign in with a different user), we call
disconnectAndClear()followed byconnect()with a fresh connector.On one real-device occurrence,
connect()never started the sync client: noStarting Rust sync iterationlog, no error, no isolate exit — total silence. Every subsequentdisconnect()/connect()call (triggered by further auth transitions) also produced no sync activity, consistent with them queueing forever onsyncConnectMutexbehind the stuckconnect().Only killing and restarting the app recovered. The same flow works fine most of the time — this looks like a race.
Environment
powersync2.2.0 (Flutter, Rust sync client, Sync Streams)PowerSyncBackendConnectorrecreated on eachconnect()@experimentalattachments API) active with 62 synced attachments, periodic sync every 30 sconnection_manager.dart/native_powersync_database.dartbetween 2.2.0 and 2.3.2: the connect/disconnect/mutex mechanics areidentical, so we believe 2.3.2 is affected as well.
Timeline (from logcat, single app process)
Context: for ~20 minutes before the incident the device had no DNS (
Failed host lookup), with the Rust client retrying every 5 s — the sync recovered right before the account switch (checkpoint applied).The attachment queue's periodic timer kept running normally the whole time (so the main isolate event loop was healthy).
db.currentStatusstayedfrozen at the state set by
disconnectAndClear()(connected: false,connecting: false,hasSynced: false) — that is how our UI-level watchdognow detects the condition.
What we could rule out
disconnectAndClear()completed (the cleared attachments table triggered watch queries; our code after it ran — the new owner id was persisted).connect()(Sync Isolate exitlogged at 21:32:11).connect()into ourtry/catcharound the auth-transition handler (or it was swallowed — see next point): nothingwas reported through our error pipeline either.
Candidate spots we noticed while reading 2.2.0 sources
We could not pin down the exact mechanism, but while reading
connection_manager.dart/native_powersync_database.dart/sync_isolate_protocol.dartwe noticed a few places that could produce a silent never-completingconnect():ConnectionManager.connect()holdssyncConnectMutexacrossconnectInternal(), which awaitshasInitPort.future. If the freshly spawned isolate never sendsinit,connect()never returns and every laterconnect()/disconnect()queues on the mutex forever — matching the observed "everything silent from this point on"._abortCurrentSync()'selsebranch (disconnector.aborted == true) awaitsonCompletionbut does not reset_abortActiveSyncto null; the followingassert(_abortActiveSync == null)inconnect()would throw in debug builds (anAssertionErrorescaping through the caller rather than a clean state)._RemoteMutex.lock()ignores itsabortTriggerparameter, so a sync isolate stuck acquiring the remotesync/crudmutex cannot be aborted:openedStreamingSync?.abort()would hang inshutdown(), the isolate never exits, anddisconnect()then hangs inwaitForShutdown()while holdingsyncConnectMutex.receiveMessages) and the exit notification (receiveExit) arrive on different ports with no ordering guarantee;handleChildIsolateExit()completing held grants relies on those orderings being benign.Given the DNS-outage recovery, an abort 2 s after a checkpoint, and an attachment queue writing concurrently, a race between those pieces seems
plausible.
Repro
Not deterministic — the same account-switch flow passed twice under instrumentation the previous day. Happy to run an instrumented build or
provide the full logcat if that helps.
Workaround we ship meanwhile
App-level watchdog: after asking for a reconnection, if the status stream stays frozen with no sign of life (
connected/connecting/downloading/errors all absent) for 20 s while the first sync is pending, we tell the user to restart the app.