From 6582439fdb9706b55c3234b471e11cd610ba8dad Mon Sep 17 00:00:00 2001 From: Tobias Ibounig Date: Fri, 5 Jun 2026 09:31:13 +0200 Subject: [PATCH 1/5] perf: eliminate merge allocation in setHooks by accepting hook sources directly Signed-off-by: Tobias Ibounig --- .../java/dev/openfeature/sdk/HookSupport.java | 30 +++++++++++--- .../openfeature/sdk/OpenFeatureClient.java | 11 +++-- .../dev/openfeature/sdk/HookSupportTest.java | 41 ++++++++++++++++--- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java index 78a4f61b2..f8aebb22c 100644 --- a/src/main/java/dev/openfeature/sdk/HookSupport.java +++ b/src/main/java/dev/openfeature/sdk/HookSupport.java @@ -1,6 +1,8 @@ package dev.openfeature.sdk; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -15,19 +17,37 @@ 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}. + * Sources are iterated in order: provider → options → client → API. * * @param hookSupportData the data object to modify - * @param hooks the hooks to set + * @param providerHooks provider-level hooks + * @param optionHooks per-evaluation option hooks + * @param clientHooks client-level hooks + * @param apiHooks API-level hooks * @param type the flag value type to filter unsupported hooks */ - public void setHooks(HookSupportData hookSupportData, List hooks, FlagValueType type) { + public void setHooks( + HookSupportData hookSupportData, + Collection providerHooks, + Collection optionHooks, + Collection clientHooks, + Collection apiHooks, + FlagValueType type) { List> hookContextPairs = new ArrayList<>(); - for (Hook hook : hooks) { + addFilteredHooks(hookContextPairs, providerHooks, type); + addFilteredHooks(hookContextPairs, optionHooks, type); + addFilteredHooks(hookContextPairs, clientHooks, type); + addFilteredHooks(hookContextPairs, apiHooks, type); + hookSupportData.hooks = hookContextPairs; + } + + private static void addFilteredHooks( + List> dest, Collection source, FlagValueType type) { + for (Hook hook : source) { if (hook.supportsFlagValueType(type)) { - hookContextPairs.add(Pair.of(hook, null)); + dest.add(Pair.of(hook, null)); } } - hookSupportData.hooks = hookContextPairs; } /** diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index 117b15cc4..818583724 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -5,7 +5,6 @@ import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.OpenFeatureError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; -import dev.openfeature.sdk.internal.ObjectUtils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; import java.util.Arrays; @@ -188,9 +187,13 @@ private FlagEvaluationDetails evaluateFlag( final var state = stateManager.getState(); // Hooks are initialized as early as possible to enable the execution of error stages - var mergedHooks = ObjectUtils.merge( - provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getMutableHooks()); - hookSupport.setHooks(hookSupportData, mergedHooks, type); + hookSupport.setHooks( + hookSupportData, + provider.getProviderHooks(), + flagOptions.getHooks(), + clientHooks, + openfeatureApi.getMutableHooks(), + type); var sharedHookContext = new SharedHookContext(key, type, this.getMetadata(), provider.getMetadata(), defaultValue); diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java index 3b21aff84..98d551cb9 100644 --- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java @@ -9,6 +9,7 @@ import dev.openfeature.sdk.fixtures.HookFixtures; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,7 +39,13 @@ void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() { var sharedContext = getBaseHookContextForType(FlagValueType.STRING); var hookSupportData = new HookSupportData(); hookSupportData.evaluationContext = layered; - hookSupport.setHooks(hookSupportData, Arrays.asList(hook1, hook2), FlagValueType.STRING); + hookSupport.setHooks( + hookSupportData, + Collections.emptyList(), + Collections.emptyList(), + Arrays.asList(hook1, hook2), + Collections.emptyList(), + FlagValueType.STRING); hookSupport.setHookContexts(hookSupportData, sharedContext, layered); hookSupport.executeBeforeHooks(hookSupportData); @@ -57,7 +64,13 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) { Hook genericHook = mockGenericHook(); var hookSupportData = new HookSupportData(); - hookSupport.setHooks(hookSupportData, List.of(genericHook), flagValueType); + hookSupport.setHooks( + hookSupportData, + Collections.emptyList(), + Collections.emptyList(), + List.of(genericHook), + Collections.emptyList(), + flagValueType); callAllHooks(hookSupportData); @@ -73,7 +86,13 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) { void shouldPassDataAcrossStages(FlagValueType flagValueType) { var testHook = new TestHookWithData(); var hookSupportData = new HookSupportData(); - hookSupport.setHooks(hookSupportData, List.of(testHook), flagValueType); + hookSupport.setHooks( + hookSupportData, + Collections.emptyList(), + Collections.emptyList(), + List.of(testHook), + Collections.emptyList(), + flagValueType); hookSupport.setHookContexts( hookSupportData, getBaseHookContextForType(flagValueType), @@ -102,7 +121,13 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) { var testHook2 = new TestHookWithData(2); var hookSupportData = new HookSupportData(); - hookSupport.setHooks(hookSupportData, List.of(testHook1, testHook2), flagValueType); + hookSupport.setHooks( + hookSupportData, + Collections.emptyList(), + Collections.emptyList(), + List.of(testHook1, testHook2), + Collections.emptyList(), + flagValueType); hookSupport.setHookContexts( hookSupportData, getBaseHookContextForType(flagValueType), @@ -132,7 +157,13 @@ public Optional before(HookContext ctx, Map hints) { var layeredEvaluationContext = new LayeredEvaluationContext(evaluationContextWithValue("key", "value"), null, null, null); hookSupportData.evaluationContext = layeredEvaluationContext; - hookSupport.setHooks(hookSupportData, List.of(recursiveHook, emptyHook), FlagValueType.STRING); + hookSupport.setHooks( + hookSupportData, + Collections.emptyList(), + Collections.emptyList(), + List.of(recursiveHook, emptyHook), + Collections.emptyList(), + FlagValueType.STRING); hookSupport.setHookContexts( hookSupportData, getBaseHookContextForType(FlagValueType.STRING), layeredEvaluationContext); From 187eb047c5e1464178dd34dd8050c5f790e4655e Mon Sep 17 00:00:00 2001 From: Tobias Ibounig Date: Mon, 8 Jun 2026 17:02:35 +0200 Subject: [PATCH 2/5] improve test, to ensure hook order. Signed-off-by: Tobias Ibounig --- .../dev/openfeature/sdk/HookSupportTest.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/test/java/dev/openfeature/sdk/HookSupportTest.java b/src/test/java/dev/openfeature/sdk/HookSupportTest.java index 98d551cb9..f7583321a 100644 --- a/src/test/java/dev/openfeature/sdk/HookSupportTest.java +++ b/src/test/java/dev/openfeature/sdk/HookSupportTest.java @@ -66,9 +66,9 @@ void shouldAlwaysCallGenericHook(FlagValueType flagValueType) { var hookSupportData = new HookSupportData(); hookSupport.setHooks( hookSupportData, + List.of(genericHook), Collections.emptyList(), Collections.emptyList(), - List.of(genericHook), Collections.emptyList(), flagValueType); @@ -89,9 +89,9 @@ void shouldPassDataAcrossStages(FlagValueType flagValueType) { hookSupport.setHooks( hookSupportData, Collections.emptyList(), - Collections.emptyList(), List.of(testHook), Collections.emptyList(), + Collections.emptyList(), flagValueType); hookSupport.setHookContexts( hookSupportData, @@ -125,8 +125,8 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) { hookSupportData, Collections.emptyList(), Collections.emptyList(), - List.of(testHook1, testHook2), Collections.emptyList(), + List.of(testHook1, testHook2), flagValueType); hookSupport.setHookContexts( hookSupportData, @@ -139,6 +139,28 @@ void shouldIsolateDataBetweenHooks(FlagValueType flagValueType) { assertHookData(testHook2, 2, "before", "after", "finallyAfter", "error"); } + @Test + @DisplayName("should place hooks in provider → options → client → API order") + void shouldOrderHooksBySource() { + Hook providerHook = mockGenericHook(); + Hook optionHook = mockGenericHook(); + Hook clientHook = mockGenericHook(); + Hook apiHook = mockGenericHook(); + + var hookSupportData = new HookSupportData(); + hookSupport.setHooks( + hookSupportData, + List.of(providerHook), + List.of(optionHook), + List.of(clientHook), + List.of(apiHook), + FlagValueType.STRING); + + assertThat(hookSupportData.getHooks()) + .extracting(Pair::getKey) + .containsExactly(providerHook, optionHook, clientHook, apiHook); + } + @Test void hookThatReturnsTheGivenContext_doesNotResultInAStackOverflow() { var hookSupportData = new HookSupportData(); From 1de90b149e83e7a8c198a7ebb089569ed8faa1b9 Mon Sep 17 00:00:00 2001 From: Tobias Ibounig Date: Tue, 9 Jun 2026 10:55:48 +0200 Subject: [PATCH 3/5] Remove unused ObjectUtils#merge Signed-off-by: Tobias Ibounig --- .../openfeature/sdk/internal/ObjectUtils.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java index 86a9ddd70..95a63b375 100644 --- a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java +++ b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java @@ -1,7 +1,5 @@ package dev.openfeature.sdk.internal; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -56,20 +54,4 @@ public static T defaultIfNull(T source, Supplier defaultValue) { } return source; } - - /** - * Concatenate a bunch of lists. - * - * @param sources bunch of lists. - * @param list type - * @return resulting object - */ - @SafeVarargs - public static List merge(Collection... sources) { - List merged = new ArrayList<>(); - for (Collection source : sources) { - merged.addAll(source); - } - return merged; - } } From 940651ab903284af3cb99d18c6c3cdfe1b69e953 Mon Sep 17 00:00:00 2001 From: Tobias Ibounig Date: Thu, 11 Jun 2026 12:43:46 +0200 Subject: [PATCH 4/5] reintroduce and deprecate ObjectUtils.merge Signed-off-by: Tobias Ibounig --- .../java/dev/openfeature/sdk/HookSupport.java | 1 - .../openfeature/sdk/internal/ObjectUtils.java | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java index f8aebb22c..5586dface 100644 --- a/src/main/java/dev/openfeature/sdk/HookSupport.java +++ b/src/main/java/dev/openfeature/sdk/HookSupport.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java index 95a63b375..298712f13 100644 --- a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java +++ b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java @@ -1,5 +1,7 @@ package dev.openfeature.sdk.internal; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -54,4 +56,22 @@ public static T defaultIfNull(T source, Supplier defaultValue) { } return source; } + + /** + * Concatenate a bunch of lists. + * + * @param sources bunch of lists. + * @param list type + * @return resulting object + * @deprecated Not used in the project anymore. This method will be removed in a future release. + */ + @Deprecated(forRemoval = true) + @SafeVarargs + public static List merge(Collection... sources) { + List merged = new ArrayList<>(); + for (Collection source : sources) { + merged.addAll(source); + } + return merged; + } } From 4f6554dcf4305060003712a5b06280bf519d78a2 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 11 Jun 2026 08:39:28 -0400 Subject: [PATCH 5/5] fixup: added comment Signed-off-by: Todd Baert --- src/main/java/dev/openfeature/sdk/HookSupport.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/openfeature/sdk/HookSupport.java b/src/main/java/dev/openfeature/sdk/HookSupport.java index 5586dface..e936fdeda 100644 --- a/src/main/java/dev/openfeature/sdk/HookSupport.java +++ b/src/main/java/dev/openfeature/sdk/HookSupport.java @@ -16,7 +16,10 @@ 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}. - * Sources are iterated in order: provider → options → client → API. + * Sources are iterated in order: provider, options, client, API (reversed for the {@code before} stage by + * {@link #executeBeforeHooks}). + * + *

The four hook sources are accepted as separate collections to avoid allocation on the evaluation hot path. * * @param hookSupportData the data object to modify * @param providerHooks provider-level hooks