Skip to content

Commit

Permalink
feat: context propagation (#848)
Browse files Browse the repository at this point in the history
Signed-off-by: Sviatoslav Sharaev <[email protected]>
Co-authored-by: Kavindu Dodanduwa <[email protected]>
  • Loading branch information
ssharaev and Kavindu-Dodan authored Mar 29, 2024
1 parent 46d04fe commit de5aa64
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 42 deletions.
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,17 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.

## 🌟 Features

| Status | Features | Description |
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|| [Logging](#logging) | Integrate with popular logging packages. |
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
| Status | Features | Description |
| ------ |-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|| [Logging](#logging) | Integrate with popular logging packages. |
|| [Named clients](#named-clients) | Utilize multiple providers in a single application. |
|| [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|| [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|| [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). |
|| [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |

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

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

### Transaction Context Propagation
Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP).
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).
By default, the `NoOpTransactionContextPropagator` is used, which doesn't store anything.
To register a `ThreadLocal` context propagator, you can use the `setTransactionContextPropagator` method as shown below.
```java
// registering the ThreadLocalTransactionContextPropagator
OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator());
```
Once you've registered a transaction context propagator, you can propagate the data into request scoped transaction context.

```java
// adding userId to transaction context
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
Map<String, Value> transactionAttrs = new HashMap<>();
transactionAttrs.put("userId", new Value("userId"));
EvaluationContext transactionCtx = new ImmutableContext(transactionAttrs);
api.setTransactionContext(apiCtx);
```
Additionally, you can develop a custom transaction context propagator by implementing the `TransactionContextPropagator` interface and registering it as shown above.

## Extending

### Develop a provider
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.openfeature.sdk;

/**
* A {@link TransactionContextPropagator} that simply returns empty context.
*/
public class NoOpTransactionContextPropagator implements TransactionContextPropagator {

/**
* {@inheritDoc}
* @return empty immutable context
*/
@Override
public EvaluationContext getTransactionContext() {
return new ImmutableContext();
}

/**
* {@inheritDoc}
*/
@Override
public void setTransactionContext(EvaluationContext evaluationContext) {

}
}
42 changes: 42 additions & 0 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
private ProviderRepository providerRepository;
private EventSupport eventSupport;
private EvaluationContext evaluationContext;
private TransactionContextPropagator transactionContextPropagator;

protected OpenFeatureAPI() {
apiHooks = new ArrayList<>();
providerRepository = new ProviderRepository();
eventSupport = new EventSupport();
transactionContextPropagator = new NoOpTransactionContextPropagator();
}

private static class SingletonHolder {
Expand Down Expand Up @@ -96,6 +98,46 @@ public EvaluationContext getEvaluationContext() {
}
}

/**
* Return the transaction context propagator.
*/
public TransactionContextPropagator getTransactionContextPropagator() {
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
return this.transactionContextPropagator;
}
}

/**
* Sets the transaction context propagator.
*
* @throws IllegalArgumentException if {@code transactionContextPropagator} is null
*/
public void setTransactionContextPropagator(TransactionContextPropagator transactionContextPropagator) {
if (transactionContextPropagator == null) {
throw new IllegalArgumentException("Transaction context propagator cannot be null");
}
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
this.transactionContextPropagator = transactionContextPropagator;
}
}

/**
* Returns the currently defined transaction context using the registered transaction
* context propagator.
*
* @return {@link EvaluationContext} The current transaction context
*/
EvaluationContext getTransactionContext() {
return this.transactionContextPropagator.getTransactionContext();
}

/**
* Sets the transaction context using the registered transaction context propagator.
*/
public void setTransactionContext(EvaluationContext evaluationContext) {
this.transactionContextPropagator.setTransactionContext(evaluationContext);
}

/**
* Set the default provider.
*/
Expand Down
38 changes: 24 additions & 14 deletions src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,6 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
FeatureProvider provider;

try {
final EvaluationContext apiContext;
final EvaluationContext clientContext;

// openfeatureApi.getProvider() must be called once to maintain a consistent reference
provider = openfeatureApi.getProvider(this.name);

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

// merge of: API.context, client.context, invocation.context
apiContext = openfeatureApi.getEvaluationContext() != null
? openfeatureApi.getEvaluationContext()
: new ImmutableContext();
clientContext = this.getEvaluationContext() != null
? this.getEvaluationContext()
: new ImmutableContext();

EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);

EvaluationContext invocationCtx = ctx.merge(ctxFromHook);

EvaluationContext mergedCtx = apiContext.merge(clientContext.merge(invocationCtx));
EvaluationContext mergedCtx = mergeEvaluationContext(ctxFromHook, ctx);

ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key,
defaultValue, provider, mergedCtx);
Expand Down Expand Up @@ -157,6 +144,29 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
return details;
}

/**
* Merge hook and invocation contexts with API, transaction and client contexts.
*
* @param hookContext hook context
* @param invocationContext invocation context
* @return merged evaluation context
*/
private EvaluationContext mergeEvaluationContext(
EvaluationContext hookContext,
EvaluationContext invocationContext) {
final EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null
? openfeatureApi.getEvaluationContext()
: new ImmutableContext();
final EvaluationContext clientContext = this.getEvaluationContext() != null
? this.getEvaluationContext()
: new ImmutableContext();
final EvaluationContext transactionContext = openfeatureApi.getTransactionContext() != null
? openfeatureApi.getTransactionContext()
: new ImmutableContext();

return apiContext.merge(transactionContext.merge(clientContext.merge(invocationContext.merge(hookContext))));
}

private <T> ProviderEvaluation<?> createProviderEvaluation(
FlagValueType type,
String key,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.openfeature.sdk;

/**
* A {@link ThreadLocalTransactionContextPropagator} is a transactional context propagator
* that uses a ThreadLocal to persist a transactional context for the duration of a single thread.
*
* @see TransactionContextPropagator
*/
public class ThreadLocalTransactionContextPropagator implements TransactionContextPropagator {

private final ThreadLocal<EvaluationContext> evaluationContextThreadLocal = new ThreadLocal<>();

/**
* {@inheritDoc}
*/
@Override
public EvaluationContext getTransactionContext() {
return this.evaluationContextThreadLocal.get();
}

/**
* {@inheritDoc}
*/
@Override
public void setTransactionContext(EvaluationContext evaluationContext) {
this.evaluationContextThreadLocal.set(evaluationContext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dev.openfeature.sdk;

/**
* {@link TransactionContextPropagator} is responsible for persisting a transactional context
* for the duration of a single transaction.
* Examples of potential transaction specific context include: a user id, user agent, IP.
* Transaction context is merged with evaluation context prior to flag evaluation.
* <p>
* The precedence of merging context can be seen in
* <a href=https://openfeature.dev/specification/sections/evaluation-context#requirement-323>the specification</a>.
* </p>
*/
public interface TransactionContextPropagator {

/**
* Returns the currently defined transaction context using the registered transaction
* context propagator.
*
* @return {@link EvaluationContext} The current transaction context
*/
EvaluationContext getTransactionContext();

/**
* Sets the transaction context.
*/
void setTransactionContext(EvaluationContext evaluationContext);
}
Loading

0 comments on commit de5aa64

Please sign in to comment.