Skip to content

Commit 37acd7b

Browse files
Introduce a shared context component, independent of tracing. (#8117)
Co-authored-by: Bruce Bujon <[email protected]>
1 parent e8e9292 commit 37acd7b

19 files changed

+1013
-0
lines changed

components/context/build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
plugins {
2+
id("me.champeau.jmh")
3+
}
4+
5+
apply(from = "$rootDir/gradle/java.gradle")
6+
7+
jmh {
8+
version = "1.28"
9+
}
10+
11+
val excludedClassesInstructionCoverage by extra {
12+
listOf("datadog.context.ContextProviders") // covered by forked test
13+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package datadog.context;
2+
3+
import static datadog.context.ContextProviders.binder;
4+
import static datadog.context.ContextProviders.manager;
5+
6+
import javax.annotation.Nullable;
7+
8+
/**
9+
* Immutable context scoped to an execution unit or carrier object.
10+
*
11+
* <p>Each element of the context is accessible by its {@link ContextKey}. Keys represents product
12+
* or functional areas and should be created sparingly. Elements in the context may themselves be
13+
* mutable.
14+
*/
15+
public interface Context {
16+
17+
/**
18+
* Returns the root context.
19+
*
20+
* <p>This is the initial local context that all contexts extend.
21+
*/
22+
static Context root() {
23+
return manager().root();
24+
}
25+
26+
/**
27+
* Returns the context attached to the current execution unit.
28+
*
29+
* @return Attached context; {@link #root()} if there is none
30+
*/
31+
static Context current() {
32+
return manager().current();
33+
}
34+
35+
/**
36+
* Attaches this context to the current execution unit.
37+
*
38+
* @return Scope to be closed when the context is invalid.
39+
*/
40+
default ContextScope attach() {
41+
return manager().attach(this);
42+
}
43+
44+
/**
45+
* Swaps this context with the one attached to current execution unit.
46+
*
47+
* @return Previously attached context; {@link #root()} if there was none
48+
*/
49+
default Context swap() {
50+
return manager().swap(this);
51+
}
52+
53+
/**
54+
* Returns the context attached to the given carrier object.
55+
*
56+
* @return Attached context; {@link #root()} if there is none
57+
*/
58+
static Context from(Object carrier) {
59+
return binder().from(carrier);
60+
}
61+
62+
/** Attaches this context to the given carrier object. */
63+
default void attachTo(Object carrier) {
64+
binder().attachTo(carrier, this);
65+
}
66+
67+
/**
68+
* Detaches the context attached to the given carrier object, leaving it context-less.
69+
*
70+
* @return Previously attached context; {@link #root()} if there was none
71+
*/
72+
static Context detachFrom(Object carrier) {
73+
return binder().detachFrom(carrier);
74+
}
75+
76+
/**
77+
* Gets the value stored in this context under the given key.
78+
*
79+
* @return Value stored under the key; {@code null} if there is no value.
80+
*/
81+
@Nullable
82+
<T> T get(ContextKey<T> key);
83+
84+
/**
85+
* Creates a new context from the same elements, except the key is now mapped to the given value.
86+
*
87+
* @return New context with the key-value mapping.
88+
*/
89+
<T> Context with(ContextKey<T> key, T value);
90+
91+
/**
92+
* Creates a new context from the same elements, except the implicit key is mapped to this value.
93+
*
94+
* @return New context with the implicitly keyed value.
95+
*/
96+
default Context with(ImplicitContextKeyed value) {
97+
return value.storeInto(this);
98+
}
99+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package datadog.context;
2+
3+
/** Binds context to carrier objects. */
4+
public interface ContextBinder {
5+
6+
/**
7+
* Returns the context attached to the given carrier object.
8+
*
9+
* @return Attached context; {@link Context#root()} if there is none
10+
*/
11+
Context from(Object carrier);
12+
13+
/** Attaches the given context to the given carrier object. */
14+
void attachTo(Object carrier, Context context);
15+
16+
/**
17+
* Detaches the context attached to the given carrier object, leaving it context-less.
18+
*
19+
* @return Previously attached context; {@link Context#root()} if there was none
20+
*/
21+
Context detachFrom(Object carrier);
22+
23+
/** Requests use of a custom {@link ContextBinder}. */
24+
static void register(ContextBinder binder) {
25+
ContextProviders.customBinder = binder;
26+
}
27+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package datadog.context;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
5+
/**
6+
* {@link Context} key that maps to a value of type {@link T}.
7+
*
8+
* <p>Keys are compared by identity rather than by name. Each stored context type should either
9+
* share its key for re-use or implement {@link ImplicitContextKeyed} to keep its key private.
10+
*/
11+
public final class ContextKey<T> {
12+
private static final AtomicInteger NEXT_INDEX = new AtomicInteger(0);
13+
14+
private final String name;
15+
final int index;
16+
17+
private ContextKey(String name) {
18+
this.name = name;
19+
this.index = NEXT_INDEX.getAndIncrement();
20+
}
21+
22+
/** Creates a new key with the given name. */
23+
public static <T> ContextKey<T> named(String name) {
24+
return new ContextKey<>(name);
25+
}
26+
27+
@Override
28+
public int hashCode() {
29+
return index;
30+
}
31+
32+
// we want identity equality, so no need to override equals()
33+
34+
@Override
35+
public String toString() {
36+
return name;
37+
}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package datadog.context;
2+
3+
/** Manages context across execution units. */
4+
public interface ContextManager {
5+
6+
/**
7+
* Returns the root context.
8+
*
9+
* <p>This is the initial local context that all contexts extend.
10+
*/
11+
Context root();
12+
13+
/**
14+
* Returns the context attached to the current execution unit.
15+
*
16+
* @return Attached context; {@link #root()} if there is none
17+
*/
18+
Context current();
19+
20+
/**
21+
* Attaches the given context to the current execution unit.
22+
*
23+
* @return Scope to be closed when the context is invalid.
24+
*/
25+
ContextScope attach(Context context);
26+
27+
/**
28+
* Swaps the given context with the one attached to current execution unit.
29+
*
30+
* @return Previously attached context; {@link #root()} if there was none
31+
*/
32+
Context swap(Context context);
33+
34+
/** Requests use of a custom {@link ContextManager}. */
35+
static void register(ContextManager manager) {
36+
ContextProviders.customManager = manager;
37+
}
38+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package datadog.context;
2+
3+
/** Provides {@link ContextManager} and {@link ContextBinder} implementations. */
4+
final class ContextProviders {
5+
6+
static volatile ContextManager customManager;
7+
static volatile ContextBinder customBinder;
8+
9+
private static final class ProvidedManager {
10+
static final ContextManager INSTANCE =
11+
null != ContextProviders.customManager
12+
? ContextProviders.customManager
13+
: new ThreadLocalContextManager();
14+
}
15+
16+
private static final class ProvidedBinder {
17+
static final ContextBinder INSTANCE =
18+
null != ContextProviders.customBinder
19+
? ContextProviders.customBinder
20+
: new WeakMapContextBinder();
21+
}
22+
23+
static ContextManager manager() {
24+
return ProvidedManager.INSTANCE; // may be overridden by instrumentation
25+
}
26+
27+
static ContextBinder binder() {
28+
return ProvidedBinder.INSTANCE; // may be overridden by instrumentation
29+
}
30+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package datadog.context;
2+
3+
/** Controls the validity of context attached to an execution unit. */
4+
public interface ContextScope extends AutoCloseable {
5+
6+
/** Returns the context controlled by this scope. */
7+
Context context();
8+
9+
/** Detaches the context from the execution unit. */
10+
@Override
11+
void close();
12+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package datadog.context;
2+
3+
/** {@link Context} containing no values. */
4+
final class EmptyContext implements Context {
5+
static final Context INSTANCE = new EmptyContext();
6+
7+
@Override
8+
public <T> T get(ContextKey<T> key) {
9+
return null;
10+
}
11+
12+
@Override
13+
public <T> Context with(ContextKey<T> key, T value) {
14+
return new SingletonContext(key.index, value);
15+
}
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package datadog.context;
2+
3+
/** {@link Context} value that has its own implicit {@link ContextKey}. */
4+
public interface ImplicitContextKeyed {
5+
6+
/**
7+
* Creates a new context with this value under its chosen key.
8+
*
9+
* @return New context with the implicitly keyed value.
10+
*/
11+
Context storeInto(Context context);
12+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package datadog.context;
2+
3+
import static java.lang.Math.max;
4+
import static java.util.Arrays.copyOfRange;
5+
6+
import java.util.Arrays;
7+
8+
/** {@link Context} containing many values. */
9+
final class IndexedContext implements Context {
10+
private final Object[] store;
11+
12+
IndexedContext(Object[] store) {
13+
this.store = store;
14+
}
15+
16+
@Override
17+
@SuppressWarnings("unchecked")
18+
public <T> T get(ContextKey<T> key) {
19+
int index = key.index;
20+
return index < store.length ? (T) store[index] : null;
21+
}
22+
23+
@Override
24+
public <T> Context with(ContextKey<T> key, T value) {
25+
int index = key.index;
26+
Object[] newStore = copyOfRange(store, 0, max(store.length, index + 1));
27+
newStore[index] = value;
28+
29+
return new IndexedContext(newStore);
30+
}
31+
32+
@Override
33+
public boolean equals(Object o) {
34+
if (this == o) return true;
35+
if (o == null || getClass() != o.getClass()) return false;
36+
IndexedContext that = (IndexedContext) o;
37+
return Arrays.equals(store, that.store);
38+
}
39+
40+
@Override
41+
public int hashCode() {
42+
int result = 31;
43+
result = 31 * result + Arrays.hashCode(store);
44+
return result;
45+
}
46+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package datadog.context;
2+
3+
import static java.lang.Math.max;
4+
5+
import java.util.Objects;
6+
7+
/** {@link Context} containing a single value. */
8+
final class SingletonContext implements Context {
9+
private final int index;
10+
private final Object value;
11+
12+
SingletonContext(int index, Object value) {
13+
this.index = index;
14+
this.value = value;
15+
}
16+
17+
@Override
18+
@SuppressWarnings("unchecked")
19+
public <V> V get(ContextKey<V> key) {
20+
return index == key.index ? (V) value : null;
21+
}
22+
23+
@Override
24+
public <V> Context with(ContextKey<V> secondKey, V secondValue) {
25+
int secondIndex = secondKey.index;
26+
if (index == secondIndex) {
27+
return new SingletonContext(index, secondValue);
28+
} else {
29+
Object[] store = new Object[max(index, secondIndex) + 1];
30+
store[index] = value;
31+
store[secondIndex] = secondValue;
32+
return new IndexedContext(store);
33+
}
34+
}
35+
36+
@Override
37+
public boolean equals(Object o) {
38+
if (this == o) return true;
39+
if (o == null || getClass() != o.getClass()) return false;
40+
SingletonContext that = (SingletonContext) o;
41+
return index == that.index && Objects.equals(value, that.value);
42+
}
43+
44+
@Override
45+
public int hashCode() {
46+
int result = 31;
47+
result = 31 * result + index;
48+
result = 31 * result + Objects.hashCode(value);
49+
return result;
50+
}
51+
}

0 commit comments

Comments
 (0)