Skip to content

Commit b1db32e

Browse files
mccullsdevflow.devflow-routing-intake
andauthored
Provide ability to capture continuations using the Context API (#11663)
Provide ability to capture continuations using the Context API This is meant as a stepping stone from the old API towards a simpler foundation Clean up use of EmptyContext.INSTANCE Apply @ParametersAreNonnullByDefault to test listeners Explain choice of rehash value Extend context/scope tests Introduce ContextHolder to hold/update current context without the overhead of ThreadLocal Replace weak-ref NoopContextScope cache with simple allocation Benchmarking shows these scopes aren't alive long enough to pay for the caching Greatly reduce allocations by treating existing context containers as no-op scopes Add microbenchmarks to exercise Context API for both old and new implementations Refactor no-op scope approach Allow contexts to wrap themselves as scopes without attaching. This helps reduce allocations when attaching the same context multiple times. Merge remote-tracking branch 'origin/master' into mcculls/context-continuations Rename previous to beforeAttach / beforeSwap Make DDSpan, NoopSpan (and BlackHoleSpan) all SelfScopedContexts to further reduce allocations Fix merge conflict Merge remote-tracking branch 'origin/master' into mcculls/context-continuations Use dotted property form for JMH options (jmh.includes) and testJvm property for the JMH JVM Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 0ac2d54 commit b1db32e

30 files changed

Lines changed: 1767 additions & 95 deletions

components/context/src/main/java/datadog/context/Context.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ static Context detachFrom(Object carrier) {
101101
return binder().detachFrom(carrier);
102102
}
103103

104+
/**
105+
* Captures this (attached) context so it can be resumed in another execution unit.
106+
*
107+
* @return continuation capturing this context.
108+
*/
109+
default ContextContinuation capture() {
110+
return manager().capture(this);
111+
}
112+
104113
/**
105114
* Gets the value stored in this context under the given key.
106115
*
@@ -159,4 +168,13 @@ default Context with(@Nullable ImplicitContextKeyed value) {
159168
}
160169
return value.storeInto(this);
161170
}
171+
172+
/**
173+
* Wraps context as a scope without attaching it to the current execution unit.
174+
*
175+
* @return a scope that has no effect on execution units.
176+
*/
177+
default ContextScope asScope() {
178+
return new NoopContextScope(this);
179+
}
162180
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package datadog.context;
2+
3+
/**
4+
* Captures a context attached to one execution unit so it can be resumed in another.
5+
*
6+
* <p>To propagate context to a single background task:
7+
*
8+
* <pre>{@code
9+
* ContextContinuation continuation = Context.current().capture();
10+
* executor.execute(() -> {
11+
* try (ContextScope scope = continuation.resume()) {
12+
* // ... Context.current() here returns the captured context
13+
* }
14+
* // context implicitly released from continuation when resumed scope closes
15+
* });
16+
* }</pre>
17+
*
18+
* <p>If a continuation is never resumed (e.g. a task is cancelled before it runs), you must release
19+
* it explicitly to avoid resource leaks:
20+
*
21+
* <pre>{@code
22+
* ContextContinuation continuation = Context.current().capture();
23+
* Future<?> future = executor.submit(() -> {
24+
* try (ContextScope scope = continuation.resume()) {
25+
* // ...
26+
* }
27+
* });
28+
* // ...
29+
* if (future.cancel(false)) {
30+
* continuation.release(); // task will never resume, so release manually
31+
* }
32+
* }</pre>
33+
*
34+
* <p>When the same context is resumed concurrently across multiple threads, call {@link #hold()}
35+
* immediately after capture to prevent the first {@link #resume()} from releasing the context:
36+
*
37+
* <pre>{@code
38+
* ContextContinuation continuation = Context.current().capture().hold();
39+
* for (int i = 0; i < N; i++) {
40+
* executor.execute(() -> {
41+
* try (ContextScope scope = continuation.resume()) {
42+
* // ...
43+
* }
44+
* });
45+
* }
46+
* // ...
47+
* continuation.release(); // remember to release the hold once all tasks are resumed/done
48+
* }</pre>
49+
*/
50+
public interface ContextContinuation {
51+
52+
/**
53+
* Optional builder method to stop {@link #resume()} from implicitly releasing the captured
54+
* context. This is useful when multiple threads may concurrently resume the context. You must
55+
* then explicitly {@link #release() release} the context once all threads are resumed/done.
56+
*
57+
* @return this continuation, but with implicit release-after-resume turned off.
58+
*/
59+
ContextContinuation hold();
60+
61+
/**
62+
* Returns the context captured by this continuation.
63+
*
64+
* @return the captured context.
65+
*/
66+
Context context();
67+
68+
/**
69+
* Resumes the context captured by this continuation by attaching it to the current execution
70+
* unit. Implicitly {@link #release() releases} the captured context at the end of the resumed
71+
* scope, unless {@link #hold()} was called when creating the continuation.
72+
*
73+
* @return a scope to be closed when the resumed context is invalid.
74+
*/
75+
ContextScope resume();
76+
77+
/** Explicitly releases the context captured by this continuation. */
78+
void release();
79+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package datadog.context;
2+
3+
/** Listener of context events. */
4+
public interface ContextListener {
5+
6+
/**
7+
* Notifies that the given context has been attached to the current execution unit.
8+
*
9+
* @param context the attached context.
10+
*/
11+
default void onAttach(Context context) {}
12+
13+
/**
14+
* Notifies that the given context has been detached from the current execution unit.
15+
*
16+
* @param context the detached context.
17+
*/
18+
default void onDetach(Context context) {}
19+
20+
/**
21+
* Notifies that the given context has been captured by a continuation.
22+
*
23+
* @param context the captured context.
24+
*/
25+
default void onCapture(Context context) {}
26+
27+
/**
28+
* Notifies that the given context has been released from a continuation.
29+
*
30+
* @param context the released context.
31+
*/
32+
default void onRelease(Context context) {}
33+
}

components/context/src/main/java/datadog/context/ContextManager.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package datadog.context;
22

3+
import static datadog.context.ContextProviders.manager;
4+
35
/** Manages context across execution units. */
46
public interface ContextManager {
57
/**
@@ -25,6 +27,29 @@ public interface ContextManager {
2527
*/
2628
Context swap(Context context);
2729

30+
/**
31+
* Captures the given (attached) context so it can be resumed in another execution unit.
32+
*
33+
* @return continuation capturing the context.
34+
*/
35+
ContextContinuation capture(Context context);
36+
37+
/**
38+
* Registers the given listener to receive context events.
39+
*
40+
* @param listener the listener to register
41+
*/
42+
void addListener(ContextListener listener);
43+
44+
/**
45+
* Registers the given listener to receive context events.
46+
*
47+
* @param listener the listener to register.
48+
*/
49+
static void register(ContextListener listener) {
50+
manager().addListener(listener);
51+
}
52+
2853
/**
2954
* Requests use of a custom {@link ContextManager}.
3055
*

components/context/src/main/java/datadog/context/EmptyContext.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import static java.util.Objects.requireNonNull;
44

55
import javax.annotation.Nullable;
6-
import javax.annotation.ParametersAreNonnullByDefault;
76

87
/** {@link Context} containing no values. */
9-
@ParametersAreNonnullByDefault
10-
final class EmptyContext implements Context {
8+
final class EmptyContext implements SelfScopedContext {
119
static final Context INSTANCE = new EmptyContext();
1210

11+
private EmptyContext() {}
12+
1313
@Override
1414
@Nullable
1515
public <T> T get(ContextKey<T> key) {

components/context/src/main/java/datadog/context/ImplicitContextKeyed.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package datadog.context;
22

3-
import javax.annotation.ParametersAreNonnullByDefault;
4-
53
/** {@link Context} value that has its own implicit {@link ContextKey}. */
6-
@ParametersAreNonnullByDefault
74
public interface ImplicitContextKeyed {
85
/**
96
* Creates a new context with this value under its chosen key.

components/context/src/main/java/datadog/context/IndexedContext.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66

77
import java.util.Arrays;
88
import javax.annotation.Nullable;
9-
import javax.annotation.ParametersAreNonnullByDefault;
109

1110
/** {@link Context} containing many values. */
12-
@ParametersAreNonnullByDefault
13-
final class IndexedContext implements Context {
11+
final class IndexedContext implements SelfScopedContext {
1412
final Object[] store;
1513

1614
IndexedContext(Object[] store) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package datadog.context;
2+
3+
/** {@link ContextContinuation} that has no effect on execution units. */
4+
final class NoopContextContinuation implements ContextContinuation, ContextScope {
5+
private final Context context;
6+
7+
NoopContextContinuation(Context context) {
8+
this.context = context;
9+
}
10+
11+
@Override
12+
public ContextContinuation hold() {
13+
return this;
14+
}
15+
16+
@Override
17+
public Context context() {
18+
return context;
19+
}
20+
21+
@Override
22+
public ContextScope resume() {
23+
return this; // acts as no-op scope, avoiding allocation
24+
}
25+
26+
@Override
27+
public void release() {}
28+
29+
@Override
30+
public void close() {}
31+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package datadog.context;
2+
3+
/** {@link ContextScope} that has no effect on execution units. */
4+
final class NoopContextScope implements ContextScope {
5+
private final Context context;
6+
7+
NoopContextScope(Context context) {
8+
this.context = context;
9+
}
10+
11+
@Override
12+
public Context context() {
13+
return context;
14+
}
15+
16+
@Override
17+
public void close() {}
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package datadog.context;
2+
3+
/** Context that acts as its own unattached scope. */
4+
public interface SelfScopedContext extends Context, ContextScope {
5+
@Override
6+
default ContextScope asScope() {
7+
return this; // acts as no-op scope, avoiding allocation
8+
}
9+
10+
@Override
11+
default Context context() {
12+
return this;
13+
}
14+
15+
@Override
16+
default void close() {}
17+
}

0 commit comments

Comments
 (0)