fix(sidecar): absorb first-leg double-tap collision sequentially

This commit is contained in:
pj committed 2026-06-06 11:04:51 +05:30
1 parent 693b86fc61
commit a68cbb0224
2 files changed
+26 -3

No files matched your search

@@ -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
}
try {
firstTap.join()
} catch (_: Throwable) {
tapAction()
}
}
data class MetricsSample(
@@ -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