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
17 changes: 17 additions & 0 deletions src/main/java/dev/openfeature/sdk/FeatureProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ default List<Hook> getProviderHooks() {
return new ArrayList<>();
}

/**
* Returns all hooks that support the given flag value type.
*
* @param flagType the flag value type to support
* @return a list of hooks that support the given flag value type
*/
default List<Hook> getProviderHooks(FlagValueType flagType) {
var allHooks = getProviderHooks();
var filteredHooks = new ArrayList<Hook>(allHooks.size());
for (Hook hook : allHooks) {
if (hook.supportsFlagValueType(flagType)) {
filteredHooks.add(hook);
}
}
return filteredHooks;
}

ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx);

ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx);
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/dev/openfeature/sdk/FlagEvaluationOptions.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.openfeature.sdk;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -15,4 +17,18 @@ public class FlagEvaluationOptions {

@Builder.Default
Map<String, Object> hookHints = new HashMap<>();

List<Hook> getHooks(FlagValueType flagValueType) {
if (hooks == null || hooks.isEmpty()) {
return Collections.emptyList();
}

var result = new ArrayList<Hook>(hooks.size());
for (var hook : hooks) {
if (hook.supportsFlagValueType(flagValueType)) {
result.add(hook);
}
}
return result;
}
}
65 changes: 49 additions & 16 deletions src/main/java/dev/openfeature/sdk/HookSupport.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package dev.openfeature.sdk;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentLinkedQueue;
import lombok.extern.slf4j.Slf4j;

