Skip to content

Commit de5aa64

Browse files
feat: context propagation (#848)
Signed-off-by: Sviatoslav Sharaev <sviatoslav.sharaev@gmail.com> Co-authored-by: Kavindu Dodanduwa <Kavindu-Dodan@users.noreply.github.com>
1 parent 46d04fe commit de5aa64

11 files changed

+367
-42
lines changed

README.md

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,17 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.
120120

121121
## 🌟 Features
122122

123-
| Status | Features | Description |
124-
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
125-
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
126-
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
127-
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
128-
|| [Logging](#logging) | Integrate with popular logging packages. |
129-
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
130-
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
131-
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
132-
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
123+
| Status | Features | Description |
124+
| ------ |-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
125+
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
126+
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
127+
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
128+
|| [Logging](#logging) | Integrate with popular logging packages. |
129+
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
130+
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
131+
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
132+
|| [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). |
133+
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
133134

134135
<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
135136

@@ -272,6 +273,27 @@ This should only be called when your application is in the process of shutting d
272273
OpenFeatureAPI.getInstance().shutdown();
273274
```
274275

276+
### Transaction Context Propagation
277+
Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP).
278+
Transaction context can be set where specific data is available (e.g. an auth service or request handler) and by using the transaction context propagator it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread).
279+
By default, the `NoOpTransactionContextPropagator` is used, which doesn't store anything.
280+
To register a `ThreadLocal` context propagator, you can use the `setTransactionContextPropagator` method as shown below.
281+
```java
282+
// registering the ThreadLocalTransactionContextPropagator
283+
OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator());
284+
```
285+
Once you've registered a transaction context propagator, you can propagate the data into request scoped transaction context.
286+
287+
```java
288+
// adding userId to transaction context
289+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
290+
Map<String, Value> transactionAttrs = new HashMap<>();
291+
transactionAttrs.put("userId", new Value("userId"));
292+
EvaluationContext transactionCtx = new ImmutableContext(transactionAttrs);
293+
api.setTransactionContext(apiCtx);
294+
```
295+
Additionally, you can develop a custom transaction context propagator by implementing the `TransactionContextPropagator` interface and registering it as shown above.
296+
275297
## Extending
276298

277299
### Develop a provider
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package dev.openfeature.sdk;
2+
3+
/**
4+
* A {@link TransactionContextPropagator} that simply returns empty context.
5+
*/
6+
public class NoOpTransactionContextPropagator implements TransactionContextPropagator {
7+
8+
/**
9+
* {@inheritDoc}
10+
* @return empty immutable context
11+
*/
12+
@Override
13+
public EvaluationContext getTransactionContext() {
14+
return new ImmutableContext();
15+
}
16+
17+
/**
18+
* {@inheritDoc}
19+
*/
20+
@Override
21+
public void setTransactionContext(EvaluationContext evaluationContext) {
22+
23+
}
24+
}

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
2727
private ProviderRepository providerRepository;
2828
private EventSupport eventSupport;
2929
private EvaluationContext evaluationContext;
30+
private TransactionContextPropagator transactionContextPropagator;
3031

3132
protected OpenFeatureAPI() {
3233
apiHooks = new ArrayList<>();
3334
providerRepository = new ProviderRepository();
3435
eventSupport = new EventSupport();
36+
transactionContextPropagator = new NoOpTransactionContextPropagator();
3537
}
3638

3739
private static class SingletonHolder {
@@ -96,6 +98,46 @@ public EvaluationContext getEvaluationContext() {
9698
}
9799
}
98100

101+
/**
102+
* Return the transaction context propagator.
103+
*/
104+
public TransactionContextPropagator getTransactionContextPropagator() {
105+
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
106+
return this.transactionContextPropagator;
107+
}
108+
}
109+
110+
/**
111+
* Sets the transaction context propagator.
112+
*
113+
* @throws IllegalArgumentException if {@code transactionContextPropagator} is null
114+
*/
115+
public void setTransactionContextPropagator(TransactionContextPropagator transactionContextPropagator) {
116+
if (transactionContextPropagator == null) {
117+
throw new IllegalArgumentException("Transaction context propagator cannot be null");
118+
}
119+
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
120+
this.transactionContextPropagator = transactionContextPropagator;
121+
}
122+
}
123+
124+
/**
125+
* Returns the currently defined transaction context using the registered transaction
126+
* context propagator.
127+
*
128+
* @return {@link EvaluationContext} The current transaction context
129+
*/
130+
EvaluationContext getTransactionContext() {
131+
return this.transactionContextPropagator.getTransactionContext();
132+
}
133+
134+
/**
135+
* Sets the transaction context using the registered transaction context propagator.
136+
*/
137+
public void setTransactionContext(EvaluationContext evaluationContext) {
138+
this.transactionContextPropagator.setTransactionContext(evaluationContext);
139+
}
140+
99141
/**
100142
* Set the default provider.
101143
*/

src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,6 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
105105
FeatureProvider provider;
106106

107107
try {
108-
final EvaluationContext apiContext;
109-
final EvaluationContext clientContext;
110-
111108
// openfeatureApi.getProvider() must be called once to maintain a consistent reference
112109
provider = openfeatureApi.getProvider(this.name);
113110

@@ -117,19 +114,9 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
117114
hookCtx = HookContext.from(key, type, this.getMetadata(),
118115
provider.getMetadata(), ctx, defaultValue);
119116

120-
// merge of: API.context, client.context, invocation.context
121-
apiContext = openfeatureApi.getEvaluationContext() != null
122-
? openfeatureApi.getEvaluationContext()
123-
: new ImmutableContext();
124-
clientContext = this.getEvaluationContext() != null
125-
? this.getEvaluationContext()
126-
: new ImmutableContext();
127-
128117
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
129118

130-
EvaluationContext invocationCtx = ctx.merge(ctxFromHook);
131-
132-
EvaluationContext mergedCtx = apiContext.merge(clientContext.merge(invocationCtx));
119+
EvaluationContext mergedCtx = mergeEvaluationContext(ctxFromHook, ctx);
133120

134121
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key,
135122
defaultValue, provider, mergedCtx);
@@ -157,6 +144,29 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
157144
return details;
158145
}
159146

147+
/**
148+
* Merge hook and invocation contexts with API, transaction and client contexts.
149+
*
150+
* @param hookContext hook context
151+
* @param invocationContext invocation context
152+
* @return merged evaluation context
153+
*/
154+
private EvaluationContext mergeEvaluationContext(
155+
EvaluationContext hookContext,
156+
EvaluationContext invocationContext) {
157+
final EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null
158+
? openfeatureApi.getEvaluationContext()
159+
: new ImmutableContext();
160+
final EvaluationContext clientContext = this.getEvaluationContext() != null
161+
? this.getEvaluationContext()
162+
: new ImmutableContext();
163+
final EvaluationContext transactionContext = openfeatureApi.getTransactionContext() != null
164+
? openfeatureApi.getTransactionContext()
165+
: new ImmutableContext();
166+
167+
return apiContext.merge(transactionContext.merge(clientContext.merge(invocationContext.merge(hookContext))));
168+
}
169+
160170
private <T> ProviderEvaluation<?> createProviderEvaluation(
161171
FlagValueType type,
162172
String key,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.openfeature.sdk;
2+
3+
/**
4+
* A {@link ThreadLocalTransactionContextPropagator} is a transactional context propagator
5+
* that uses a ThreadLocal to persist a transactional context for the duration of a single thread.
6+
*
7+
* @see TransactionContextPropagator
8+
*/
9+
public class ThreadLocalTransactionContextPropagator implements TransactionContextPropagator {
10+
11+
private final ThreadLocal<EvaluationContext> evaluationContextThreadLocal = new ThreadLocal<>();
12+
13+
/**
14+
* {@inheritDoc}
15+
*/
16+
@Override
17+
public EvaluationContext getTransactionContext() {
18+
return this.evaluationContextThreadLocal.get();
19+
}
20+
21+
/**
22+
* {@inheritDoc}
23+
*/
24+
@Override
25+
public void setTransactionContext(EvaluationContext evaluationContext) {
26+
this.evaluationContextThreadLocal.set(evaluationContext);
27+
}
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.openfeature.sdk;
2+
3+
/**
4+
* {@link TransactionContextPropagator} is responsible for persisting a transactional context
5+
* for the duration of a single transaction.
6+
* Examples of potential transaction specific context include: a user id, user agent, IP.
7+
* Transaction context is merged with evaluation context prior to flag evaluation.
8+
* <p>
9+
* The precedence of merging context can be seen in
10+
* <a href=https://openfeature.dev/specification/sections/evaluation-context#requirement-323>the specification</a>.
11+
* </p>
12+
*/
13+
public interface TransactionContextPropagator {
14+
15+
/**
16+
* Returns the currently defined transaction context using the registered transaction
17+
* context propagator.
18+
*
19+
* @return {@link EvaluationContext} The current transaction context
20+
*/
21+
EvaluationContext getTransactionContext();
22+
23+
/**
24+
* Sets the transaction context.
25+
*/
26+
void setTransactionContext(EvaluationContext evaluationContext);
27+
}

0 commit comments

Comments
 (0)