fix: serialise the wear queue drain process-wide, stop sync cancelling itself#38
Merged
Merged
Conversation
…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
enabled auto-merge
July 15, 2026 02:46
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.
Reliability follow-up to the security PR. Two offline-switch duplication windows, both confirmed.
Wear: the queue-drain lock was per-instance
WearStore.queueDrainMutexwas a plain instance field. But three sites each construct their ownWearStoreover the same SharedPreferences-backed queue:
MainActivityWearDataLayerService(phone nudge -> refresh)QuickSwitchTrampolineActivitySo the mutex only serialised drains within one instance. Two instances could each snapshot the queue, and
each call
createFrontfor the same row before either removed it -> the front is created twice. This is thesame bug shape that
WearApiClient.refreshMutexwas hoisted to a companion object to fix;queueDrainMutexjust never got the same treatment. Now it lives in
WearStores companion object, shared process-wide.Phone: REPLACE could cancel a drain mid-createFront
SyncWorker.scheduleusedExistingWorkPolicy.REPLACE. The drain postscreateFrontbefore deleting thequeue row (the create-before-delete window the code already documents), so a
REPLACEthat cancels arunning 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 addedafter the running drain snapshotted the queue are picked up by the next
schedule()call, and every offlineaction 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-KeyoncreateFront(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:testPlayReleaseUnitTestgreen.check by making rapid offline switches on the watch and on the phone and confirming no duplicate fronts
after reconnect.