From f02077e12052b2a598d20236b2eefb6c68c30aed Mon Sep 17 00:00:00 2001 From: Sviatoslav Sharaev Date: Sat, 16 Mar 2024 18:48:07 +0400 Subject: [PATCH 1/4] feat: context propagation Signed-off-by: Sviatoslav Sharaev --- README.md | 21 ++-- .../sdk/NoOpTransactionContextPropagator.java | 24 ++++ .../dev/openfeature/sdk/OpenFeatureAPI.java | 42 +++++++ .../openfeature/sdk/OpenFeatureClient.java | 40 ++++--- ...readLocalTransactionContextPropagator.java | 28 +++++ .../sdk/TransactionContextPropagator.java | 27 +++++ .../sdk/FlagEvaluationSpecTest.java | 103 +++++++++++++++--- .../java/dev/openfeature/sdk/LockingTest.java | 14 +++ .../NoOpTransactionContextPropagatorTest.java | 23 ++++ .../openfeature/sdk/OpenFeatureAPITest.java | 5 + ...LocalTransactionContextPropagatorTest.java | 57 ++++++++++ 11 files changed, 342 insertions(+), 42 deletions(-) create mode 100644 src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java create mode 100644 src/main/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagator.java create mode 100644 src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java create mode 100644 src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java create mode 100644 src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java diff --git a/README.md b/README.md index 36095f067..d71660fa0 100644 --- a/README.md +++ b/README.md @@ -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. | Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ diff --git a/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java b/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java new file mode 100644 index 000000000..f6499f4a6 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java @@ -0,0 +1,24 @@ +package dev.openfeature.sdk; + +/** + * A {@link TransactionContextPropagator} that simply returns null. + */ +public class NoOpTransactionContextPropagator implements TransactionContextPropagator { + + /** + * {@inheritDoc} + * @return null + */ + @Override + public EvaluationContext getTransactionContext() { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void setTransactionContext(EvaluationContext evaluationContext) { + + } +} diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index a7ba42b31..c7b9c0145 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -27,11 +27,13 @@ public class OpenFeatureAPI implements EventBus { 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 { @@ -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 + */ + public EvaluationContext getTransactionContext() { + return this.transactionContextPropagator.getTransactionContext(); + } + + /** + * Sets the transaction context using the registered transaction context propagator. + */ + void setTransactionContext(EvaluationContext evaluationContext) { + this.transactionContextPropagator.setTransactionContext(evaluationContext); + } + /** * Set the default provider. */ diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index e839bf9f9..5fab0cfdd 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -105,9 +105,6 @@ private FlagEvaluationDetails 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); @@ -117,19 +114,9 @@ private FlagEvaluationDetails 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 providerEval = (ProviderEvaluation) createProviderEvaluation(type, key, defaultValue, provider, mergedCtx); @@ -157,6 +144,31 @@ private FlagEvaluationDetails 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(); + + EvaluationContext mergedInvocationCtx = invocationContext.merge(hookContext); + + return apiContext.merge(transactionContext.merge(clientContext.merge(mergedInvocationCtx))); + } + private ProviderEvaluation createProviderEvaluation( FlagValueType type, String key, diff --git a/src/main/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagator.java b/src/main/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagator.java new file mode 100644 index 000000000..59f92ceba --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagator.java @@ -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 evaluationContextThreadLocal = new ThreadLocal<>(); + + /** + * {@inheritDoc} + */ + @Override + public EvaluationContext getTransactionContext() { + return this.evaluationContextThreadLocal.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTransactionContext(EvaluationContext evaluationContext) { + this.evaluationContextThreadLocal.set(evaluationContext); + } +} diff --git a/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java b/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java new file mode 100644 index 000000000..05f7d3eb8 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java @@ -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. + *

+ * The precedence of merging context can be seen in + * the specification. + *

+ */ +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); +} diff --git a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java index e2e008818..60b6ee136 100644 --- a/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java @@ -302,41 +302,107 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { @Specification(number="3.2.1.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.") @Specification(number="3.2.2.1", text="The API MUST have a method for setting the global evaluation context.") - @Specification(number="3.2.3", text="Evaluation context MUST be merged in the order: API (global; lowest precedence) - client - invocation - before hooks (highest precedence), with duplicate values being overwritten.") + @Specification(number="3.2.3", text="Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.") @Test void multi_layer_context_merges_correctly() { DoSomethingProvider provider = new DoSomethingProvider(); FeatureProviderTestUtils.setFeatureProvider(provider); + TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); + api.setTransactionContextPropagator(transactionContextPropagator); - Map attributes = new HashMap<>(); - attributes.put("common", new Value("1")); - attributes.put("common2", new Value("1")); - attributes.put("api", new Value("2")); - EvaluationContext apiCtx = new ImmutableContext(attributes); + Map apiAttributes = new HashMap<>(); + apiAttributes.put("common1", new Value("1")); + apiAttributes.put("common2", new Value("1")); + apiAttributes.put("common3", new Value("1")); + apiAttributes.put("api", new Value("1")); + EvaluationContext apiCtx = new ImmutableContext(apiAttributes); api.setEvaluationContext(apiCtx); + Map transactionAttributes = new HashMap<>(); + // overwrite value from api context + transactionAttributes.put("common1", new Value("2")); + transactionAttributes.put("common4", new Value("2")); + transactionAttributes.put("common5", new Value("2")); + transactionAttributes.put("transaction", new Value("2")); + EvaluationContext transactionCtx = new ImmutableContext(transactionAttributes); + + api.setTransactionContext(transactionCtx); + Client c = api.getClient(); - Map attributes1 = new HashMap<>(); - attributes.put("common", new Value("3")); - attributes.put("common2", new Value("3")); - attributes.put("client", new Value("4")); - attributes.put("common", new Value("5")); - attributes.put("invocation", new Value("6")); - EvaluationContext clientCtx = new ImmutableContext(attributes); + Map clientAttributes = new HashMap<>(); + // overwrite value from api context + clientAttributes.put("common2", new Value("3")); + // overwrite value from transaction context + clientAttributes.put("common4", new Value("3")); + clientAttributes.put("common6", new Value("3")); + clientAttributes.put("client", new Value("3")); + EvaluationContext clientCtx = new ImmutableContext(clientAttributes); c.setEvaluationContext(clientCtx); - EvaluationContext invocationCtx = new ImmutableContext(); + Map invocationAttributes = new HashMap<>(); + // overwrite value from api context + invocationAttributes.put("common3", new Value("4")); + // overwrite value from transaction context + invocationAttributes.put("common5", new Value("4")); + // overwrite value from api client context + invocationAttributes.put("common6", new Value("4")); + invocationAttributes.put("invocation", new Value("4")); + EvaluationContext invocationCtx = new ImmutableContext(invocationAttributes); // dosomethingprovider inverts this value. assertTrue(c.getBooleanValue("key", false, invocationCtx)); EvaluationContext merged = provider.getMergedContext(); - assertEquals("6", merged.getValue("invocation").asString()); - assertEquals("5", merged.getValue("common").asString(), "invocation merge is incorrect"); - assertEquals("4", merged.getValue("client").asString()); + assertEquals("1", merged.getValue("api").asString()); + assertEquals("2", merged.getValue("transaction").asString()); + assertEquals("3", merged.getValue("client").asString()); + assertEquals("4", merged.getValue("invocation").asString()); + assertEquals("2", merged.getValue("common1").asString(), "transaction merge is incorrect"); assertEquals("3", merged.getValue("common2").asString(), "api client merge is incorrect"); - assertEquals("2", merged.getValue("api").asString()); + assertEquals("4", merged.getValue("common3").asString(), "invocation merge is incorrect"); + assertEquals("3", merged.getValue("common4").asString(), "api client merge is incorrect"); + assertEquals("4", merged.getValue("common5").asString(), "invocation merge is incorrect"); + assertEquals("4", merged.getValue("common6").asString(), "invocation merge is incorrect"); + + } + + @Specification(number="3.3.1.1", text="The API SHOULD have a method for setting a transaction context propagator.") + @Test void setting_transaction_context_propagator() { + DoSomethingProvider provider = new DoSomethingProvider(); + FeatureProviderTestUtils.setFeatureProvider(provider); + + TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); + api.setTransactionContextPropagator(transactionContextPropagator); + assertEquals(transactionContextPropagator, api.getTransactionContextPropagator()); + } + + @Specification(number="3.3.1.2.1", text="The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.") + @Test void setting_transaction_context() { + DoSomethingProvider provider = new DoSomethingProvider(); + FeatureProviderTestUtils.setFeatureProvider(provider); + + TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); + api.setTransactionContextPropagator(transactionContextPropagator); + + Map attributes = new HashMap<>(); + attributes.put("common", new Value("1")); + EvaluationContext transactionContext = new ImmutableContext(attributes); + + api.setTransactionContext(transactionContext); + assertEquals(transactionContext, transactionContextPropagator.getTransactionContext()); + } + + @Specification(number="3.3.1.2.2", text="A transaction context propagator MUST have a method for setting the evaluation context of the current transaction.") + @Specification(number="3.3.1.2.3", text="A transaction context propagator MUST have a method for getting the evaluation context of the current transaction.") + @Test void transaction_context_propagator_setting_context() { + TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator(); + + Map attributes = new HashMap<>(); + attributes.put("common", new Value("1")); + EvaluationContext transactionContext = new ImmutableContext(attributes); + transactionContextPropagator.setTransactionContext(transactionContext); + assertEquals(transactionContext, transactionContextPropagator.getTransactionContext()); } @Specification(number="1.3.4", text="The client SHOULD guarantee the returned value of any typed flag evaluation method is of the expected type. If the value returned by the underlying provider implementation does not match the expected type, it's to be considered abnormal execution, and the supplied default value should be returned.") @@ -355,6 +421,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { @Specification(number="1.3.2.1", text="The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), and evaluation options (optional), which returns the flag value.") @Specification(number="3.2.2.2", text="The Client and invocation MUST NOT have a method for supplying evaluation context.") @Specification(number="3.2.4.1", text="When the global evaluation context is set, the on context changed handler MUST run.") + @Specification(number="3.3.2.1", text="The API MUST NOT have a method for setting a transaction context propagator.") @Test void not_applicable_for_dynamic_context() {} } diff --git a/src/test/java/dev/openfeature/sdk/LockingTest.java b/src/test/java/dev/openfeature/sdk/LockingTest.java index f58795ad0..ddfa9c079 100644 --- a/src/test/java/dev/openfeature/sdk/LockingTest.java +++ b/src/test/java/dev/openfeature/sdk/LockingTest.java @@ -185,6 +185,20 @@ void getContextShouldReadLockAndUnlock() { verify(apiLock.readLock()).unlock(); } + @Test + void setTransactionalContextPropagatorShouldWriteLockAndUnlock() { + api.setTransactionContextPropagator(new NoOpTransactionContextPropagator()); + verify(apiLock.writeLock()).lock(); + verify(apiLock.writeLock()).unlock(); + } + + @Test + void getTransactionalContextPropagatorShouldReadLockAndUnlock() { + api.getTransactionContextPropagator(); + verify(apiLock.readLock()).lock(); + verify(apiLock.readLock()).unlock(); + } + @Test void clearHooksShouldWriteLockAndUnlock() { diff --git a/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java b/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java new file mode 100644 index 000000000..1f91451ae --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java @@ -0,0 +1,23 @@ +package dev.openfeature.sdk; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class NoOpTransactionContextPropagatorTest { + + NoOpTransactionContextPropagator contextPropagator = new NoOpTransactionContextPropagator(); + + @Test + public void emptyTransactionContext() { + EvaluationContext result = contextPropagator.getTransactionContext(); + assertNull(result); + } + + @Test + public void setTransactionContext() { + EvaluationContext firstContext = new ImmutableContext(); + contextPropagator.setTransactionContext(firstContext); + assertNull(contextPropagator.getTransactionContext()); + } +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java index e19a10aec..63a1dadd6 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java @@ -73,4 +73,9 @@ void settingDefaultProviderToNullErrors() { void settingNamedClientProviderToNullErrors() { assertThatCode(() -> api.setProvider(CLIENT_NAME, null)).isInstanceOf(IllegalArgumentException.class); } + + @Test + void settingTransactionalContextPropagatorToNullErrors() { + assertThatCode(() -> api.setTransactionContextPropagator(null)).isInstanceOf(IllegalArgumentException.class); + } } diff --git a/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java b/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java new file mode 100644 index 000000000..00b63c8a1 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java @@ -0,0 +1,57 @@ +package dev.openfeature.sdk; + +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +import static org.junit.jupiter.api.Assertions.*; + +public class ThreadLocalTransactionContextPropagatorTest { + + ThreadLocalTransactionContextPropagator contextPropagator = new ThreadLocalTransactionContextPropagator(); + + @Test + public void setTransactionContextOneThread() { + EvaluationContext firstContext = new ImmutableContext(); + contextPropagator.setTransactionContext(firstContext); + assertSame(firstContext, contextPropagator.getTransactionContext()); + EvaluationContext secondContext = new ImmutableContext(); + contextPropagator.setTransactionContext(secondContext); + assertNotSame(firstContext, contextPropagator.getTransactionContext()); + assertSame(secondContext, contextPropagator.getTransactionContext()); + } + + @Test + public void emptyTransactionContext() { + EvaluationContext result = contextPropagator.getTransactionContext(); + assertNull(result); + } + + @SneakyThrows + @Test + public void setTransactionContextTwoThreads() { + EvaluationContext firstContext = new ImmutableContext(); + EvaluationContext secondContext = new ImmutableContext(); + + Callable callable = () -> { + assertNull(contextPropagator.getTransactionContext()); + contextPropagator.setTransactionContext(secondContext); + EvaluationContext transactionContext = contextPropagator.getTransactionContext(); + assertSame(secondContext, transactionContext); + return transactionContext; + }; + contextPropagator.setTransactionContext(firstContext); + EvaluationContext firstThreadContext = contextPropagator.getTransactionContext(); + assertSame(firstContext, firstThreadContext); + + FutureTask futureTask = new FutureTask<>(callable); + Thread thread = new Thread(futureTask); + thread.start(); + EvaluationContext secondThreadContext = futureTask.get(); + + assertSame(secondContext, secondThreadContext); + assertSame(firstContext, firstThreadContext); + } +} \ No newline at end of file From 0f3c5eac19ae3c9d00cda4be31c9aaa17a69e82f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sharaev Date: Wed, 20 Mar 2024 19:14:07 +0400 Subject: [PATCH 2/4] feat: context propagation review Signed-off-by: Sviatoslav Sharaev --- README.md | 21 +++++++++++++++++++ .../sdk/NoOpTransactionContextPropagator.java | 6 +++--- .../dev/openfeature/sdk/OpenFeatureAPI.java | 4 ++-- .../openfeature/sdk/OpenFeatureClient.java | 4 +--- .../NoOpTransactionContextPropagatorTest.java | 14 +++++++++---- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d71660fa0..fd464aa4e 100644 --- a/README.md +++ b/README.md @@ -273,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 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 diff --git a/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java b/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java index f6499f4a6..a31b39b4c 100644 --- a/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java +++ b/src/main/java/dev/openfeature/sdk/NoOpTransactionContextPropagator.java @@ -1,17 +1,17 @@ package dev.openfeature.sdk; /** - * A {@link TransactionContextPropagator} that simply returns null. + * A {@link TransactionContextPropagator} that simply returns empty context. */ public class NoOpTransactionContextPropagator implements TransactionContextPropagator { /** * {@inheritDoc} - * @return null + * @return empty immutable context */ @Override public EvaluationContext getTransactionContext() { - return null; + return new ImmutableContext(); } /** diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index c7b9c0145..eb9d3a714 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -127,14 +127,14 @@ public void setTransactionContextPropagator(TransactionContextPropagator transac * * @return {@link EvaluationContext} The current transaction context */ - public EvaluationContext getTransactionContext() { + EvaluationContext getTransactionContext() { return this.transactionContextPropagator.getTransactionContext(); } /** * Sets the transaction context using the registered transaction context propagator. */ - void setTransactionContext(EvaluationContext evaluationContext) { + public void setTransactionContext(EvaluationContext evaluationContext) { this.transactionContextPropagator.setTransactionContext(evaluationContext); } diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 5fab0cfdd..3455c0c0e 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -164,9 +164,7 @@ private EvaluationContext mergeEvaluationContext( ? openfeatureApi.getTransactionContext() : new ImmutableContext(); - EvaluationContext mergedInvocationCtx = invocationContext.merge(hookContext); - - return apiContext.merge(transactionContext.merge(clientContext.merge(mergedInvocationCtx))); + return apiContext.merge(transactionContext.merge(clientContext.merge(invocationContext.merge(hookContext)))); } private ProviderEvaluation createProviderEvaluation( diff --git a/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java b/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java index 1f91451ae..06b7e93c7 100644 --- a/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java +++ b/src/test/java/dev/openfeature/sdk/NoOpTransactionContextPropagatorTest.java @@ -2,6 +2,9 @@ import org.junit.jupiter.api.Test; +import java.util.HashMap; +import java.util.Map; + import static org.junit.jupiter.api.Assertions.*; class NoOpTransactionContextPropagatorTest { @@ -11,13 +14,16 @@ class NoOpTransactionContextPropagatorTest { @Test public void emptyTransactionContext() { EvaluationContext result = contextPropagator.getTransactionContext(); - assertNull(result); + assertTrue(result.asMap().isEmpty()); } @Test public void setTransactionContext() { - EvaluationContext firstContext = new ImmutableContext(); - contextPropagator.setTransactionContext(firstContext); - assertNull(contextPropagator.getTransactionContext()); + Map transactionAttrs = new HashMap<>(); + transactionAttrs.put("userId", new Value("userId")); + EvaluationContext transactionCtx = new ImmutableContext(transactionAttrs); + contextPropagator.setTransactionContext(transactionCtx); + EvaluationContext result = contextPropagator.getTransactionContext(); + assertTrue(result.asMap().isEmpty()); } } \ No newline at end of file From 04cc4756c4c6baaa3276af49ad50db65a207f5dc Mon Sep 17 00:00:00 2001 From: Sviatoslav Sharaev Date: Thu, 21 Mar 2024 20:57:24 +0400 Subject: [PATCH 3/4] Update README.md Co-authored-by: Kavindu Dodanduwa Signed-off-by: Sviatoslav Sharaev --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd464aa4e..882f83f9f 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ OpenFeatureAPI.getInstance().shutdown(); 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: +To register a `ThreadLocal` context propagator, you can use the `setTransactionContextPropagator` method as shown below. ```java // registering the ThreadLocalTransactionContextPropagator OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); From c33f6689e0c82eff0cfd0d6579ff091533335f26 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sharaev Date: Wed, 27 Mar 2024 21:12:59 +0400 Subject: [PATCH 4/4] feat: context propagation review Signed-off-by: Sviatoslav Sharaev --- .../sdk/ThreadLocalTransactionContextPropagatorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java b/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java index 00b63c8a1..531205c16 100644 --- a/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java +++ b/src/test/java/dev/openfeature/sdk/ThreadLocalTransactionContextPropagatorTest.java @@ -52,6 +52,6 @@ public void setTransactionContextTwoThreads() { EvaluationContext secondThreadContext = futureTask.get(); assertSame(secondContext, secondThreadContext); - assertSame(firstContext, firstThreadContext); + assertSame(firstContext, contextPropagator.getTransactionContext()); } } \ No newline at end of file