/**
Expand All @@ -17,26 +18,59 @@ 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 providerHooks the hooks filtered for the proper flag value type from the respective layer
* @param flagOptionsHooks the hooks filtered for the proper flag value type from the respective layer
* @param clientHooks the hooks filtered for the proper flag value type from the respective layer
* @param apiHooks the hooks filtered for the proper flag value type from the respective layer
*/
public void setHooks(HookSupportData hookSupportData, List<Hook> hooks, FlagValueType type) {
List<Pair<Hook, HookContext>> hookContextPairs = new ArrayList<>();
for (Hook hook : hooks) {
if (hook.supportsFlagValueType(type)) {
hookContextPairs.add(Pair.of(hook, null));
}
public void setHooks(
HookSupportData hookSupportData,
List<Hook> providerHooks,
List<Hook> flagOptionsHooks,
ConcurrentLinkedQueue<Hook> clientHooks,
ConcurrentLinkedQueue<Hook> apiHooks) {
var lengthEstimate = 0;

if (providerHooks != null) {
lengthEstimate += providerHooks.size();
}
if (flagOptionsHooks != null) {
lengthEstimate += flagOptionsHooks.size();
}
if (clientHooks != null) {
lengthEstimate += clientHooks.size();
}
if (apiHooks != null) {
lengthEstimate += apiHooks.size();
}

ArrayList<Pair<Hook, HookContext>> hookContextPairs = new ArrayList<>(lengthEstimate);

addAll(hookContextPairs, providerHooks);
addAll(hookContextPairs, flagOptionsHooks);
addAll(hookContextPairs, clientHooks);
addAll(hookContextPairs, apiHooks);

hookSupportData.hooks = hookContextPairs;
}

private void addAll(List<Pair<Hook, HookContext>> accumulator, Collection<Hook> toAdd) {
if (toAdd == null || toAdd.isEmpty()) {
return;
}

for (Hook hook : toAdd) {
accumulator.add(Pair.of(hook, null));
}
}

/**
* 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) {
for (int i = 0; i < hookSupportData.hooks.size(); i++) {
Expand Down Expand Up @@ -66,10 +100,9 @@ public void updateEvaluationContext(HookSupportData hookSupportData, EvaluationC

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

for (Pair<Hook, HookContext> hookContextPair : reversedHooks) {
var hooks = data.getHooks();
for (int i = hooks.size() - 1; i >= 0; i--) {
var hookContextPair = hooks.get(i);
var hook = hookContextPair.getKey();
var hookContext = hookContextPair.getValue();

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/dev/openfeature/sdk/HookSupportData.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dev.openfeature.sdk;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import lombok.Getter;

Expand All @@ -10,7 +10,7 @@
@Getter
class HookSupportData {

List<Pair<Hook, HookContext>> hooks;
ArrayList<Pair<Hook, HookContext>> hooks;
EvaluationContext evaluationContext;
Map<String, Object> hints;

Expand Down
38 changes: 28 additions & 10 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
Expand All @@ -24,14 +24,18 @@
public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
// package-private multi-read/single-write lock
static AutoCloseableReentrantReadWriteLock lock = new AutoCloseableReentrantReadWriteLock();
private final ConcurrentLinkedQueue<Hook> apiHooks;
private final ConcurrentHashMap<FlagValueType, ConcurrentLinkedQueue<Hook>> apiHooks;
private ProviderRepository providerRepository;
private EventSupport eventSupport;
private final AtomicReference<EvaluationContext> evaluationContext = new AtomicReference<>();
private TransactionContextPropagator transactionContextPropagator;

protected OpenFeatureAPI() {
apiHooks = new ConcurrentLinkedQueue<>();
var values = FlagValueType.values();
apiHooks = new ConcurrentHashMap<>(values.length);
for (FlagValueType value : values) {
apiHooks.put(value, new ConcurrentLinkedQueue<>());
}
providerRepository = new ProviderRepository(this);
eventSupport = new EventSupport();
transactionContextPropagator = new NoOpTransactionContextPropagator();
Expand Down Expand Up @@ -304,7 +308,16 @@ public FeatureProvider getProvider(String domain) {
* @param hooks The hook to add.
*/
public void addHooks(Hook... hooks) {
this.apiHooks.addAll(Arrays.asList(hooks));
var types = FlagValueType.values();
for (int i = 0; i < hooks.length; i++) {
var current = hooks[i];
for (int j = 0; j < types.length; j++) {
var type = types[j];
if (current.supportsFlagValueType(type)) {
this.apiHooks.get(type).add(current);
}
}
}
}

/**
Expand All @@ -313,16 +326,21 @@ public void addHooks(Hook... hooks) {
* @return A list of {@link Hook}s.
*/
public List<Hook> getHooks() {
return new ArrayList<>(this.apiHooks);
// Hooks can be duplicated if they support multiple FlagValueTypes
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure, the order of hooks is important, i know that you are now using other methods, to retrieve the needed hooks, but can this implementation now be a problem, because we are not keeping the order of the hooks. Now we merge multiple hook source after each other.

var allHooks = new HashSet<Hook>();
for (var queue : this.apiHooks.values()) {
allHooks.addAll(queue);
}
return new ArrayList<>(allHooks);
}

/**
* Returns a reference to the collection of {@link Hook}s.
* Fetch the hooks associated to this client, that support the given FlagValueType.
*
* @return The collection of {@link Hook}s.
* @return A list of {@link Hook}s.
*/
Collection<Hook> getMutableHooks() {
return this.apiHooks;
ConcurrentLinkedQueue<Hook> getHooks(FlagValueType type) {
return apiHooks.get(type);
}

/**
Expand Down
37 changes: 29 additions & 8 deletions src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
import dev.openfeature.sdk.internal.ObjectUtils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
Expand Down Expand Up @@ -47,7 +48,7 @@ public class OpenFeatureClient implements Client {
@Getter
private final String version;

private final ConcurrentLinkedQueue<Hook> clientHooks;
private final ConcurrentHashMap<FlagValueType, ConcurrentLinkedQueue<Hook>> clientHooks;
private final AtomicReference<EvaluationContext> evaluationContext = new AtomicReference<>();

private final HookSupport hookSupport;
Expand All @@ -69,7 +70,11 @@ public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String ve
this.domain = domain;
this.version = version;
this.hookSupport = new HookSupport();
this.clientHooks = new ConcurrentLinkedQueue<>();
var values = FlagValueType.values();
this.clientHooks = new ConcurrentHashMap<>(values.length);
for (FlagValueType value : values) {
this.clientHooks.put(value, new ConcurrentLinkedQueue<>());
}
}

/**
Expand Down Expand Up @@ -125,7 +130,16 @@ public void track(String trackingEventName, EvaluationContext context, TrackingE
*/
@Override
public OpenFeatureClient addHooks(Hook... hooks) {
this.clientHooks.addAll(Arrays.asList(hooks));
var types = FlagValueType.values();
Copy link
Member

Choose a reason for hiding this comment

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

[suggestion] seems like this block of code repeats itself, should this maybe be a static helper function?

for (int i = 0; i < hooks.length; i++) {
var current = hooks[i];
for (int j = 0; j < types.length; j++) {
var type = types[j];
if (current.supportsFlagValueType(type)) {
this.clientHooks.get(type).add(current);
}
}
}
return this;
}

Expand All @@ -134,7 +148,11 @@ public OpenFeatureClient addHooks(Hook... hooks) {
*/
@Override
public List<Hook> getHooks() {
return new ArrayList<>(this.clientHooks);
var allHooks = new HashSet<Hook>();
for (var queue : this.clientHooks.values()) {
allHooks.addAll(queue);
}
return new ArrayList<>(allHooks);
}

/**
Expand Down Expand Up @@ -174,9 +192,12 @@ 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(type),
flagOptions.getHooks(type),
clientHooks.get(type),
openfeatureApi.getHooks(type));

var sharedHookContext =
new SharedHookContext(key, type, this.getMetadata(), provider.getMetadata(), defaultValue);
Expand Down
24 changes: 15 additions & 9 deletions src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand All @@ -14,6 +15,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.SneakyThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -101,9 +103,10 @@ void brokenProvider() {
void providerLockedPerTransaction() {

final String defaultValue = "string-value";
final OpenFeatureAPI api = new OpenFeatureAPI();
var provider1 = TestProvider.builder().initsToReady();
var provider2 = TestProvider.builder().initsToReady();
final OpenFeatureAPI testApi = new OpenFeatureAPI();
final var provider1 = TestProvider.builder().initsToReady();
final var provider2 = TestProvider.builder().initsToReady();
final var wasHookCalled = new AtomicBoolean(false);

class MutatingHook implements Hook {

Expand All @@ -112,24 +115,27 @@ class MutatingHook implements Hook {
// change the provider during a before hook - this should not impact the evaluation in progress
public Optional before(HookContext ctx, Map hints) {

api.setProviderAndWait(provider2);

testApi.setProviderAndWait(provider2);
wasHookCalled.set(true);
return Optional.empty();
}
}

final Client client = api.getClient();
api.setProviderAndWait(provider1);
api.addHooks(new MutatingHook());
final Client client = testApi.getClient();
testApi.setProviderAndWait(provider1);
testApi.addHooks(new MutatingHook());

// if provider is changed during an evaluation transaction it should proceed with the original provider
client.getStringValue("val", defaultValue);
assertEquals(1, provider1.getFlagEvaluations().size());
assertEquals(0, provider2.getFlagEvaluations().size());
assertTrue(wasHookCalled.get());

api.clearHooks();
testApi.clearHooks();

// subsequent evaluations should now use new provider set by hook
client.getStringValue("val", defaultValue);
assertEquals(1, provider1.getFlagEvaluations().size());
assertEquals(1, provider2.getFlagEvaluations().size());
}

Expand Down
Loading
Loading