Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin coroutines instrumentation #4405

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
45d2c12
Configure kotlin-coroutines module to be compilable
monosoul Dec 8, 2022
df6d722
Add a test case for kotlin coroutines tracing with suspension
monosoul Dec 9, 2022
c7b4468
Add a naive Kotlin coroutines instrumentation
monosoul Dec 9, 2022
a3512c8
Rewrite using ManagedScope abstraction for scope stack management
monosoul Dec 9, 2022
9553d24
Configure muzzle
monosoul Dec 9, 2022
b001da9
Merge remote-tracking branch 'origin/master' into feature/kotlin-coro…
monosoul Dec 9, 2022
d0cbeac
Fix a typo
monosoul Dec 9, 2022
079ab2c
ManagedScope -> ScopeState; delegateManagedScope -> newScopeState
monosoul Dec 9, 2022
e719abc
Improve createKotlinDirs task declaration to make sure it always works
monosoul Dec 9, 2022
66072b6
Add unit tests for ContinuableScopeState
monosoul Dec 9, 2022
1a342eb
Exclude CustomScopeState
monosoul Dec 9, 2022
6aca8d1
Merge remote-tracking branch 'origin/master' into feature/kotlin-coro…
monosoul Dec 12, 2022
ec1ff19
Merge remote-tracking branch 'origin/master' into feature/kotlin-coro…
monosoul Dec 14, 2022
3a9b4f1
Merge remote-tracking branch 'origin/master' into feature/kotlin-coro…
monosoul Dec 14, 2022
d5438db
Add a test case for lazily started coroutines
monosoul Dec 18, 2022
497bdca
Add a test case for starting coroutines without a parent trace
monosoul Dec 18, 2022
708c0be
Adjust the implementation to make the tests pass
monosoul Dec 18, 2022
f285a1e
Refactor CoroutineContextHelper a bit and add a comment
monosoul Dec 18, 2022
be78939
Remove unused method
monosoul Dec 19, 2022
d36e597
Fix Gradle task failures caused by kotlin coroutines module
monosoul Dec 20, 2022
1358d10
Merge remote-tracking branch 'origin/master' into feature/kotlin-coro…
monosoul Dec 20, 2022
296d400
Disable strict trace writes for the coroutines test
monosoul Dec 20, 2022
92cc4e6
Fix wrong dispatcher used in parameterized tests
monosoul Dec 20, 2022
1284f08
Refactor KotlinCoroutineTests
monosoul Dec 20, 2022
bca00e1
Merge remote-tracking branch 'upstream/master' into feature/kotlin-co…
monosoul Dec 22, 2022
7073b6e
Introduce ContinuationHandler to better manage continuations
monosoul Dec 22, 2022
a3e901b
Merge remote-tracking branch 'upstream/master' into feature/kotlin-co…
monosoul Dec 22, 2022
f193145
Add final modifiers where it makes sense
monosoul Dec 22, 2022
8f00d51
Remove redundant annotation
monosoul Dec 22, 2022
a2ead1c
Add coroutine names for easier troubleshooting
monosoul Dec 22, 2022
9eaf18c
Ensure invokeOnCompletion is invoked with the right scope stack
monosoul Dec 23, 2022
7c19f01
scopeState -> coroutineScopeState
monosoul Dec 23, 2022
971839d
currentThreadState -> currentThreadScopeState
monosoul Dec 23, 2022
01cff05
fix formatting
monosoul Dec 23, 2022
f5ba3d0
Extract ContinuationHandler into its own file
monosoul Dec 23, 2022
fcb59ae
Extract closeScopeAndCancelContinuation method
monosoul Dec 23, 2022
2f062e6
Handle lazily/eagerly started coroutines differently (#3)
monosoul Jan 26, 2023
3923d69
Merge remote-tracking branch 'origin/master' into feature/kotlin-coro…
monosoul Jan 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions dd-java-agent/instrumentation/kotlin-coroutines/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,68 @@
muzzle {
pass {
group = 'org.jetbrains.kotlin'
module = 'kotlin-stdlib'
versions = "[1.3.72,)"
extraDependency "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
}
pass {
group = 'org.jetbrains.kotlinx'
module = 'kotlinx-coroutines-core'
versions = "[1.3.0,1.3.8)"
extraDependency "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
}
// for 1.3.9 - 1.4.3 muzzle fails to choose the variant
pass {
group = 'org.jetbrains.kotlinx'
module = 'kotlinx-coroutines-core-jvm'
versions = "[1.5.0,)"
extraDependency "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
}
}

apply from: "$rootDir/gradle/java.gradle"
apply from: "$rootDir/gradle/test-with-kotlin.gradle"

// Having Groovy, Kotlin and Java in the same project is a bit problematic
// this removes Kotlin from main source set to avoid compilation issues
sourceSets {
main {
kotlin {
srcDirs = []
}
java {
srcDirs = ["src/main/java"]
}
}
}

// this creates Kotlin dirs to make JavaCompile tasks work
def createKotlinDirs = tasks.register("createKotlinDirs") {
def dirsToCreate = ["classes/kotlin/main"]
doFirst {
dirsToCreate.forEach {
new File(project.buildDir, it).mkdirs()
}
}

outputs.dirs(
dirsToCreate.collect {
project.layout.buildDirectory.dir(it)
}
)
}

tasks.withType(JavaCompile).configureEach {
inputs.files(createKotlinDirs)
}

dependencies {
implementation project(':dd-java-agent:instrumentation:java-concurrent')
testImplementation project(':dd-trace-api')
compileOnly deps.kotlin
compileOnly deps.coroutines

testImplementation deps.kotlin
testImplementation deps.coroutines

testImplementation project(':dd-trace-api')
testImplementation project(':dd-java-agent:instrumentation:trace-annotation')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package datadog.trace.instrumentation.kotlin.coroutines;

import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.instrumentation.kotlin.coroutines.CoroutineContextHelper.getScopeStateContext;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isOverriddenFrom;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import kotlin.coroutines.CoroutineContext;
import kotlinx.coroutines.AbstractCoroutine;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(Instrumenter.class)
public class AbstractCoroutineInstrumentation extends Instrumenter.Tracing
implements Instrumenter.ForTypeHierarchy {

private static final String ABSTRACT_COROUTINE_CLASS_NAME =
"kotlinx.coroutines.AbstractCoroutine";

private static final String JOB_SUPPORT_CLASS_NAME = "kotlinx.coroutines.JobSupport";
private static final String COROUTINE_CONTEXT_CLASS_NAME = "kotlin.coroutines.CoroutineContext";

public AbstractCoroutineInstrumentation() {
super("kotlin_coroutine.experimental");
}

@Override
protected final boolean defaultEnabled() {
return false;
}

@Override
public String[] helperClassNames() {
return new String[] {
packageName + ".ScopeStateCoroutineContext",
packageName + ".ScopeStateCoroutineContext$ContextElementKey",
packageName + ".CoroutineContextHelper",
};
}

@Override
public void adviceTransformations(AdviceTransformation transformation) {
transformation.applyAdvice(
isConstructor()
.and(isDeclaredBy(named(ABSTRACT_COROUTINE_CLASS_NAME)))
.and(takesArguments(2))
.and(takesArgument(0, named(COROUTINE_CONTEXT_CLASS_NAME)))
.and(takesArgument(1, named("boolean"))),
AbstractCoroutineInstrumentation.class.getName() + "$AbstractCoroutineConstructorAdvice");

transformation.applyAdvice(
isMethod()
.and(isOverriddenFrom(named(ABSTRACT_COROUTINE_CLASS_NAME)))
.and(named("onStart"))
.and(takesNoArguments())
.and(returns(void.class)),
AbstractCoroutineInstrumentation.class.getName() + "$AbstractCoroutineOnStartAdvice");

transformation.applyAdvice(
isMethod()
.and(isOverriddenFrom(named(JOB_SUPPORT_CLASS_NAME)))
.and(named("onCompletionInternal"))
.and(takesArguments(1))
.and(returns(void.class)),
AbstractCoroutineInstrumentation.class.getName()
+ "$JobSupportAfterCompletionInternalAdvice");
}

@Override
public String hierarchyMarkerType() {
return ABSTRACT_COROUTINE_CLASS_NAME;
}

@Override
public ElementMatcher<TypeDescription> hierarchyMatcher() {
return extendsClass(named(hierarchyMarkerType()));
}

/**
* Guarantees every coroutine created has a new instance of ScopeStateCoroutineContext, so that it
* is never inherited from the parent context.
*
* @see ScopeStateCoroutineContext
* @see AbstractCoroutine#AbstractCoroutine(CoroutineContext, boolean)
*/
public static class AbstractCoroutineConstructorAdvice {
@Advice.OnMethodEnter
public static void constructorInvocation(
@Advice.Argument(value = 0, readOnly = false) CoroutineContext parentContext,
@Advice.Argument(value = 1) final boolean active) {
final ScopeStateCoroutineContext scopeStackContext = new ScopeStateCoroutineContext();
parentContext = parentContext.plus(scopeStackContext);
if (active) {
// if this is not a lazy coroutine, inherit parent span from the coroutine constructor call
// site
scopeStackContext.maybeInitialize();
}
}
}

/**
* If/when coroutine is started lazily, initializes ScopeStateCoroutineContext element on
* coroutine start
*
* @see ScopeStateCoroutineContext
* @see AbstractCoroutine#onStart()
*/
public static class AbstractCoroutineOnStartAdvice {
@Advice.OnMethodEnter
public static void onStartInvocation(@Advice.This final AbstractCoroutine<?> coroutine) {
final ScopeStateCoroutineContext scopeStackContext =
getScopeStateContext(coroutine.getContext());
if (scopeStackContext != null) {
// try to inherit parent span from the coroutine start call site
scopeStackContext.maybeInitialize();
}
}
}

/**
* Guarantees a ScopeStateCoroutineContext element is always closed when coroutine transitions
* into a terminal state.
*
* @see ScopeStateCoroutineContext
* @see AbstractCoroutine#onCompletionInternal(Object)
*/
public static class JobSupportAfterCompletionInternalAdvice {
@Advice.OnMethodEnter
public static void onCompletionInternal(@Advice.This final AbstractCoroutine<?> coroutine) {
final ScopeStateCoroutineContext scopeStackContext =
getScopeStateContext(coroutine.getContext());
if (scopeStackContext != null) {
// close the scope if needed
scopeStackContext.maybeCloseScopeAndCancelContinuation();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package datadog.trace.instrumentation.kotlin.coroutines;

import kotlin.coroutines.CoroutineContext;
import kotlinx.coroutines.Job;
import org.jetbrains.annotations.Nullable;

public class CoroutineContextHelper {
/*
IntelliJ shows a warning here for Job being out of bounds, but that's not true, the class compiles.
*/

@Nullable
@SuppressWarnings("unchecked")
public static Job getJob(final CoroutineContext context) {
return context.get((CoroutineContext.Key<Job>) Job.Key);
}

@Nullable
public static ScopeStateCoroutineContext getScopeStateContext(final CoroutineContext context) {
return context.get(ScopeStateCoroutineContext.KEY);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package datadog.trace.instrumentation.kotlin.coroutines;

import datadog.trace.bootstrap.instrumentation.api.AgentScope;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.ScopeState;
import kotlin.coroutines.CoroutineContext;
import kotlin.jvm.functions.Function2;
import kotlinx.coroutines.ThreadContextElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class ScopeStateCoroutineContext implements ThreadContextElement<ScopeState> {

public static final Key<ScopeStateCoroutineContext> KEY = new ContextElementKey();
private final ScopeState coroutineScopeState;
@Nullable private AgentScope.Continuation continuation;
@Nullable private AgentScope continuationScope;
private boolean isInitialized = false;

public ScopeStateCoroutineContext() {
coroutineScopeState = AgentTracer.get().newScopeState();
}

/**
* If there is an active scope at the time of invocation, and it is async propagated, then
* captures the scope's continuation
*/
public void maybeInitialize() {
if (!isInitialized) {
final AgentScope activeScope = AgentTracer.get().activeScope();
if (activeScope != null && activeScope.isAsyncPropagating()) {
continuation = activeScope.captureConcurrent();
}
isInitialized = true;
}
}

@Override
public void restoreThreadContext(
@NotNull final CoroutineContext coroutineContext, final ScopeState oldState) {
oldState.activate();
}

@Override
public ScopeState updateThreadContext(@NotNull final CoroutineContext coroutineContext) {
final ScopeState oldScopeState = AgentTracer.get().newScopeState();
oldScopeState.fetchFromActive();

coroutineScopeState.activate();

if (continuation != null && continuationScope == null) {
continuationScope = continuation.activate();
}

return oldScopeState;
}

/**
* If the context element has a captured scope continuation and an active scope, then closes the
* scope and cancels the continuation.
*/
public void maybeCloseScopeAndCancelContinuation() {
final ScopeState currentThreadScopeState = AgentTracer.get().newScopeState();
currentThreadScopeState.fetchFromActive();

coroutineScopeState.activate();

if (continuationScope != null) {
continuationScope.close();
}
if (continuation != null) {
continuation.cancel();
}

currentThreadScopeState.activate();
}

@Nullable
@Override
public <E extends Element> E get(@NotNull final Key<E> key) {
return CoroutineContext.Element.DefaultImpls.get(this, key);
}

@NotNull
@Override
public CoroutineContext minusKey(@NotNull final Key<?> key) {
return CoroutineContext.Element.DefaultImpls.minusKey(this, key);
}

@NotNull
@Override
public CoroutineContext plus(@NotNull final CoroutineContext coroutineContext) {
return CoroutineContext.DefaultImpls.plus(this, coroutineContext);
}

@Override
public <R> R fold(
R initial, @NotNull Function2<? super R, ? super Element, ? extends R> operation) {
return CoroutineContext.Element.DefaultImpls.fold(this, initial, operation);
}

@NotNull
@Override
public Key<?> getKey() {
return KEY;
}

static class ContextElementKey implements Key<ScopeStateCoroutineContext> {}
}
Loading