Skip to content

fix: serialise the wear queue drain process-wide, stop sync cancelling itself#38

Merged
SiteRelEnby merged 2 commits into
mainfrom
fix/wear-drain-mutex-and-sync-policy
Jul 15, 2026
Merged

fix: serialise the wear queue drain process-wide, stop sync cancelling itself#38
SiteRelEnby merged 2 commits into
mainfrom
fix/wear-drain-mutex-and-sync-policy

Conversation

@SiteRelEnby

Copy link
Copy Markdown
Collaborator

Reliability follow-up to the security PR. Two offline-switch duplication windows, both confirmed.

Wear: the queue-drain lock was per-instance

WearStore.queueDrainMutex was a plain instance field. But three sites each construct their own WearStore
over the same SharedPreferences-backed queue:

  • MainActivity
  • WearDataLayerService (phone nudge -> refresh)
  • QuickSwitchTrampolineActivity

So the mutex only serialised drains within one instance. Two instances could each snapshot the queue, and
each call createFront for the same row before either removed it -> the front is created twice. This is the
same bug shape that WearApiClient.refreshMutex was hoisted to a companion object to fix; queueDrainMutex
just never got the same treatment. Now it lives in WearStores companion object, shared process-wide.

Phone: REPLACE could cancel a drain mid-createFront

SyncWorker.schedule used ExistingWorkPolicy.REPLACE. The drain posts createFront before deleting the
queue row (the create-before-delete window the code already documents), so a REPLACE that cancels a
running drain right then can leave the front created on the server with the row still queued -> replayed and
duplicated on the next run.

Switched to KEEP: an enqueue while a drain is in flight is dropped instead of cancelling it. Rows added
after the running drain snapshotted the queue are picked up by the next schedule() call, and every offline
action plus every reconnect calls it, so the tail latency is small. This trades a little latency for not
double-creating fronts. True exactly-once still needs a server-side Idempotency-Key on createFront
(already flagged in doWork); this closes the client-side half.

Not fixed here

The phone handoff from the watch still trusts the BLE send-Task with no application-level ack and no uuid
dedup on the phone. That is a protocol change (the phone would need to dedup on the switch uuid it already
receives but currently drops) and is worth its own PR.

Verification

  • :app:assemblePlayRelease :wear:assembleRelease :app:testPlayReleaseUnitTest :wear:testPlayReleaseUnitTest green.
  • Hard to unit-test meaningfully (both are concurrency/WorkManager-scheduling behaviour); worth a device
    check by making rapid offline switches on the watch and on the phone and confirming no duplicate fronts
    after reconnect.

…ancelling itself

Two duplication windows in the offline-switch path.

Wear: queueDrainMutex was a per-instance field, but the activity, the Data Layer
service and the quick-switch trampoline each construct their own WearStore over
one shared SharedPreferences queue. So two of them could snapshot and re-submit
the same queued row before either removed it, double-creating a front. Hoisted
the mutex into the companion object so it is shared across every WearStore in the
process, mirroring WearApiClient.refreshMutex.

Phone: SyncWorker.schedule used ExistingWorkPolicy.REPLACE. createFront is posted
before its queue row is deleted, so a REPLACE that cancels a drain mid-createFront
is exactly the window that can duplicate a front. Switched to KEEP: an enqueue
while a drain is running is dropped instead of cancelling it, and rows added after
that run snapshotted are picked up by the next schedule() (every offline action
and every reconnect calls it). True exactly-once still needs a backend
Idempotency-Key, already noted in doWork.
@SiteRelEnby
SiteRelEnby enabled auto-merge July 15, 2026 02:46
@SiteRelEnby
SiteRelEnby merged commit 4dcf017 into main Jul 15, 2026
1 check passed
@SiteRelEnby
SiteRelEnby deleted the fix/wear-drain-mutex-and-sync-policy branch July 15, 2026 02:51
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