diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java index c7a7630da..ab732d6b1 100644 --- a/src/main/java/dev/openfeature/sdk/HookSupport.java +++ b/src/main/java/dev/openfeature/sdk/HookSupport.java @@ -17,9 +17,9 @@ class HookSupport { * Sets the {@link Hook}-{@link HookContext}-{@link Pair} list in the given data object with {@link HookContext} * set to null. Filters hooks by supported {@link FlagValueType}. * - * @param hookSupportData the data object to modify - * @param hooks the hooks to set - * @param type the flag value type to filter unsupported hooks + * @param hookSupportData the data object to modify + * @param hooks the hooks to set + * @param type the flag value type to filter unsupported hooks */ public void setHooks(HookSupportData hookSupportData, List hooks, FlagValueType type) { List> hookContextPairs = new ArrayList<>(); @@ -35,35 +35,20 @@ public void setHooks(HookSupportData hookSupportData, List hooks, FlagValu * Creates & sets a {@link HookContext} for every {@link Hook}-{@link HookContext}-{@link Pair} * in the given data object with a new {@link HookData} instance. * - * @param hookSupportData the data object to modify - * @param sharedContext the shared context from which the new {@link HookContext} is created + * @param hookSupportData the data object to modify + * @param sharedContext the shared context from which the new {@link HookContext} is created */ - public void setHookContexts(HookSupportData hookSupportData, SharedHookContext sharedContext) { + public void setHookContexts( + HookSupportData hookSupportData, + SharedHookContext sharedContext, + LayeredEvaluationContext evaluationContext) { for (int i = 0; i < hookSupportData.hooks.size(); i++) { Pair hookContextPair = hookSupportData.hooks.get(i); - HookContext curHookContext = sharedContext.hookContextFor(null, new DefaultHookData()); + HookContext curHookContext = sharedContext.hookContextFor(evaluationContext, new DefaultHookData()); hookContextPair.setValue(curHookContext); } } - /** - * Updates the evaluation context in the given data object's eval context and each hooks eval context. - * - * @param hookSupportData the data object to modify - * @param evaluationContext the new context to set - */ - public void updateEvaluationContext(HookSupportData hookSupportData, EvaluationContext evaluationContext) { - hookSupportData.evaluationContext = evaluationContext; - if (hookSupportData.hooks != null) { - for (Pair hookContextPair : hookSupportData.hooks) { - var curHookContext = hookContextPair.getValue(); - if (curHookContext != null) { - curHookContext.setCtx(evaluationContext); - } - } - } - } - public void executeBeforeHooks(HookSupportData data) { // These traverse backwards from normal. List> reversedHooks = new ArrayList<>(data.getHooks()); @@ -77,8 +62,10 @@ public void executeBeforeHooks(HookSupportData data) { hook.before(hookContext, data.getHints())) .orElse(Optional.empty()); if (returnedEvalContext.isPresent()) { - // update shared evaluation context for all hooks - updateEvaluationContext(data, data.getEvaluationContext().merge(returnedEvalContext.get())); + var returnedContext = returnedEvalContext.get(); + if (!returnedContext.isEmpty()) { + data.evaluationContext.putHookContext(returnedContext.asMap()); + } } } } diff --git a/src/main/java/dev/openfeature/sdk/HookSupportData.java b/src/main/java/dev/openfeature/sdk/HookSupportData.java index 2d3346ba1..174702ea2 100644 --- a/src/main/java/dev/openfeature/sdk/HookSupportData.java +++ b/src/main/java/dev/openfeature/sdk/HookSupportData.java @@ -11,7 +11,7 @@ class HookSupportData { List> hooks; - EvaluationContext evaluationContext; + LayeredEvaluationContext evaluationContext; Map hints; HookSupportData() {} diff --git a/src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java b/src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java new file mode 100644 index 000000000..3f6eea4bc --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/LayeredEvaluationContext.java @@ -0,0 +1,212 @@ +package dev.openfeature.sdk; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * LayeredEvaluationContext implements EvaluationContext by layering multiple contexts: + * API-level, Transaction-level, Client-level, Invocation-level, and Hook-level. + * The contexts are checked in that order for values, with Hook-level having the highest precedence. + */ +public class LayeredEvaluationContext implements EvaluationContext { + private final EvaluationContext apiContext; + private final EvaluationContext transactionContext; + private final EvaluationContext clientContext; + private final EvaluationContext invocationContext; + private final HashMap hookContext = new HashMap<>(); + + private String targetingKey; + private Set keySet = null; + + /** + * Constructor for LayeredEvaluationContext. + */ + public LayeredEvaluationContext( + EvaluationContext apiContext, + EvaluationContext transactionContext, + EvaluationContext clientContext, + EvaluationContext invocationContext) { + this.apiContext = apiContext; + this.transactionContext = transactionContext; + this.clientContext = clientContext; + this.invocationContext = invocationContext; + + if (invocationContext != null && invocationContext.getTargetingKey() != null) { + this.targetingKey = invocationContext.getTargetingKey(); + } else if (clientContext != null && clientContext.getTargetingKey() != null) { + this.targetingKey = clientContext.getTargetingKey(); + } else if (transactionContext != null && transactionContext.getTargetingKey() != null) { + this.targetingKey = transactionContext.getTargetingKey(); + } else if (apiContext != null && apiContext.getTargetingKey() != null) { + this.targetingKey = apiContext.getTargetingKey(); + } else { + this.targetingKey = null; + } + } + + @Override + public String getTargetingKey() { + return targetingKey; + } + + /** + * Using this method should be avoided as it comes with a performance cost. + * Consider constructing a new LayeredEvaluationContext instead. + * + *

+ * Does not modify this object. + * + * @param overridingContext overriding context + * @return A new LayeredEvaluationContext containing the context from this object, with the overridingContext + * merged on top. + * @deprecated Use of this method is discouraged due to performance considerations. + */ + @Deprecated + @Override + public EvaluationContext merge(EvaluationContext overridingContext) { + var merged = new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + merged.hookContext.putAll(this.hookContext); + merged.hookContext.putAll(overridingContext.asMap()); + var otherTargetingKey = overridingContext.getTargetingKey(); + if (otherTargetingKey != null) { + merged.targetingKey = otherTargetingKey; + } + return merged; + } + + @Override + public boolean isEmpty() { + return hookContext.isEmpty() + && (invocationContext == null || invocationContext.isEmpty()) + && (clientContext == null || clientContext.isEmpty()) + && (transactionContext == null || transactionContext.isEmpty()) + && (apiContext == null || apiContext.isEmpty()); + } + + @Override + public Set keySet() { + return Collections.unmodifiableSet(ensureKeySet()); + } + + private Set ensureKeySet() { + if (this.keySet != null) { + return this.keySet; + } + + var keys = new HashSet<>(hookContext.keySet()); + + if (invocationContext != null) { + keys.addAll(invocationContext.keySet()); + } + if (clientContext != null) { + keys.addAll(clientContext.keySet()); + } + if (transactionContext != null) { + keys.addAll(transactionContext.keySet()); + } + if (apiContext != null) { + keys.addAll(apiContext.keySet()); + } + this.keySet = keys; + return keys; + } + + private Value getFromContext(EvaluationContext context, String key) { + if (context != null) { + return context.getValue(key); + } + return null; + } + + private Value getFromContext(HashMap context, String key) { + if (context != null) { + return context.get(key); + } + return null; + } + + @Override + public Value getValue(String key) { + var hookValue = getFromContext(hookContext, key); + if (hookValue != null) { + return hookValue; + } + var invocationValue = getFromContext(invocationContext, key); + if (invocationValue != null) { + return invocationValue; + } + var clientValue = getFromContext(clientContext, key); + if (clientValue != null) { + return clientValue; + } + var transactionValue = getFromContext(transactionContext, key); + if (transactionValue != null) { + return transactionValue; + } + return getFromContext(apiContext, key); + } + + @Override + public Map asMap() { + var keySet = ensureKeySet(); + var keys = keySet.size(); + if (keys == 0) { + return new HashMap<>(1); + } + var map = new HashMap(keys); + + for (String key : keySet) { + map.put(key, getValue(key)); + } + return map; + } + + @Override + public Map asUnmodifiableMap() { + var keySet = ensureKeySet(); + var keys = keySet.size(); + if (keys == 0) { + return Collections.emptyMap(); + } + var map = new HashMap(keys); + + for (String key : keySet) { + map.put(key, getValue(key)); + } + return Collections.unmodifiableMap(map); + } + + @Override + public Map asObjectMap() { + var keySet = ensureKeySet(); + var keys = keySet.size(); + if (keys == 0) { + return new HashMap<>(1); + } + var map = new HashMap(keys); + + for (String key : keySet) { + map.put(key, convertValue(getValue(key))); + } + return map; + } + + void putHookContext(Map context) { + if (context == null) { + return; + } + + var targetingKey = context.get("targetingKey"); + if (targetingKey != null) { + var targetingKeyStr = targetingKey.asString(); + if (targetingKeyStr != null) { + this.targetingKey = targetingKeyStr; + this.hookContext.put("targetingKey", targetingKey); + } + } + this.hookContext.putAll(context); + } +} diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 614bc1e34..c5548b394 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -166,6 +166,12 @@ private FlagEvaluationDetails evaluateFlag( var flagOptions = ObjectUtils.defaultIfNull( options, () -> FlagEvaluationOptions.builder().build()); hookSupportData.hints = Collections.unmodifiableMap(flagOptions.getHookHints()); + var context = new LayeredEvaluationContext( + openfeatureApi.getEvaluationContext(), + openfeatureApi.getTransactionContext(), + evaluationContext.get(), + ctx); + hookSupportData.evaluationContext = context; try { final var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain); @@ -180,10 +186,7 @@ private FlagEvaluationDetails evaluateFlag( var sharedHookContext = new SharedHookContext(key, type, this.getMetadata(), provider.getMetadata(), defaultValue); - hookSupport.setHookContexts(hookSupportData, sharedHookContext); - - var evalContext = mergeEvaluationContext(ctx); - hookSupport.updateEvaluationContext(hookSupportData, evalContext); + hookSupport.setHookContexts(hookSupportData, sharedHookContext, context); hookSupport.executeBeforeHooks(hookSupportData); diff --git a/src/test/java/dev/openfeature/sdk/HookSpecTest.java b/src/test/java/dev/openfeature/sdk/HookSpecTest.java index 163007120..69e47f353 100644 --- a/src/test/java/dev/openfeature/sdk/HookSpecTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSpecTest.java @@ -723,7 +723,7 @@ void mergeHappensCorrectly() { invocationCtx, FlagEvaluationOptions.builder().hook(hook).build()); - ArgumentCaptor captor = ArgumentCaptor.forClass(ImmutableContext.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(LayeredEvaluationContext.class); verify(provider).getBooleanEvaluation(any(), any(), captor.capture()); EvaluationContext ec = captor.getValue(); assertEquals("works", ec.getValue("test").asString()); diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java index b1bb70ba1..ef8dcc396 100644 --- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java @@ -33,11 +33,12 @@ void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() { when(hook1.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("bla", "blubber"))); when(hook2.before(any(), any())).thenReturn(Optional.of(evaluationContextWithValue("foo", "bar"))); + var layered = new LayeredEvaluationContext(baseEvalContext, null, null, null); var sharedContext = getBaseHookContextForType(FlagValueType.STRING); var hookSupportData = new HookSupportData(); + hookSupportData.evaluationContext = layered; hookSupport.setHooks(hookSupportData, Arrays.asList(hook1, hook2), FlagValueType.STRING); - hookSupport.setHookContexts(hookSupportData, sharedContext); - hookSupport.updateEvaluationContext(hookSupportData, baseEvalContext); + hookSupport.setHookContexts(hookSupportData, sharedContext, layered); hookSupport.executeBeforeHooks(hookSupportData); @@ -72,7 +73,10 @@ void shouldPassDataAcrossStages(FlagValueType flagValueType) { var testHook = new TestHookWithData(); var hookSupportData = new HookSupportData(); hookSupport.setHooks(hookSupportData, List.of(testHook), flagValueType); - hookSupport.setHookContexts(hookSupportData, getBaseHookContextForType(flagValueType)); + hookSupport.setHookContexts( + hookSupportData, + getBaseHookContextForType(flagValueType), + new LayeredEvaluationContext(null, null, null, null)); hookSupport.executeBeforeHooks(hookSupportData); assertHookData(testHook, "before"); @@ -98,7 +102,10 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) { var hookSupportData = new HookSupportData(); hookSupport.setHooks(hookSupportData, List.of(testHook1, testHook2), flagValueType); - hookSupport.setHookContexts(hookSupportData, getBaseHookContextForType(flagValueType)); + hookSupport.setHookContexts( + hookSupportData, + getBaseHookContextForType(flagValueType), + new LayeredEvaluationContext(null, null, null, null)); callAllHooks(hookSupportData); diff --git a/src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java b/src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java new file mode 100644 index 000000000..f09140c5d --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java @@ -0,0 +1,256 @@ +package dev.openfeature.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class LayeredEvaluationContextTest { + final EvaluationContext apiContext = + new MutableContext("api-level", Map.of("api", new Value("api"), "override", new Value("api"))); + final EvaluationContext transactionContext = new MutableContext( + "transaction-level", Map.of("transaction", new Value("transaction"), "override", new Value("transaction"))); + final EvaluationContext clientContext = + new MutableContext("client-level", Map.of("client", new Value("client"), "override", new Value("client"))); + final EvaluationContext invocationContext = new MutableContext( + "invocation-level", Map.of("invocation", new Value("invocation"), "override", new Value("invocation"))); + + @Test + void creatingLayeredContextWithNullsWorks() { + LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(null, null, null, null); + assertNotNull(layeredContext); + assertNull(layeredContext.getTargetingKey()); + assertEquals(Map.of(), layeredContext.asMap()); + assertEquals(Map.of(), layeredContext.asObjectMap()); + assertEquals(Map.of(), layeredContext.asUnmodifiableMap()); + assertEquals(Set.of(), layeredContext.keySet()); + assertTrue(layeredContext.isEmpty()); + } + + @Nested + class TargetingKey { + @Test + void hookWins() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + layeredContext.putHookContext(Map.of("targetingKey", new Value("hook-level"))); + assertEquals("hook-level", layeredContext.getTargetingKey()); + } + + @Test + void invocationWinsIfHookNotSet() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + assertEquals("invocation-level", layeredContext.getTargetingKey()); + } + + @Test + void clientWinsIfInvocationAndHookNotSet() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, null); + assertEquals("client-level", layeredContext.getTargetingKey()); + } + + @Test + void transactionWinsIfClientInvocationAndHookNotSet() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, null, null); + assertEquals("transaction-level", layeredContext.getTargetingKey()); + } + + @Test + void apiWinsIfTransactionClientInvocationAndHookNotSet() { + LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(apiContext, null, null, null); + assertEquals("api-level", layeredContext.getTargetingKey()); + } + } + + @Nested + class GetValue { + @Test + void doesNotOverrideUniqueValues() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + layeredContext.putHookContext(Map.of("hook", new Value("hook"), "targetingKey", new Value("hook-level"))); + + assertEquals("hook", layeredContext.getValue("hook").asString()); + assertEquals("invocation", layeredContext.getValue("invocation").asString()); + assertEquals("client", layeredContext.getValue("client").asString()); + assertEquals("transaction", layeredContext.getValue("transaction").asString()); + assertEquals("api", layeredContext.getValue("api").asString()); + } + + @Test + void hookWins() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + layeredContext.putHookContext( + Map.of("override", new Value("hook"), "targetingKey", new Value("hook-level"))); + assertEquals("hook", layeredContext.getValue("override").asString()); + } + + @Test + void invocationWinsIfHookNotSet() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + assertEquals("invocation", layeredContext.getValue("override").asString()); + } + + @Test + void clientWinsIfInvocationAndHookNotSet() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, null); + assertEquals("client", layeredContext.getValue("override").asString()); + } + + @Test + void transactionWinsIfClientInvocationAndHookNotSet() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, null, null); + assertEquals("transaction", layeredContext.getValue("override").asString()); + } + + @Test + void apiWinsIfTransactionClientInvocationAndHookNotSet() { + LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(apiContext, null, null, null); + assertEquals("api", layeredContext.getValue("override").asString()); + } + } + + @Nested + class KeySet { + @Test + void keySetIsGeneratedCorrectly() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + layeredContext.putHookContext(Map.of("hook", new Value("hook"), "targetingKey", new Value("hook-level"))); + + Set expectedKeys = Set.of( + "hook", + "invocation", + "client", + "transaction", + "api", + "override", + "targetingKey" // expected, even though not explicitly set + ); + + assertEquals(expectedKeys, layeredContext.keySet()); + } + } + + @Nested + class AsMap { + @Test + void mapIsGeneratedCorrectly() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + layeredContext.putHookContext(Map.of( + "hook", new Value("hook"), "override", new Value("hook"), "targetingKey", new Value("hook-level"))); + + var expectedKeys = Map.of( + "hook", new Value("hook"), + "invocation", new Value("invocation"), + "client", new Value("client"), + "transaction", new Value("transaction"), + "api", new Value("api"), + "override", new Value("hook"), + "targetingKey", new Value("hook-level") // expected, even though not explicitly set + ); + + assertEquals(expectedKeys, layeredContext.asMap()); + assertEquals(expectedKeys, layeredContext.asUnmodifiableMap()); + } + } + + @Nested + class AsObjectMap { + @Test + void mapIsGeneratedCorrectly() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + layeredContext.putHookContext(Map.of( + "hook", new Value("hook"), "override", new Value("hook"), "targetingKey", new Value("hook-level"))); + + var expectedKeys = Map.of( + "hook", "hook", + "invocation", "invocation", + "client", "client", + "transaction", "transaction", + "api", "api", + "override", "hook", + "targetingKey", "hook-level" // expected, even though not explicitly set in map + ); + + assertEquals(expectedKeys, layeredContext.asObjectMap()); + } + } + + @Nested + class IsEmpty { + @Test + void isEmptyWhenAllContextsAreNull() { + LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(null, null, null, null); + assertTrue(layeredContext.isEmpty()); + } + + @Test + void isNotEmptyWhenApiContextIsSet() { + LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(apiContext, null, null, null); + assertFalse(layeredContext.isEmpty()); + } + + @Test + void isNotEmptyWhenTransactionContextIsSet() { + LayeredEvaluationContext layeredContext = + new LayeredEvaluationContext(null, transactionContext, null, null); + assertFalse(layeredContext.isEmpty()); + } + + @Test + void isNotEmptyWhenClientContextIsSet() { + LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(null, null, clientContext, null); + assertFalse(layeredContext.isEmpty()); + } + + @Test + void isNotEmptyWhenInvocationContextIsSet() { + LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(null, null, null, invocationContext); + assertFalse(layeredContext.isEmpty()); + } + + @Test + void isNotEmptyWhenHookContextIsSet() { + LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(null, null, null, null); + layeredContext.putHookContext(Map.of("hook", new Value("hook"), "targetingKey", new Value("hook-level"))); + assertFalse(layeredContext.isEmpty()); + } + } + + @Nested + class Merge { + @Test + void mergesCorrectly() { + LayeredEvaluationContext ctx1 = + new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext); + EvaluationContext ctx2 = new MutableContext( + "mutable", Map.of("override", new Value("other"), "unique", new Value("unique"))); + + EvaluationContext merged = ctx1.merge(ctx2); + + assertEquals( + Map.of( + "invocation", new Value("invocation"), + "client", new Value("client"), + "transaction", new Value("transaction"), + "api", new Value("api"), + "override", new Value("other"), + "targetingKey", new Value("mutable"), + "unique", new Value("unique")), + merged.asMap()); + assertEquals("mutable", merged.getTargetingKey()); + } + } +} diff --git a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java index 91509bd45..31937ec2d 100644 --- a/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java +++ b/src/test/java/dev/openfeature/sdk/OpenFeatureClientTest.java @@ -12,6 +12,8 @@ import dev.openfeature.sdk.fixtures.HookFixtures; import dev.openfeature.sdk.testutils.testProvider.TestProvider; import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -135,4 +137,53 @@ void shouldSupportUsageOfHookData(boolean isError) { assertThat(testHook.hookData.get("error")).isEqualTo(null); } } + + @Test + void flagEvaluationsUseTheCorrectContext() { + OpenFeatureAPI api = new OpenFeatureAPI(); + api.setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); + var provider = TestProvider.builder().allowUnknownFlags(true).initsToReady(); + api.setProviderAndWait(provider); + + var apiContext = new MutableContext("api-level", Map.of("api", new Value("api"), "override", new Value("api"))); + var transactionContext = new MutableContext( + "transaction-level", + Map.of("transaction", new Value("transaction"), "override", new Value("transaction"))); + var clientContext = new MutableContext( + "client-level", Map.of("client", new Value("client"), "override", new Value("client"))); + var invocationContext = new MutableContext( + "invocation-level", Map.of("invocation", new Value("invocation"), "override", new Value("invocation"))); + + var hookContext = + new MutableContext("hook-level", Map.of("hook", new Value("hook"), "override", new Value("hook"))); + var ctxHook = new Hook<>() { + @Override + public Optional before(HookContext ctx, Map hints) { + return Optional.of(hookContext); + } + }; + + api.setEvaluationContext(apiContext); + api.setTransactionContext(transactionContext); + + var client = api.getClient(); + client.addHooks(ctxHook); + client.setEvaluationContext(clientContext); + + client.getStringValue("flag", "idc", invocationContext); + + var flagEvaluations = provider.getFlagEvaluations(); + assertThat(flagEvaluations).hasSize(1); + + var evaluation = flagEvaluations.get(0); + assertThat(evaluation.evaluationContext.getValue("api").asString()).isEqualTo("api"); + assertThat(evaluation.evaluationContext.getValue("transaction").asString()) + .isEqualTo("transaction"); + assertThat(evaluation.evaluationContext.getValue("client").asString()).isEqualTo("client"); + assertThat(evaluation.evaluationContext.getValue("invocation").asString()) + .isEqualTo("invocation"); + assertThat(evaluation.evaluationContext.getValue("hook").asString()).isEqualTo("hook"); + assertThat(evaluation.evaluationContext.getValue("override").asString()).isEqualTo("hook"); + assertThat(evaluation.evaluationContext.getTargetingKey()).isEqualTo("hook-level"); + } } diff --git a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java index d6a03efd6..1b9a9f535 100644 --- a/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java +++ b/src/test/java/dev/openfeature/sdk/benchmark/AllocationBenchmark.java @@ -23,9 +23,8 @@ import java.util.Map; import java.util.Optional; import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.infra.Blackhole; /** * Runs a large volume of flag evaluations on a VM with 1G memory and GC @@ -36,9 +35,9 @@ public class AllocationBenchmark { // 10K iterations works well with Xmx1024m (we don't want to run out of memory) private static final int ITERATIONS = 10000; - @Benchmark - @BenchmarkMode(Mode.SingleShotTime) - @Fork(jvmArgsAppend = {"-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"}) + // @Benchmark + // @BenchmarkMode(Mode.SingleShotTime) + // @Fork(jvmArgsAppend = {"-Xmx1024m", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseEpsilonGC"}) public void run() { OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); @@ -95,4 +94,63 @@ public Optional before(HookContext ctx, Map transactionAttr; + public final Map transactionAttr2; + public final EvaluationContext invocationContext; + + public AllocationBenchmarkState() { + long start = System.currentTimeMillis(); + OpenFeatureAPI.getInstance().setProviderAndWait(new NoOpProvider()); + OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator()); + long end = System.currentTimeMillis(); + System.out.println("Setup time: " + (end - start) + "ms"); + Map globalAttrs = new HashMap<>(); + globalAttrs.put("global", new Value(1)); + EvaluationContext globalContext = new ImmutableContext(globalAttrs); + OpenFeatureAPI.getInstance().setEvaluationContext(globalContext); + + client = OpenFeatureAPI.getInstance().getClient(); + + Map clientAttrs = new HashMap<>(); + clientAttrs.put("client", new Value(2)); + client.setEvaluationContext(new ImmutableContext(clientAttrs)); + + transactionAttr = new HashMap<>(); + transactionAttr.put("trans", new Value(4)); + + transactionAttr2 = new HashMap<>(); + transactionAttr2.put("trans2", new Value(5)); + + Map invocationAttrs = new HashMap<>(); + invocationAttrs.put("invoke", new Value(3)); + invocationContext = new ImmutableContext(invocationAttrs); + } +}