From a68cbb02246ec3eb62ea7faa50997d740984f32c Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 6 Jun 2026 11:04:51 +0530 Subject: [PATCH] fix(sidecar): absorb first-leg double-tap collision sequentially --- .../dev/sanderling/sidecar/DriverBackend.kt | 13 +++++++++---- .../sanderling/sidecar/DriverServiceTest.kt | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt index 12703bf..e75ed56 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt @@ -186,9 +186,10 @@ private fun walkForStructuralHash(node: com.fasterxml.jackson.databind.JsonNode, // overlappedDoubleTap fires the second tap while the first is still in // flight, so the on-device gap stays tight on transports with high per-tap -// latency. The overlap can collide with the first tap still executing ("only -// one gesture can be performed at a time"); the second tap then waits the -// first out and lands sequentially instead of failing the step. +// latency. The overlap can collide with the other tap still executing ("only +// one gesture can be performed at a time") on either leg; the colliding leg +// then lands sequentially after the surviving one instead of failing the +// step. internal fun overlappedDoubleTap(tapAction: () -> Unit) { val firstTap = java.util.concurrent.CompletableFuture.runAsync { tapAction() } Thread.sleep(40) @@ -199,7 +200,11 @@ internal fun overlappedDoubleTap(tapAction: () -> Unit) { tapAction() return } - firstTap.join() + try { + firstTap.join() + } catch (_: Throwable) { + tapAction() + } } data class MetricsSample( diff --git a/sidecar/src/test/kotlin/dev/sanderling/sidecar/DriverServiceTest.kt b/sidecar/src/test/kotlin/dev/sanderling/sidecar/DriverServiceTest.kt index 98538de..77f148f 100644 --- a/sidecar/src/test/kotlin/dev/sanderling/sidecar/DriverServiceTest.kt +++ b/sidecar/src/test/kotlin/dev/sanderling/sidecar/DriverServiceTest.kt @@ -201,6 +201,24 @@ class DriverServiceTest { assertEquals(2, invocations.get()) } + @Test fun overlappedDoubleTapRetriesWhenFirstLegCollides() { + val landed = java.util.concurrent.atomic.AtomicInteger(0) + val failedFirst = java.util.concurrent.atomic.AtomicBoolean(false) + // The async first tap loses the race and collides; the second tap + // succeeds. The collision must be absorbed with a sequential retry, + // not propagated out of the join. + val tapAction = { + if (failedFirst.compareAndSet(false, true)) { + Thread.sleep(60) + throw IllegalStateException("only one gesture can be performed at a time") + } + landed.incrementAndGet() + Unit + } + overlappedDoubleTap(tapAction) + assertEquals(2, landed.get()) + } + @Test fun doubleTapDefaultComposesTwoTaps() { // Interface delegation would bind the default doubleTap to the // delegate, bypassing the tap override, so implement the interface