Skip to content

Commit df6d722

Browse files
committed
Add a test case for kotlin coroutines tracing with suspension
Signed-off-by: monosoul <[email protected]>
1 parent 45d2c12 commit df6d722

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

dd-java-agent/instrumentation/kotlin-coroutines/src/test/groovy/KotlinCoroutineInstrumentationTest.groovy

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import datadog.trace.core.DDSpan
21
import datadog.trace.agent.test.AgentTestRunner
2+
import datadog.trace.core.DDSpan
33
import kotlinx.coroutines.Dispatchers
44
import kotlinx.coroutines.ThreadPoolDispatcherKt
55

@@ -100,6 +100,41 @@ class KotlinCoroutineInstrumentationTest extends AgentTestRunner {
100100
dispatcher << dispatchersToTest
101101
}
102102

103+
def "kotlin suspension should not mess up traces"() {
104+
setup:
105+
KotlinCoroutineTests kotlinTest = new KotlinCoroutineTests(
106+
ThreadPoolDispatcherKt.newSingleThreadContext("Single-Thread")
107+
)
108+
int expectedNumberOfSpans = kotlinTest.tracedWithSuspendingCoroutines()
109+
110+
expect:
111+
assertTraces(1) {
112+
trace(expectedNumberOfSpans, true) {
113+
span(4) {
114+
operationName "trace.annotation"
115+
parent()
116+
}
117+
def topLevelSpan = span(3)
118+
span(3) {
119+
operationName "top-level"
120+
childOf span(4)
121+
}
122+
span(2) {
123+
operationName "synchronous-child"
124+
childOf topLevelSpan
125+
}
126+
span(1) {
127+
operationName "second-span"
128+
childOf topLevelSpan
129+
}
130+
span(0) {
131+
operationName "first-span"
132+
childOf topLevelSpan
133+
}
134+
}
135+
}
136+
}
137+
103138
private static DDSpan findSpan(List<DDSpan> trace, String opName) {
104139
for (DDSpan span : trace) {
105140
if (span.getOperationName() == opName) {

dd-java-agent/instrumentation/kotlin-coroutines/src/test/kotlin/KotlinCoroutineTests.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import datadog.trace.api.Trace
2+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan
23
import datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeScope
34
import datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan
5+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer.get
6+
import datadog.trace.bootstrap.instrumentation.api.ScopeSource.INSTRUMENTATION
47
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
58
import kotlinx.coroutines.CompletableDeferred
69
import kotlinx.coroutines.CoroutineDispatcher
10+
import kotlinx.coroutines.CoroutineName
711
import kotlinx.coroutines.CoroutineScope
812
import kotlinx.coroutines.CoroutineStart
13+
import kotlinx.coroutines.Deferred
914
import kotlinx.coroutines.async
1015
import kotlinx.coroutines.awaitAll
1116
import kotlinx.coroutines.channels.actor
1217
import kotlinx.coroutines.channels.consumeEach
1318
import kotlinx.coroutines.channels.produce
1419
import kotlinx.coroutines.channels.toChannel
20+
import kotlinx.coroutines.delay
1521
import kotlinx.coroutines.launch
1622
import kotlinx.coroutines.runBlocking
1723
import kotlinx.coroutines.selects.select
24+
import kotlinx.coroutines.sync.Mutex
25+
import kotlinx.coroutines.sync.withLock
1826
import kotlinx.coroutines.withTimeout
1927
import kotlinx.coroutines.yield
2028
import java.util.concurrent.TimeUnit
@@ -142,6 +150,68 @@ class KotlinCoroutineTests(private val dispatcher: CoroutineDispatcher) {
142150
activeSpan().setSpanName(opName)
143151
}
144152

153+
fun childSpan(opName: String): AgentSpan = get().buildSpan(opName)
154+
.withResourceName("coroutines-test-span")
155+
.start()
156+
157+
/**
158+
* --- First job starts -------------------------- First job completes ---
159+
*
160+
* -------------------- Second job starts ---------------------------- Second job completes ---
161+
*/
162+
@Trace
163+
fun tracedWithSuspendingCoroutines(): Int {
164+
fun jobContext(jobName: String) = CoroutineName(jobName)
165+
166+
return runTest {
167+
val jobs = mutableListOf<Deferred<Unit>>()
168+
169+
val beforeFirstJobStartedMutex = Mutex(locked = true)
170+
val afterFirstJobStartedMutex = Mutex(locked = true)
171+
172+
val beforeFirstJobCompletedMutex = Mutex(locked = true)
173+
val afterFirstJobCompletedMutex = Mutex(locked = true)
174+
175+
childSpan("top-level").activateAndUse {
176+
childSpan("synchronous-child").activateAndUse {
177+
delay(5)
178+
}
179+
180+
// this coroutine starts before the second one starts and completes before the second one
181+
async(jobContext("first")) {
182+
beforeFirstJobStartedMutex.lock()
183+
childSpan("first-span").activateAndUse {
184+
afterFirstJobStartedMutex.unlock()
185+
beforeFirstJobCompletedMutex.lock()
186+
}
187+
afterFirstJobCompletedMutex.unlock()
188+
}.run(jobs::add)
189+
190+
// this coroutine starts after the first one and completes after the first one
191+
async(jobContext("second")) {
192+
afterFirstJobStartedMutex.withLock {
193+
childSpan("second-span").activateAndUse {
194+
beforeFirstJobCompletedMutex.unlock()
195+
afterFirstJobCompletedMutex.lock()
196+
}
197+
}
198+
}.run(jobs::add)
199+
}
200+
beforeFirstJobStartedMutex.unlock()
201+
202+
jobs.awaitAll()
203+
204+
5
205+
}
206+
}
207+
208+
private suspend fun AgentSpan.activateAndUse(block: suspend () -> Unit) {
209+
get().activateSpan(this, INSTRUMENTATION).use {
210+
block()
211+
finish()
212+
}
213+
}
214+
145215
private fun <T> runTest(asyncPropagation: Boolean = true, block: suspend CoroutineScope.() -> T): T {
146216
activeScope().setAsyncPropagation(asyncPropagation)
147217
return runBlocking(dispatcher, block = block)

0 commit comments

Comments
 (0)