opentelemetry-java-instrume.../dd-java-agent/instrumentation/java-concurrent/src/test/groovy/ExecutorInstrumentationTest...

222 lines
9.8 KiB
Groovy

import datadog.opentracing.DDSpan
import datadog.opentracing.scopemanager.ContinuableScope
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.Trace
import datadog.trace.bootstrap.instrumentation.java.concurrent.CallableWrapper
import datadog.trace.bootstrap.instrumentation.java.concurrent.RunnableWrapper
import io.opentracing.util.GlobalTracer
import spock.lang.Retry
import spock.lang.Shared
import java.lang.reflect.InvocationTargetException
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.Callable
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
import java.util.concurrent.Future
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
@Retry
class ExecutorInstrumentationTest extends AgentTestRunner {
@Shared
def executeRunnable = { e, c -> e.execute((Runnable) c) }
@Shared
def executeForkJoinTask = { e, c -> e.execute((ForkJoinTask) c) }
@Shared
def submitRunnable = { e, c -> e.submit((Runnable) c) }
@Shared
def submitCallable = { e, c -> e.submit((Callable) c) }
@Shared
def submitForkJoinTask = { e, c -> e.submit((ForkJoinTask) c) }
@Shared
def invokeAll = { e, c -> e.invokeAll([(Callable) c]) }
@Shared
def invokeAllTimeout = { e, c -> e.invokeAll([(Callable) c], 10, TimeUnit.SECONDS) }
@Shared
def invokeAny = { e, c -> e.invokeAny([(Callable) c]) }
@Shared
def invokeAnyTimeout = { e, c -> e.invokeAny([(Callable) c], 10, TimeUnit.SECONDS) }
@Shared
def invokeForkJoinTask = { e, c -> e.invoke((ForkJoinTask) c) }
@Shared
def scheduleRunnable = { e, c -> e.schedule((Runnable) c, 10, TimeUnit.MILLISECONDS) }
@Shared
def scheduleCallable = { e, c -> e.schedule((Callable) c, 10, TimeUnit.MILLISECONDS) }
def "#poolImpl '#name' propagates"() {
setup:
def pool = poolImpl
def m = method
new Runnable() {
@Override
@Trace(operationName = "parent")
void run() {
((ContinuableScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(true)
// this child will have a span
m(pool, new JavaAsyncChild())
// this child won't
m(pool, new JavaAsyncChild(false, false))
}
}.run()
TEST_WRITER.waitForTraces(1)
List<DDSpan> trace = TEST_WRITER.get(0)
expect:
TEST_WRITER.size() == 1
trace.size() == 2
trace.get(0).operationName == "parent"
trace.get(1).operationName == "asyncChild"
trace.get(1).parentId == trace.get(0).spanId
cleanup:
pool?.shutdown()
// Unfortunately, there's no simple way to test the cross product of methods/pools.
where:
name | method | poolImpl
"execute Runnable" | executeRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"submit Runnable" | submitRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"submit Callable" | submitCallable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"invokeAll" | invokeAll | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"invokeAll with timeout" | invokeAllTimeout | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"invokeAny" | invokeAny | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"invokeAny with timeout" | invokeAnyTimeout | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
// Scheduled executor has additional methods and also may get disabled because it wraps tasks
"execute Runnable" | executeRunnable | new ScheduledThreadPoolExecutor(1)
"submit Runnable" | submitRunnable | new ScheduledThreadPoolExecutor(1)
"submit Callable" | submitCallable | new ScheduledThreadPoolExecutor(1)
"invokeAll" | invokeAll | new ScheduledThreadPoolExecutor(1)
"invokeAll with timeout" | invokeAllTimeout | new ScheduledThreadPoolExecutor(1)
"invokeAny" | invokeAny | new ScheduledThreadPoolExecutor(1)
"invokeAny with timeout" | invokeAnyTimeout | new ScheduledThreadPoolExecutor(1)
"schedule Runnable" | scheduleRunnable | new ScheduledThreadPoolExecutor(1)
"schedule Callable" | scheduleCallable | new ScheduledThreadPoolExecutor(1)
// ForkJoinPool has additional set of method overloads for ForkJoinTask to deal with
"execute Runnable" | executeRunnable | new ForkJoinPool()
"execute ForkJoinTask" | executeForkJoinTask | new ForkJoinPool()
"submit Runnable" | submitRunnable | new ForkJoinPool()
"submit Callable" | submitCallable | new ForkJoinPool()
"submit ForkJoinTask" | submitForkJoinTask | new ForkJoinPool()
"invoke ForkJoinTask" | invokeForkJoinTask | new ForkJoinPool()
"invokeAll" | invokeAll | new ForkJoinPool()
"invokeAll with timeout" | invokeAllTimeout | new ForkJoinPool()
"invokeAny" | invokeAny | new ForkJoinPool()
"invokeAny with timeout" | invokeAnyTimeout | new ForkJoinPool()
}
def "#poolImpl '#name' disabled wrapping"() {
setup:
def pool = poolImpl
def m = method
def w = wrap
JavaAsyncChild child = new JavaAsyncChild(true, true)
new Runnable() {
@Override
@Trace(operationName = "parent")
void run() {
((ContinuableScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(true)
m(pool, w(child))
}
}.run()
// We block in child to make sure spans close in predictable order
child.unblock()
// Expect two traces because async propagation gets effectively disabled
TEST_WRITER.waitForTraces(2)
expect:
TEST_WRITER.size() == 2
TEST_WRITER.get(0).size() == 1
TEST_WRITER.get(0).get(0).operationName == "parent"
TEST_WRITER.get(1).size() == 1
TEST_WRITER.get(1).get(0).operationName == "asyncChild"
cleanup:
pool?.shutdown()
where:
// Scheduled executor cannot accept wrapped tasks
// TODO: we should have a test that passes lambda, but this is hard
// because this requires tests to be run in java8+ only.
// Instead we 'hand-wrap' tasks in this test.
name | method | wrap | poolImpl
"execute Runnable" | executeRunnable | { new RunnableWrapper(it) } | new ScheduledThreadPoolExecutor(1)
"submit Runnable" | submitRunnable | { new RunnableWrapper(it) } | new ScheduledThreadPoolExecutor(1)
"submit Callable" | submitCallable | { new CallableWrapper(it) } | new ScheduledThreadPoolExecutor(1)
"schedule Runnable" | scheduleRunnable | { new RunnableWrapper(it) } | new ScheduledThreadPoolExecutor(1)
"schedule Callable" | scheduleCallable | { new CallableWrapper(it) } | new ScheduledThreadPoolExecutor(1)
}
def "#poolImpl '#name' reports after canceled jobs"() {
setup:
def pool = poolImpl
def m = method
List<JavaAsyncChild> children = new ArrayList<>()
List<Future> jobFutures = new ArrayList<>()
new Runnable() {
@Override
@Trace(operationName = "parent")
void run() {
((ContinuableScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(true)
try {
for (int i = 0; i < 20; ++i) {
// Our current instrumentation instrumentation does not behave very well
// if we try to reuse Callable/Runnable. Namely we would be getting 'orphaned'
// child traces sometimes since state can contain only one continuation - and
// we do not really have a good way for attributing work to correct parent span
// if we reuse Callable/Runnable.
// Solution for now is to never reuse a Callable/Runnable.
final JavaAsyncChild child = new JavaAsyncChild(true, true)
children.add(child)
try {
Future f = m(pool, new JavaAsyncChild())
jobFutures.add(f)
} catch (InvocationTargetException e) {
throw e.getCause()
}
}
} catch (RejectedExecutionException e) {
}
for (Future f : jobFutures) {
f.cancel(false)
}
for (JavaAsyncChild child : children) {
child.unblock()
}
}
}.run()
TEST_WRITER.waitForTraces(1)
expect:
// FIXME: we should improve this test to make sure continuations are actually closed
TEST_WRITER.size() == 1
where:
name | method | poolImpl
"submit Runnable" | submitRunnable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
"submit Callable" | submitCallable | new ThreadPoolExecutor(1, 1, 1000, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<Runnable>(1))
// Scheduled executor has additional methods and also may get disabled because it wraps tasks
"submit Runnable" | submitRunnable | new ScheduledThreadPoolExecutor(1)
"submit Callable" | submitCallable | new ScheduledThreadPoolExecutor(1)
"schedule Runnable" | scheduleRunnable | new ScheduledThreadPoolExecutor(1)
"schedule Callable" | scheduleCallable | new ScheduledThreadPoolExecutor(1)
// ForkJoinPool has additional set of method overloads for ForkJoinTask to deal with
"submit Runnable" | submitRunnable | new ForkJoinPool()
"submit Callable" | submitCallable | new ForkJoinPool()
}
}