Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: context propagation #848

Merged
merged 4 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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). |
Kavindu-Dodan marked this conversation as resolved.
Show resolved Hide resolved
| ✅ | [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.
ssharaev marked this conversation as resolved.
Show resolved Hide resolved
*
* @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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea to extract this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! :)

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
Loading