Skip to content

Commit 656e748

Browse files
committed
ADFA-5231: name the diagnostics reschedule as a self-cancelling send
1 parent 5fa7210 commit 656e748

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.itsaky.androidide.utils
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.SupervisorJob
6+
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.runBlocking
8+
import org.junit.Test
9+
import java.util.concurrent.CountDownLatch
10+
import java.util.concurrent.TimeUnit
11+
import java.util.concurrent.atomic.AtomicBoolean
12+
import java.util.concurrent.atomic.AtomicInteger
13+
import kotlin.time.Duration.Companion.milliseconds
14+
15+
/**
16+
* Scheduling the key an action is *currently running for* cancels that run.
17+
*
18+
* The worker races `actionJob.onJoin` against `channel.onReceive`, so a send from inside the action
19+
* is indistinguishable from a newer key arriving: the receive wins, the in-flight job is cancelled,
20+
* and the key is re-sent. `KotlinDiagnosticProvider` relies on both halves of that - it reschedules
21+
* from inside its own action and expects the analysis it just discarded to run again.
22+
*/
23+
class KeyedDebouncingActionSelfScheduleTest {
24+
private companion object {
25+
const val SECOND_RUN_TIMEOUT_SECONDS = 5L
26+
}
27+
28+
@Test
29+
fun `scheduling from inside the action cancels that run and re-runs it`() =
30+
runBlocking {
31+
val runs = AtomicInteger(0)
32+
val firstRunFinished = AtomicBoolean(false)
33+
val secondRunStarted = CountDownLatch(1)
34+
lateinit var debouncer: KeyedDebouncingAction<String>
35+
36+
debouncer =
37+
KeyedDebouncingAction(
38+
scope = CoroutineScope(SupervisorJob()),
39+
debounceDuration = 20.milliseconds,
40+
action = { key, _ ->
41+
if (runs.incrementAndGet() == 1) {
42+
debouncer.schedule(key)
43+
// A suspension point is where the cancellation from the self-send takes effect.
44+
delay(200)
45+
firstRunFinished.set(true)
46+
} else {
47+
secondRunStarted.countDown()
48+
}
49+
},
50+
)
51+
52+
debouncer.schedule("k")
53+
54+
assertThat(secondRunStarted.await(SECOND_RUN_TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue()
55+
assertThat(firstRunFinished.get()).isFalse()
56+
debouncer.cancelAll()
57+
}
58+
}

lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ private fun doAnalyze(
143143

144144
if (superseded) {
145145
logger.debug("dropping superseded diagnostics for {}", file)
146+
/*
147+
* On the debounced path this is a self-send: doAnalyze runs as fileAnalyzer's own action, so the
148+
* send reads to the worker as a newer key and cancels the run it came from. Deliberate - the
149+
* reschedule still lands, and the only casualty is the NO_UPDATE publish below, which had nothing
150+
* to say anyway. Reached from KotlinLanguageServer.analyze() instead, it is a plain reschedule.
151+
*/
146152
env.fileAnalyzer.schedule(file)
147153
} else {
148154
logger.warn("File {} is not accessible", file)

0 commit comments

Comments
 (0)