Skip to content

Commit d40fa11

Browse files
committed
Provide ability to capture continuations using the Context API
This is meant as a stepping stone from the old API towards a simpler foundation
1 parent 4e8d69f commit d40fa11

23 files changed

Lines changed: 1245 additions & 60 deletions

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

Lines changed: 9 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
*
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: 2 additions & 2 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
108
final class EmptyContext implements Context {
119
static final Context INSTANCE = new EmptyContext();
1210

11+
private EmptyContext() {}
12+
1313
@Override
1414
@Nullable
1515
public <T> T get(ContextKey<T> key) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package datadog.context;
2+
3+
/** {@link ContextContinuation} capturing the empty (root) context. */
4+
final class EmptyContextContinuation implements ContextContinuation {
5+
static final ContextContinuation INSTANCE = new EmptyContextContinuation();
6+
7+
private EmptyContextContinuation() {}
8+
9+
@Override
10+
public ContextContinuation hold() {
11+
return this;
12+
}
13+
14+
@Override
15+
public Context context() {
16+
return EmptyContext.INSTANCE;
17+
}
18+
19+
@Override
20+
public ContextScope resume() {
21+
return NoopContextScope.create(context());
22+
}
23+
24+
@Override
25+
public void release() {}
26+
}

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: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
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
1311
final class IndexedContext implements Context {
1412
final Object[] store;
1513

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package datadog.context;
2+
3+
import java.lang.ref.WeakReference;
4+
5+
/** {@link ContextScope} that has no effect on execution units. */
6+
final class NoopContextScope extends WeakReference<Context> implements ContextScope {
7+
private static final ContextScope ROOT_SCOPE = new NoopContextScope(Context.root());
8+
9+
private static final int CACHE_SIZE = 32; // must be power of 2
10+
private static final int SLOT_MASK = CACHE_SIZE - 1;
11+
private static final int MAX_HASH_ATTEMPTS = 3;
12+
13+
/** Bounded cache of no-op scopes to reduce (re)allocations. */
14+
private static final NoopContextScope[] cache = new NoopContextScope[CACHE_SIZE];
15+
16+
@SuppressWarnings({"resource", "StatementWithEmptyBody"})
17+
static ContextScope create(Context context) {
18+
if (context == Context.root()) {
19+
return ROOT_SCOPE;
20+
}
21+
int hash = System.identityHashCode(context);
22+
int evictedSlot = -1;
23+
// search by repeated hashing; stop when we find an empty slot,
24+
// a matching slot, or we exhaust all attempts and re-use a slot
25+
for (int i = 1, h = hash; true; i++, h = rehash(h)) {
26+
int slot = SLOT_MASK & h;
27+
NoopContextScope existing = cache[slot];
28+
if (existing != null) {
29+
// slot already used
30+
Context existingContext = existing.get();
31+
if (context == existingContext) {
32+
return existing; // match found
33+
}
34+
if (i < MAX_HASH_ATTEMPTS) {
35+
// still more slots to search
36+
if (existingContext == null && evictedSlot < 0) {
37+
// record first evicted slot for re-use later
38+
evictedSlot = slot;
39+
}
40+
continue; // rehash and try again
41+
}
42+
// exhausted attempts, pick best slot to re-use
43+
if (evictedSlot >= 0) {
44+
slot = evictedSlot; // re-use first evicted slot
45+
} else if (existingContext == null) {
46+
// last hashed slot is itself evicted, re-use it
47+
} else {
48+
slot = SLOT_MASK & hash; // re-use first hashed slot
49+
}
50+
}
51+
return (cache[slot] = new NoopContextScope(context));
52+
}
53+
}
54+
55+
private NoopContextScope(Context context) {
56+
super(context);
57+
}
58+
59+
@Override
60+
public Context context() {
61+
Context context = get();
62+
// no-op scopes are used when the context is already attached so the reference
63+
// value should still be there; if not then we fall back to empty (root) context
64+
return context != null ? context : Context.root();
65+
}
66+
67+
@Override
68+
public void close() {}
69+
70+
private static int rehash(int oldHash) {
71+
return Integer.reverseBytes(oldHash * 0x9e3775cd) * 0x9e3775cd;
72+
}
73+
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55

66
import java.util.Objects;
77
import javax.annotation.Nullable;
8-
import javax.annotation.ParametersAreNonnullByDefault;
98

109
/** {@link Context} containing a single value. */
11-
@ParametersAreNonnullByDefault
1210
final class SingletonContext implements Context {
1311
final int index;
1412
final Object value;

0 commit comments

Comments
 (0)