🐛 allocate v2 exchange generations before async dispatch and make verifier acquisition atomic - #4693
Merged
Merged
Conversation
…ifier acquisition atomic
…m-up cancel comment
guiyanakuang
force-pushed
the
fix/issue-4692-queued-warmup-cancel-race
branch
from
July 30, 2026 13:45
b48fb0f to
15fd436
Compare
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.
Closes #4692. Stacked on #4690 (base is its branch; retargets to
mainonce #4690 merges).Changes
1. Generation allocation moved ahead of the async event boundary.
SyncManager.exchangeKeysForPairing(appInstanceId): the warm-up's generation is allocated and recorded in the ledger synchronously at the UI call site, before the event enters the resolver queue;SyncEvent.ExchangeKeysForPairing,SyncHandler, andSyncDeviceManagercarry it through.SyncDeviceManager.exchangeKeysForPairingsends only while the ledger still holds that exact generation: a dialog dismissed while the warm-up event was still queued now captures the generation, its cancel consumes the ledger entry first, and the late warm-up sends nothing. This closes the last residual window from 🐛 release the responder's pending v2 exchange when the initiator abandons pairing #4690 (previously the cancel found an empty ledger and the orphaned responder entry had to self-heal).2. Atomic verifier acquisition.
AppTokenApi.acquireVerifier(appInstanceId, showToken)replaces the hand-composedaddPendingVerifier+startRefreshpair. Acquisition is idempotent per verifier: repeated acquisition (v1 dialog reopened, repeated/sync/showToken) reopens the overlay without incrementing the refresh count twice — fixing the historical v1 accumulation where reopening the dialog n times leaked n−1 counts past the confirm.3. Lock-free CAS ledger.
PendingExchangeLedgernow keeps its per-peer map in an atomicfu atomic reference;consumeis a true compare-and-set loop, so a stale consume racing a replacementrecordfor the same peer can never drop the newer generation. Covered by a 1000-round concurrent stress test.4. Extension parity.
buildKeyExchangeRequestemits strictly monotonic timestamps (maxOf(now, prev + 1)), so same-millisecond attempts can no longer collide as generations.exchangeV2cancels its exact signed generation when the exchange fails after the request may have reached the desktop (lost or invalid response), before surfacing the original failure.Tests
GeneralSyncManagerTest: warm-up generation is recorded synchronously before dispatch.SyncDeviceManagerTest: a warm-up whose generation was already consumed (cancelled) or superseded sends no request; send/skip matrix updated to the carried-generation signature.PendingExchangeLedgerTest: concurrent stale-consume vs. replacement-record stress test (1000 rounds); consume drops the record.AppTokenServiceTest: repeated acquisition by one verifier owns exactly one count; release-after-acquire ordering; hideToken leaves counts owned by others untouched.sync-v2v3.test.ts):exchangeV2auto-cancels its exact generation on a tampered response and on a lost response; 32 concurrentbuildKeyExchangeRequestcalls produce 32 distinct generations.app:desktopTest, web vitest (102 tests), andtsc --noEmitpass; core JS library rebuilt and consumed by the web tests.