Skip to content
Open
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
29 changes: 24 additions & 5 deletions src/main/java/dev/openfeature/sdk/HookSupport.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.openfeature.sdk;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
Expand All @@ -16,19 +17,37 @@
/**
* 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<Hook> hooks, FlagValueType type) {
public void setHooks(
HookSupportData hookSupportData,
Collection<Hook> providerHooks,

Check warning on line 31 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6W8-Ktp6McGkyPQEWV&open=AZ6W8-Ktp6McGkyPQEWV&pullRequest=1956
Collection<Hook> optionHooks,

Check warning on line 32 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6W8-Ktp6McGkyPQEWW&open=AZ6W8-Ktp6McGkyPQEWW&pullRequest=1956
Collection<Hook> clientHooks,

Check warning on line 33 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6W8-Ktp6McGkyPQEWX&open=AZ6W8-Ktp6McGkyPQEWX&pullRequest=1956
Collection<Hook> apiHooks,

Check warning on line 34 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6W8-Ktp6McGkyPQEWY&open=AZ6W8-Ktp6McGkyPQEWY&pullRequest=1956
FlagValueType type) {
List<Pair<Hook, HookContext>> 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<Pair<Hook, HookContext>> dest, Collection<Hook> source, FlagValueType type) {

Check warning on line 45 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6W8-Ktp6McGkyPQEWb&open=AZ6W8-Ktp6McGkyPQEWb&pullRequest=1956

Check warning on line 45 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6W8-Ktp6McGkyPQEWZ&open=AZ6W8-Ktp6McGkyPQEWZ&pullRequest=1956

Check warning on line 45 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6W8-Ktp6McGkyPQEWa&open=AZ6W8-Ktp6McGkyPQEWa&pullRequest=1956
for (Hook hook : source) {

Check warning on line 46 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6W8-Ktp6McGkyPQEWc&open=AZ6W8-Ktp6McGkyPQEWc&pullRequest=1956
if (hook.supportsFlagValueType(type)) {
hookContextPairs.add(Pair.of(hook, null));
dest.add(Pair.of(hook, null));
}
}
hookSupportData.hooks = hookContextPairs;
}
Comment on lines +29 to 51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To prevent potential NullPointerExceptions and further optimize performance, we should:

  1. Add Null Checks: Ensure that we safely handle cases where any of the hook collections (such as providerHooks or optionHooks) or individual hooks within them are null.
  2. Pre-size the ArrayList: Since we are aiming to optimize performance and eliminate unnecessary allocations, pre-sizing the ArrayList with the sum of the sizes of the non-null collections avoids internal array resizing overhead during population.
    public void setHooks(
            HookSupportData hookSupportData,
            Collection<Hook> providerHooks,
            Collection<Hook> optionHooks,
            Collection<Hook> clientHooks,
            Collection<Hook> apiHooks,
            FlagValueType type) {
        int expectedSize = 0;
        if (providerHooks != null) {
            expectedSize += providerHooks.size();
        }
        if (optionHooks != null) {
            expectedSize += optionHooks.size();
        }
        if (clientHooks != null) {
            expectedSize += clientHooks.size();
        }
        if (apiHooks != null) {
            expectedSize += apiHooks.size();
        }

        List<Pair<Hook, HookContext>> hookContextPairs = new ArrayList<>(expectedSize);
        addFilteredHooks(hookContextPairs, providerHooks, type);
        addFilteredHooks(hookContextPairs, optionHooks, type);
        addFilteredHooks(hookContextPairs, clientHooks, type);
        addFilteredHooks(hookContextPairs, apiHooks, type);
        hookSupportData.hooks = hookContextPairs;
    }

    private static void addFilteredHooks(
            List<Pair<Hook, HookContext>> dest, Collection<Hook> source, FlagValueType type) {
        if (source == null) {
            return;
        }
        for (Hook hook : source) {
            if (hook != null && hook.supportsFlagValueType(type)) {
                dest.add(Pair.of(hook, null));
            }
        }
    }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we don't know which hooks will be added to the array list, so we would be overestimating the size of the list. I'm not sure if this is worth it, and if we can come up with a benchmark that represensts real life usage

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Under 10 hooks, it looks like the runtime will allocate at least that many anyway: https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/ArrayList.java#L119 - if I'm reading this correctly.

I don't think we'll often have more than 10, so I think doing more than what's here now is a micro-optimization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Are null checks that Gemini mentions are concern?
As far as I can tell there were none in place previously, which is why I didn't consider adding them.


/**
Expand All @@ -51,10 +70,10 @@

public void executeBeforeHooks(HookSupportData data) {
// These traverse backwards from normal.
List<Pair<Hook, HookContext>> reversedHooks = new ArrayList<>(data.getHooks());

Check warning on line 73 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6nwkpbkFrxIXTN2BHH&open=AZ6nwkpbkFrxIXTN2BHH&pullRequest=1956

Check warning on line 73 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6nwkpbkFrxIXTN2BHG&open=AZ6nwkpbkFrxIXTN2BHG&pullRequest=1956
Collections.reverse(reversedHooks);

for (Pair<Hook, HookContext> hookContextPair : reversedHooks) {

Check warning on line 76 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6nwkpbkFrxIXTN2BHI&open=AZ6nwkpbkFrxIXTN2BHI&pullRequest=1956

Check warning on line 76 in src/main/java/dev/openfeature/sdk/HookSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide the parametrized type for this generic.

See more on https://sonarcloud.io/project/issues?id=open-feature_java-sdk&issues=AZ6nwkpbkFrxIXTN2BHJ&open=AZ6nwkpbkFrxIXTN2BHJ&pullRequest=1956
var hook = hookContextPair.getKey();
var hookContext = hookContextPair.getValue();

Expand Down
11 changes: 7 additions & 4 deletions src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -185,9 +184,13 @@ private <T> FlagEvaluationDetails<T> 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);
Expand Down
18 changes: 0 additions & 18 deletions src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -56,20 +54,4 @@ public static <T> T defaultIfNull(T source, Supplier<T> defaultValue) {
}
return source;
}

/**
* Concatenate a bunch of lists.
*
* @param sources bunch of lists.
* @param <T> list type
* @return resulting object
*/
@SafeVarargs
public static <T> List<T> merge(Collection<T>... sources) {
List<T> merged = new ArrayList<>();
for (Collection<T> source : sources) {
merged.addAll(source);
}
return merged;
}
}
63 changes: 58 additions & 5 deletions src/test/java/dev/openfeature/sdk/HookSupportTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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,
List.of(genericHook),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
flagValueType);

callAllHooks(hookSupportData);

Expand All @@ -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(),
List.of(testHook),
Collections.emptyList(),
Collections.emptyList(),
flagValueType);
hookSupport.setHookContexts(
hookSupportData,
getBaseHookContextForType(flagValueType),
Expand Down Expand Up @@ -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(),
Collections.emptyList(),
List.of(testHook1, testHook2),
flagValueType);
hookSupport.setHookContexts(
hookSupportData,
getBaseHookContextForType(flagValueType),
Expand All @@ -114,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();
Expand All @@ -132,7 +179,13 @@ public Optional<EvaluationContext> 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd pass the non empty list in different order for each of these tests, and I think we should also add a test to verify that the hooks are placed in the correct order in the resulting list

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adapted the tests as suggested and added an extra test to ensure order.

hookSupportData,
Collections.emptyList(),
Collections.emptyList(),
List.of(recursiveHook, emptyHook),
Collections.emptyList(),
FlagValueType.STRING);
hookSupport.setHookContexts(
hookSupportData, getBaseHookContextForType(FlagValueType.STRING), layeredEvaluationContext);

Expand Down