From 58c308c25e809b49e7397ebc0dd8028ad2f7e8b8 Mon Sep 17 00:00:00 2001 From: suvaidkhan Date: Tue, 1 Jul 2025 02:10:29 -0500 Subject: [PATCH 1/3] MultiProvider: Added strategies, unit tests and documentation Signed-off-by: suvaidkhan --- pom.xml | 6 + .../sdk/multiprovider/FirstMatchStrategy.java | 53 ++++ .../FirstSuccessfulStrategy.java | 41 +++ .../sdk/multiprovider/MultiProvider.java | 148 ++++++++++ .../sdk/multiprovider/Strategy.java | 17 ++ .../sdk/multiProvider/MultiProviderTest.java | 275 ++++++++++++++++++ 6 files changed, 540 insertions(+) create mode 100644 src/main/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategy.java create mode 100644 src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java create mode 100644 src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java create mode 100644 src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java create mode 100644 src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java diff --git a/pom.xml b/pom.xml index 67b4c5722..f7916bf84 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,12 @@ 2.0.17 + + org.json + json + 20250517 + + com.tngtech.archunit diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategy.java b/src/main/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategy.java new file mode 100644 index 000000000..546565be0 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategy.java @@ -0,0 +1,53 @@ +package dev.openfeature.sdk.multiprovider; + +import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import java.util.Map; +import java.util.function.Function; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * First match strategy. Return the first result returned by a provider. Skip providers that + * indicate they had no value due to FLAG_NOT_FOUND. In all other cases, use the value returned by + * the provider. If any provider returns an error result other than FLAG_NOT_FOUND, the whole + * evaluation should error and “bubble up” the individual provider’s error in the result. As soon as + * a value is returned by a provider, the rest of the operation should short-circuit and not call + * the rest of the providers. + */ +@Slf4j +@NoArgsConstructor +public class FirstMatchStrategy implements Strategy { + + /** + * Represents a strategy that evaluates providers based on a first-match approach. Provides a + * method to evaluate providers using a specified function and return the evaluation result. + * + * @param providerFunction provider function + * @param ProviderEvaluation type + * @return the provider evaluation + */ + @Override + public ProviderEvaluation evaluate( + Map providers, + String key, + T defaultValue, + EvaluationContext ctx, + Function> providerFunction) { + for (FeatureProvider provider : providers.values()) { + try { + ProviderEvaluation res = providerFunction.apply(provider); + if (!FLAG_NOT_FOUND.equals(res.getErrorCode())) { + return res; + } + } catch (FlagNotFoundError e) { + log.debug("flag not found {}", e.getMessage()); + } + } + throw new FlagNotFoundError("flag not found"); + } +} diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java b/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java new file mode 100644 index 000000000..85ca3b9bc --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java @@ -0,0 +1,41 @@ +package dev.openfeature.sdk.multiprovider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.exceptions.GeneralError; +import java.util.Map; +import java.util.function.Function; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * First Successful Strategy. Similar to “First Match”, except that errors from evaluated providers + * do not halt execution. Instead, it will return the first successful result from a provider. If no + * provider successfully responds, it will throw an error result. + */ +@Slf4j +@NoArgsConstructor +public class FirstSuccessfulStrategy implements Strategy { + + @Override + public ProviderEvaluation evaluate( + Map providers, + String key, + T defaultValue, + EvaluationContext ctx, + Function> providerFunction) { + for (FeatureProvider provider : providers.values()) { + try { + ProviderEvaluation res = providerFunction.apply(provider); + if (res.getErrorCode() == null) { + return res; + } + } catch (Exception e) { + log.debug("evaluation exception {}", e.getMessage()); + } + } + + throw new GeneralError("evaluation error"); + } +} diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java new file mode 100644 index 000000000..1689818c0 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java @@ -0,0 +1,148 @@ +package dev.openfeature.sdk.multiprovider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.GeneralError; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONObject; + +/** Experimental: Provider implementation for Multi-provider. */ +@Slf4j +public class MultiProvider extends EventProvider { + + @Getter + private static final String NAME = "multiprovider"; + + public static final int INIT_THREADS_COUNT = 8; + private final Map providers; + private final Strategy strategy; + private String metadataName; + + /** + * Constructs a MultiProvider with the given list of FeatureProviders, using a default strategy. + * + * @param providers the list of FeatureProviders to initialize the MultiProvider with + */ + public MultiProvider(List providers) { + this(providers, null); + } + + /** + * Constructs a MultiProvider with the given list of FeatureProviders and a strategy. + * + * @param providers the list of FeatureProviders to initialize the MultiProvider with + * @param strategy the strategy + */ + public MultiProvider(List providers, Strategy strategy) { + this.providers = buildProviders(providers); + if (strategy != null) { + this.strategy = strategy; + } else { + this.strategy = new FirstMatchStrategy(); + } + } + + protected static Map buildProviders(List providers) { + Map providersMap = new LinkedHashMap<>(providers.size()); + for (FeatureProvider provider : providers) { + FeatureProvider prevProvider = + providersMap.put(provider.getMetadata().getName(), provider); + if (prevProvider != null) { + log.warn("duplicated provider name: {}", provider.getMetadata().getName()); + } + } + return Collections.unmodifiableMap(providersMap); + } + + /** + * Initialize the provider. + * + * @param evaluationContext evaluation context + * @throws Exception on error + */ + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + JSONObject json = new JSONObject(); + json.put("name", NAME); + JSONObject providersMetadata = new JSONObject(); + json.put("originalMetadata", providersMetadata); + ExecutorService initPool = Executors.newFixedThreadPool(INIT_THREADS_COUNT); + Collection> tasks = new ArrayList<>(providers.size()); + for (FeatureProvider provider : providers.values()) { + tasks.add(() -> { + provider.initialize(evaluationContext); + return true; + }); + JSONObject providerMetadata = new JSONObject(); + providerMetadata.put("name", provider.getMetadata().getName()); + providersMetadata.put(provider.getMetadata().getName(), providerMetadata); + } + List> results = initPool.invokeAll(tasks); + for (Future result : results) { + if (!result.get()) { + throw new GeneralError("init failed"); + } + } + metadataName = json.toString(); + } + + @Override + public Metadata getMetadata() { + return () -> metadataName; + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + return strategy.evaluate( + providers, key, defaultValue, ctx, p -> p.getBooleanEvaluation(key, defaultValue, ctx)); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getStringEvaluation(key, defaultValue, ctx)); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + return strategy.evaluate( + providers, key, defaultValue, ctx, p -> p.getIntegerEvaluation(key, defaultValue, ctx)); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getDoubleEvaluation(key, defaultValue, ctx)); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getObjectEvaluation(key, defaultValue, ctx)); + } + + @Override + public void shutdown() { + log.debug("shutdown begin"); + for (FeatureProvider provider : providers.values()) { + try { + provider.shutdown(); + } catch (Exception e) { + log.error("error shutdown provider {}", provider.getMetadata().getName(), e); + } + } + log.debug("shutdown end"); + } +} diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java b/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java new file mode 100644 index 000000000..49b9c8d06 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java @@ -0,0 +1,17 @@ +package dev.openfeature.sdk.multiprovider; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.ProviderEvaluation; +import java.util.Map; +import java.util.function.Function; + +/** strategy. */ +public interface Strategy { + ProviderEvaluation evaluate( + Map providers, + String key, + T defaultValue, + EvaluationContext ctx, + Function> providerFunction); +} diff --git a/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java b/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java new file mode 100644 index 000000000..50c3c86b9 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java @@ -0,0 +1,275 @@ +package dev.openfeature.sdk.multiProvider; + +import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.multiprovider.FirstMatchStrategy; +import dev.openfeature.sdk.multiprovider.FirstSuccessfulStrategy; +import dev.openfeature.sdk.multiprovider.MultiProvider; +import dev.openfeature.sdk.multiprovider.Strategy; +import dev.openfeature.sdk.providers.memory.Flag; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +class MultiProviderTest { + + @SneakyThrows + @Test + public void testInit() { + FeatureProvider provider1 = mock(FeatureProvider.class); + FeatureProvider provider2 = mock(FeatureProvider.class); + when(provider1.getMetadata()).thenReturn(() -> "provider1"); + when(provider2.getMetadata()).thenReturn(() -> "provider2"); + + List providers = new ArrayList<>(2); + providers.add(provider1); + providers.add(provider2); + Strategy strategy = mock(Strategy.class); + MultiProvider multiProvider = new MultiProvider(providers, strategy); + multiProvider.initialize(null); + + assertNotNull(multiProvider); + assertEquals( + "{\"originalMetadata\":{\"provider1\":{\"name\":\"provider1\"}," + + "\"provider2\":{\"name\":\"provider2\"}},\"name\":\"multiprovider\"}", + multiProvider.getMetadata().getName()); + } + + @SneakyThrows + @Test + public void testInitOneFails() { + FeatureProvider provider1 = mock(FeatureProvider.class); + FeatureProvider provider2 = mock(FeatureProvider.class); + when(provider1.getMetadata()).thenReturn(() -> "provider1"); + when(provider2.getMetadata()).thenReturn(() -> "provider2"); + doThrow(new GeneralError()).when(provider1).initialize(any()); + doThrow(new GeneralError()).when(provider1).shutdown(); + + List providers = new ArrayList<>(2); + providers.add(provider1); + providers.add(provider2); + Strategy strategy = mock(Strategy.class); + MultiProvider multiProvider = new MultiProvider(providers, strategy); + assertThrows(ExecutionException.class, () -> multiProvider.initialize(null)); + assertDoesNotThrow(() -> multiProvider.shutdown()); + } + + @Test + public void testDuplicateProviderNames() { + FeatureProvider provider1 = mock(FeatureProvider.class); + FeatureProvider provider2 = mock(FeatureProvider.class); + when(provider1.getMetadata()).thenReturn(() -> "provider"); + when(provider2.getMetadata()).thenReturn(() -> "provider"); + + List providers = new ArrayList<>(2); + providers.add(provider1); + providers.add(provider2); + + assertDoesNotThrow(() -> new MultiProvider(providers, null).initialize(null)); + } + + @SneakyThrows + @Test + public void testRetrieveMetadataName() { + List providers = new ArrayList<>(); + FeatureProvider mockProvider = mock(FeatureProvider.class); + when(mockProvider.getMetadata()).thenReturn(() -> "MockProvider"); + providers.add(mockProvider); + Strategy mockStrategy = mock(Strategy.class); + MultiProvider multiProvider = new MultiProvider(providers, mockStrategy); + multiProvider.initialize(null); + + assertEquals( + "{\"originalMetadata\":{\"MockProvider\":{\"name\":\"MockProvider\"}}," + "\"name\":\"multiprovider\"}", + multiProvider.getMetadata().getName()); + } + + @SneakyThrows + @Test + public void testEvaluations() { + Map> flags1 = new HashMap<>(); + flags1.put( + "b1", + Flag.builder() + .variant("true", true) + .variant("false", false) + .defaultVariant("true") + .build()); + flags1.put("i1", Flag.builder().variant("v", 1).defaultVariant("v").build()); + flags1.put("d1", Flag.builder().variant("v", 1.0).defaultVariant("v").build()); + flags1.put("s1", Flag.builder().variant("v", "str1").defaultVariant("v").build()); + flags1.put( + "o1", + Flag.builder().variant("v", new Value("v1")).defaultVariant("v").build()); + InMemoryProvider provider1 = new InMemoryProvider(flags1) { + public Metadata getMetadata() { + return () -> "old-provider"; + } + }; + Map> flags2 = new HashMap<>(); + flags2.put( + "b1", + Flag.builder() + .variant("true", true) + .variant("false", false) + .defaultVariant("false") + .build()); + flags2.put("i1", Flag.builder().variant("v", 2).defaultVariant("v").build()); + flags2.put("d1", Flag.builder().variant("v", 2.0).defaultVariant("v").build()); + flags2.put("s1", Flag.builder().variant("v", "str2").defaultVariant("v").build()); + flags2.put( + "o1", + Flag.builder().variant("v", new Value("v2")).defaultVariant("v").build()); + + flags2.put( + "s2", Flag.builder().variant("v", "s2str2").defaultVariant("v").build()); + InMemoryProvider provider2 = new InMemoryProvider(flags2) { + public Metadata getMetadata() { + return () -> "new-provider"; + } + }; + List providers = new ArrayList<>(2); + providers.add(provider1); + providers.add(provider2); + MultiProvider multiProvider = new MultiProvider(providers); + multiProvider.initialize(null); + + assertEquals(true, multiProvider.getBooleanEvaluation("b1", false, null).getValue()); + assertEquals(1, multiProvider.getIntegerEvaluation("i1", 0, null).getValue()); + assertEquals(1.0, multiProvider.getDoubleEvaluation("d1", 0.0, null).getValue()); + assertEquals("str1", multiProvider.getStringEvaluation("s1", "", null).getValue()); + assertEquals( + "v1", + multiProvider.getObjectEvaluation("o1", null, null).getValue().asString()); + + assertEquals("s2str2", multiProvider.getStringEvaluation("s2", "", null).getValue()); + MultiProvider finalMultiProvider1 = multiProvider; + assertThrows(FlagNotFoundError.class, () -> finalMultiProvider1.getStringEvaluation("non-existing", "", null)); + + multiProvider.shutdown(); + multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy()); + multiProvider.initialize(null); + + assertEquals(true, multiProvider.getBooleanEvaluation("b1", false, null).getValue()); + assertEquals(1, multiProvider.getIntegerEvaluation("i1", 0, null).getValue()); + assertEquals(1.0, multiProvider.getDoubleEvaluation("d1", 0.0, null).getValue()); + assertEquals("str1", multiProvider.getStringEvaluation("s1", "", null).getValue()); + assertEquals( + "v1", + multiProvider.getObjectEvaluation("o1", null, null).getValue().asString()); + + assertEquals("s2str2", multiProvider.getStringEvaluation("s2", "", null).getValue()); + MultiProvider finalMultiProvider2 = multiProvider; + assertThrows(GeneralError.class, () -> finalMultiProvider2.getStringEvaluation("non-existing", "", null)); + + multiProvider.shutdown(); + Strategy customStrategy = new Strategy() { + final FirstMatchStrategy fallbackStrategy = new FirstMatchStrategy(); + + @Override + public ProviderEvaluation evaluate( + Map providers, + String key, + T defaultValue, + EvaluationContext ctx, + Function> providerFunction) { + Value contextProvider = null; + if (ctx != null) { + contextProvider = ctx.getValue("provider"); + } + if (contextProvider != null && "new-provider".equals(contextProvider.asString())) { + return providerFunction.apply(providers.get("new-provider")); + } + return fallbackStrategy.evaluate(providers, key, defaultValue, ctx, providerFunction); + } + }; + multiProvider = new MultiProvider(providers, customStrategy); + multiProvider.initialize(null); + + EvaluationContext context = new MutableContext().add("provider", "new-provider"); + assertEquals( + false, multiProvider.getBooleanEvaluation("b1", true, context).getValue()); + assertEquals(true, multiProvider.getBooleanEvaluation("b1", true, null).getValue()); + } + + @Test + public void testFirstMatchStrategyErrorCode() { + FeatureProvider provider1 = mock(FeatureProvider.class); + FeatureProvider provider2 = mock(FeatureProvider.class); + FeatureProvider provider3 = mock(FeatureProvider.class); + + when(provider1.getMetadata()).thenReturn(() -> "provider1"); + when(provider2.getMetadata()).thenReturn(() -> "provider2"); + when(provider3.getMetadata()).thenReturn(() -> "provider3"); + + ProviderEvaluation flagNotFoundResult = mock(ProviderEvaluation.class); + when(flagNotFoundResult.getErrorCode()).thenReturn(FLAG_NOT_FOUND); + + ProviderEvaluation successResult = mock(ProviderEvaluation.class); + when(successResult.getErrorCode()).thenReturn(null); + when(successResult.getValue()).thenReturn("success"); + + when(provider1.getStringEvaluation("test", "default", null)).thenReturn(flagNotFoundResult); + when(provider2.getStringEvaluation("test", "default", null)).thenReturn(successResult); + + Map providers = new LinkedHashMap<>(); + providers.put("provider1", provider1); + providers.put("provider2", provider2); + providers.put("provider3", provider3); + FirstMatchStrategy strategy = new FirstMatchStrategy(); + ProviderEvaluation result = strategy.evaluate( + providers, "test", "default", null, p -> p.getStringEvaluation("test", "default", null)); + + assertEquals("success", result.getValue()); + } + + @Test + public void testFirstSuccessfulStrategyErrorCode() { + FeatureProvider provider1 = mock(FeatureProvider.class); + FeatureProvider provider2 = mock(FeatureProvider.class); + when(provider1.getMetadata()).thenReturn(() -> "provider1"); + when(provider2.getMetadata()).thenReturn(() -> "provider2"); + + ProviderEvaluation flagNotFoundResult = mock(ProviderEvaluation.class); + when(flagNotFoundResult.getErrorCode()).thenReturn(FLAG_NOT_FOUND); + + ProviderEvaluation successResult = mock(ProviderEvaluation.class); + when(successResult.getErrorCode()).thenReturn(null); + when(successResult.getValue()).thenReturn("success"); + + when(provider1.getStringEvaluation("test", "default", null)).thenReturn(flagNotFoundResult); + when(provider2.getStringEvaluation("test", "default", null)).thenReturn(successResult); + + Map providers = new LinkedHashMap<>(); + providers.put("provider1", provider1); + providers.put("provider2", provider2); + FirstSuccessfulStrategy strategy = new FirstSuccessfulStrategy(); + ProviderEvaluation result = strategy.evaluate( + providers, "test", "default", null, p -> p.getStringEvaluation("test", "default", null)); + + assertEquals("success", result.getValue()); + } +} From 11039490bb92841b17ee24ebe384fad34e98030b Mon Sep 17 00:00:00 2001 From: suvaidkhan Date: Wed, 16 Jul 2025 18:21:04 -0500 Subject: [PATCH 2/3] MultiProvider: Added MultiProviderMetadata.java, refactored tests and removed json dep Signed-off-by: suvaidkhan --- pom.xml | 6 - .../sdk/multiprovider/MultiProvider.java | 28 ++- .../multiprovider/MultiProviderMetadata.java | 21 ++ .../sdk/multiProvider/BaseStrategyTest.java | 212 +++++++++++++++++ .../multiProvider/FirstMatchStrategyTest.java | 97 ++++++++ .../FirstSuccessfulStrategyTest.java | 80 +++++++ .../sdk/multiProvider/MultiProviderTest.java | 223 +++++------------- 7 files changed, 478 insertions(+), 189 deletions(-) create mode 100644 src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderMetadata.java create mode 100644 src/test/java/dev/openfeature/sdk/multiProvider/BaseStrategyTest.java create mode 100644 src/test/java/dev/openfeature/sdk/multiProvider/FirstMatchStrategyTest.java create mode 100644 src/test/java/dev/openfeature/sdk/multiProvider/FirstSuccessfulStrategyTest.java diff --git a/pom.xml b/pom.xml index f7916bf84..67b4c5722 100644 --- a/pom.xml +++ b/pom.xml @@ -70,12 +70,6 @@ 2.0.17 - - org.json - json - 20250517 - - com.tngtech.archunit diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java index 1689818c0..ff9372412 100644 --- a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java +++ b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java @@ -7,9 +7,11 @@ import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.GeneralError; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -19,7 +21,6 @@ import java.util.concurrent.Future; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.json.JSONObject; /** Experimental: Provider implementation for Multi-provider. */ @Slf4j @@ -31,10 +32,10 @@ public class MultiProvider extends EventProvider { public static final int INIT_THREADS_COUNT = 8; private final Map providers; private final Strategy strategy; - private String metadataName; + private MultiProviderMetadata metadata; /** - * Constructs a MultiProvider with the given list of FeatureProviders, using a default strategy. + * Constructs a MultiProvider with the given list of FeatureProviders, by default uses FirstMatchStrategy. * * @param providers the list of FeatureProviders to initialize the MultiProvider with */ @@ -77,33 +78,34 @@ protected static Map buildProviders(List providersMetadata = new HashMap<>(); + ExecutorService initPool = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size())); Collection> tasks = new ArrayList<>(providers.size()); for (FeatureProvider provider : providers.values()) { tasks.add(() -> { provider.initialize(evaluationContext); return true; }); - JSONObject providerMetadata = new JSONObject(); - providerMetadata.put("name", provider.getMetadata().getName()); - providersMetadata.put(provider.getMetadata().getName(), providerMetadata); + Metadata providerMetadata = provider.getMetadata(); + providersMetadata.put(providerMetadata.getName(), providerMetadata); } + metadataBuilder.originalMetadata(providersMetadata); List> results = initPool.invokeAll(tasks); for (Future result : results) { if (!result.get()) { throw new GeneralError("init failed"); } } - metadataName = json.toString(); + initPool.shutdown(); + metadata = metadataBuilder.build(); } + @SuppressFBWarnings(value = "EI_EXPOSE_REP") @Override public Metadata getMetadata() { - return () -> metadataName; + return metadata; } @Override diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderMetadata.java b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderMetadata.java new file mode 100644 index 000000000..28ef64f75 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderMetadata.java @@ -0,0 +1,21 @@ +package dev.openfeature.sdk.multiprovider; + +import dev.openfeature.sdk.Metadata; +import java.util.Map; +import lombok.Builder; +import lombok.Data; + +/** + * Metadata class for Multiprovider. + */ +@Data +@Builder +public class MultiProviderMetadata implements Metadata { + String name; + Map originalMetadata; + + @Override + public String getName() { + return name; + } +} diff --git a/src/test/java/dev/openfeature/sdk/multiProvider/BaseStrategyTest.java b/src/test/java/dev/openfeature/sdk/multiProvider/BaseStrategyTest.java new file mode 100644 index 000000000..a832b4c83 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/multiProvider/BaseStrategyTest.java @@ -0,0 +1,212 @@ +package dev.openfeature.sdk.multiProvider; + +import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.providers.memory.Flag; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; + +public abstract class BaseStrategyTest { + + protected FeatureProvider mockProvider1; + protected FeatureProvider mockProvider2; + protected FeatureProvider mockProvider3; + + protected Metadata mockMetaData1; + protected Metadata mockMetaData2; + protected Metadata mockMetaData3; + + protected InMemoryProvider inMemoryProvider1; + protected InMemoryProvider inMemoryProvider2; + + protected Map orderedProviders; + + protected EvaluationContext contextWithNewProvider; + + protected static final String FLAG_KEY = "test-flag"; + protected static final String DEFAULT_STRING = "default"; + protected static final boolean DEFAULT_BOOLEAN = false; + protected static final int DEFAULT_INTEGER = 0; + protected static final double DEFAULT_DOUBLE = 0.0; + + @BeforeEach + void setUp() { + setupMockProviders(); + setupInMemoryProviders(); + setupOrderedProviders(); + setupEvaluationContexts(); + } + + protected void setupMockProviders() { + mockProvider1 = mock(FeatureProvider.class); + mockProvider2 = mock(FeatureProvider.class); + mockProvider3 = mock(FeatureProvider.class); + mockMetaData1 = mock(Metadata.class); + mockMetaData2 = mock(Metadata.class); + mockMetaData3 = mock(Metadata.class); + when(mockMetaData1.getName()).thenReturn("provider1"); + when(mockMetaData2.getName()).thenReturn("provider2"); + when(mockMetaData3.getName()).thenReturn("provider3"); + when(mockProvider1.getMetadata()).thenReturn(mockMetaData1); + when(mockProvider2.getMetadata()).thenReturn(mockMetaData2); + when(mockProvider3.getMetadata()).thenReturn(mockMetaData3); + } + + protected void setupInMemoryProviders() { + Map> flags1 = createFlags1(); + Map> flags2 = createFlags2(); + + inMemoryProvider1 = new InMemoryProvider(flags1) { + public Metadata getMetadata() { + return () -> "old-provider"; + } + }; + + inMemoryProvider2 = new InMemoryProvider(flags2) { + public Metadata getMetadata() { + return () -> "new-provider"; + } + }; + } + + protected void setupOrderedProviders() { + orderedProviders = new LinkedHashMap<>(); + orderedProviders.put("provider1", mockProvider1); + orderedProviders.put("provider2", mockProvider2); + orderedProviders.put("provider3", mockProvider3); + } + + protected void setupEvaluationContexts() { + contextWithNewProvider = new MutableContext().add("provider", "new-provider"); + } + + protected Map> createFlags1() { + Map> flags = new HashMap<>(); + + flags.put( + "b1", + Flag.builder() + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); + + flags.put( + "i1", + Flag.builder().variant("default", 1).defaultVariant("default").build()); + + flags.put( + "d1", + Flag.builder().variant("default", 1.0).defaultVariant("default").build()); + + flags.put( + "s1", + Flag.builder() + .variant("default", "str1") + .defaultVariant("default") + .build()); + + flags.put( + "o1", + Flag.builder() + .variant("default", new Value("v1")) + .defaultVariant("default") + .build()); + + return flags; + } + + protected Map> createFlags2() { + Map> flags = new HashMap<>(); + + flags.put( + "b1", + Flag.builder() + .variant("on", true) + .variant("off", false) + .defaultVariant("off") + .build()); + + flags.put( + "i1", + Flag.builder().variant("default", 2).defaultVariant("default").build()); + + flags.put( + "d1", + Flag.builder().variant("default", 2.0).defaultVariant("default").build()); + + flags.put( + "s1", + Flag.builder() + .variant("default", "str2") + .defaultVariant("default") + .build()); + + flags.put( + "o1", + Flag.builder() + .variant("default", new Value("v2")) + .defaultVariant("default") + .build()); + + flags.put( + "s2", + Flag.builder() + .variant("default", "s2str2") + .defaultVariant("default") + .build()); + + return flags; + } + + protected ProviderEvaluation createErrorResult(ErrorCode errorCode) { + ProviderEvaluation result = mock(ProviderEvaluation.class); + when(result.getErrorCode()).thenReturn(errorCode); + return result; + } + + protected void setupProviderFlagNotFound(FeatureProvider provider) { + ProviderEvaluation stringResult = createErrorResult(FLAG_NOT_FOUND); + ProviderEvaluation booleanResult = createErrorResult(FLAG_NOT_FOUND); + ProviderEvaluation integerResult = createErrorResult(FLAG_NOT_FOUND); + ProviderEvaluation doubleResult = createErrorResult(FLAG_NOT_FOUND); + ProviderEvaluation objectResult = createErrorResult(FLAG_NOT_FOUND); + + when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null)) + .thenReturn(stringResult); + when(provider.getBooleanEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_BOOLEAN, null)) + .thenReturn(booleanResult); + when(provider.getIntegerEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_INTEGER, null)) + .thenReturn(integerResult); + when(provider.getDoubleEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_DOUBLE, null)) + .thenReturn(doubleResult); + when(provider.getObjectEvaluation(BaseStrategyTest.FLAG_KEY, null, null)) + .thenReturn(objectResult); + } + + protected void setupProviderError(FeatureProvider provider, ErrorCode errorCode) { + ProviderEvaluation result = createErrorResult(errorCode); + when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null)) + .thenReturn(result); + } + + protected void setupProviderSuccess(FeatureProvider provider, String value) { + ProviderEvaluation result = mock(ProviderEvaluation.class); + when(result.getErrorCode()).thenReturn(null); + when(result.getValue()).thenReturn(value); + when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null)) + .thenReturn(result); + } +} diff --git a/src/test/java/dev/openfeature/sdk/multiProvider/FirstMatchStrategyTest.java b/src/test/java/dev/openfeature/sdk/multiProvider/FirstMatchStrategyTest.java new file mode 100644 index 000000000..7049f57ad --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/multiProvider/FirstMatchStrategyTest.java @@ -0,0 +1,97 @@ +package dev.openfeature.sdk.multiProvider; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.multiprovider.FirstMatchStrategy; +import org.junit.jupiter.api.Test; + +class FirstMatchStrategyTest extends BaseStrategyTest { + + private final FirstMatchStrategy strategy = new FirstMatchStrategy(); + + @Test + void shouldSkipFlagNotFoundAndReturnFirstMatch() { + setupProviderFlagNotFound(mockProvider1); + setupProviderSuccess(mockProvider2, "success"); + + ProviderEvaluation result = strategy.evaluate( + orderedProviders, + FLAG_KEY, + DEFAULT_STRING, + null, + p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null)); + + assertNotNull(result); + assertEquals("success", result.getValue()); + assertNull(result.getErrorCode()); + } + + @Test + void shouldReturnFirstNonFlagNotFoundError() { + setupProviderError(mockProvider1, ErrorCode.PARSE_ERROR); + setupProviderSuccess(mockProvider2, "success"); + ProviderEvaluation result = strategy.evaluate( + orderedProviders, + FLAG_KEY, + DEFAULT_STRING, + null, + p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null)); + + assertNotNull(result); + assertEquals(ErrorCode.PARSE_ERROR, result.getErrorCode()); + } + + @Test + void shouldReturnSuccessWhenFirstProviderSucceeds() { + setupProviderSuccess(mockProvider1, "first-success"); + setupProviderFlagNotFound(mockProvider2); + ProviderEvaluation result = strategy.evaluate( + orderedProviders, + FLAG_KEY, + DEFAULT_STRING, + null, + p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null)); + + assertNotNull(result); + assertEquals("first-success", result.getValue()); + assertNull(result.getErrorCode()); + } + + @Test + void shouldThrowFlagNotFoundWhenAllProvidersReturnFlagNotFound() { + setupProviderFlagNotFound(mockProvider1); + setupProviderFlagNotFound(mockProvider2); + setupProviderFlagNotFound(mockProvider3); + FlagNotFoundError exception = assertThrows(FlagNotFoundError.class, () -> { + strategy.evaluate( + orderedProviders, + FLAG_KEY, + DEFAULT_STRING, + null, + p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null)); + }); + + assertEquals("flag not found", exception.getMessage()); + } + + @Test + void shouldSkipMultipleFlagNotFoundAndReturnFirstOtherError() { + setupProviderFlagNotFound(mockProvider1); + setupProviderFlagNotFound(mockProvider2); + setupProviderError(mockProvider3, ErrorCode.PARSE_ERROR); + ProviderEvaluation result = strategy.evaluate( + orderedProviders, + FLAG_KEY, + DEFAULT_STRING, + null, + p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null)); + assertNotNull(result); + assertEquals(ErrorCode.PARSE_ERROR, result.getErrorCode()); + } +} diff --git a/src/test/java/dev/openfeature/sdk/multiProvider/FirstSuccessfulStrategyTest.java b/src/test/java/dev/openfeature/sdk/multiProvider/FirstSuccessfulStrategyTest.java new file mode 100644 index 000000000..b7c09c78d --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/multiProvider/FirstSuccessfulStrategyTest.java @@ -0,0 +1,80 @@ +package dev.openfeature.sdk.multiProvider; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.multiprovider.FirstSuccessfulStrategy; +import org.junit.jupiter.api.Test; + +class FirstSuccessfulStrategyTest extends BaseStrategyTest { + + private final FirstSuccessfulStrategy strategy = new FirstSuccessfulStrategy(); + + @Test + void shouldSkipFlagNotFoundAndReturnFirstSuccess() { + setupProviderFlagNotFound(mockProvider1); + setupProviderSuccess(mockProvider2, "success"); + ProviderEvaluation result = strategy.evaluate( + orderedProviders, + FLAG_KEY, + DEFAULT_STRING, + null, + p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null)); + assertNotNull(result); + assertEquals("success", result.getValue()); + assertNull(result.getErrorCode()); + } + + @Test + void shouldThrowGeneralErrorWhenAllProvidersFail() { + setupProviderFlagNotFound(mockProvider1); + setupProviderError(mockProvider2, ErrorCode.PARSE_ERROR); + setupProviderError(mockProvider3, ErrorCode.TYPE_MISMATCH); + GeneralError exception = assertThrows(GeneralError.class, () -> { + strategy.evaluate( + orderedProviders, + FLAG_KEY, + DEFAULT_STRING, + null, + p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null)); + }); + + assertEquals("evaluation error", exception.getMessage()); + } + + @Test + void shouldSkipProvidersThatOnlyReturnErrors() { + setupProviderError(mockProvider1, ErrorCode.INVALID_CONTEXT); + setupProviderError(mockProvider2, ErrorCode.PROVIDER_NOT_READY); + setupProviderError(mockProvider3, ErrorCode.GENERAL); + + assertThrows(GeneralError.class, () -> { + strategy.evaluate( + orderedProviders, + FLAG_KEY, + DEFAULT_STRING, + null, + p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null)); + }); + } + + @Test + void shouldThrowGeneralErrorForNonExistentFlag() { + orderedProviders.clear(); + orderedProviders.put("old-provider", inMemoryProvider1); + orderedProviders.put("new-provider", inMemoryProvider2); + assertThrows(GeneralError.class, () -> { + strategy.evaluate( + orderedProviders, + "non-existent-flag", + DEFAULT_STRING, + null, + p -> p.getStringEvaluation("non-existent-flag", DEFAULT_STRING, null)); + }); + } +} diff --git a/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java b/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java index 50c3c86b9..624721124 100644 --- a/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java @@ -1,6 +1,5 @@ package dev.openfeature.sdk.multiProvider; -import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -19,14 +18,10 @@ import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.multiprovider.FirstMatchStrategy; -import dev.openfeature.sdk.multiprovider.FirstSuccessfulStrategy; import dev.openfeature.sdk.multiprovider.MultiProvider; +import dev.openfeature.sdk.multiprovider.MultiProviderMetadata; import dev.openfeature.sdk.multiprovider.Strategy; -import dev.openfeature.sdk.providers.memory.Flag; -import dev.openfeature.sdk.providers.memory.InMemoryProvider; import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -34,129 +29,72 @@ import lombok.SneakyThrows; import org.junit.jupiter.api.Test; -class MultiProviderTest { +class MultiProviderTest extends BaseStrategyTest { @SneakyThrows @Test - public void testInit() { - FeatureProvider provider1 = mock(FeatureProvider.class); - FeatureProvider provider2 = mock(FeatureProvider.class); - when(provider1.getMetadata()).thenReturn(() -> "provider1"); - when(provider2.getMetadata()).thenReturn(() -> "provider2"); - + void shouldInitializeSuccessfully() { List providers = new ArrayList<>(2); - providers.add(provider1); - providers.add(provider2); + providers.add(mockProvider1); + providers.add(mockProvider2); Strategy strategy = mock(Strategy.class); MultiProvider multiProvider = new MultiProvider(providers, strategy); multiProvider.initialize(null); assertNotNull(multiProvider); - assertEquals( - "{\"originalMetadata\":{\"provider1\":{\"name\":\"provider1\"}," - + "\"provider2\":{\"name\":\"provider2\"}},\"name\":\"multiprovider\"}", - multiProvider.getMetadata().getName()); + MultiProviderMetadata metadata = (MultiProviderMetadata) multiProvider.getMetadata(); + Map map = metadata.getOriginalMetadata(); + assertEquals(mockMetaData1, map.get(mockProvider1.getMetadata().getName())); + assertEquals(mockMetaData2, map.get(mockProvider2.getMetadata().getName())); + assertEquals("multiprovider", multiProvider.getMetadata().getName()); } @SneakyThrows @Test - public void testInitOneFails() { - FeatureProvider provider1 = mock(FeatureProvider.class); - FeatureProvider provider2 = mock(FeatureProvider.class); - when(provider1.getMetadata()).thenReturn(() -> "provider1"); - when(provider2.getMetadata()).thenReturn(() -> "provider2"); - doThrow(new GeneralError()).when(provider1).initialize(any()); - doThrow(new GeneralError()).when(provider1).shutdown(); - + void shouldHandleInitializationFailure() { + doThrow(new GeneralError()).when(mockProvider1).initialize(any()); + doThrow(new GeneralError()).when(mockProvider1).shutdown(); List providers = new ArrayList<>(2); - providers.add(provider1); - providers.add(provider2); + providers.add(mockProvider1); + providers.add(mockProvider2); Strategy strategy = mock(Strategy.class); MultiProvider multiProvider = new MultiProvider(providers, strategy); assertThrows(ExecutionException.class, () -> multiProvider.initialize(null)); - assertDoesNotThrow(() -> multiProvider.shutdown()); + assertDoesNotThrow(multiProvider::shutdown); } @Test - public void testDuplicateProviderNames() { - FeatureProvider provider1 = mock(FeatureProvider.class); - FeatureProvider provider2 = mock(FeatureProvider.class); - when(provider1.getMetadata()).thenReturn(() -> "provider"); - when(provider2.getMetadata()).thenReturn(() -> "provider"); - + void shouldHandleDuplicateProviderNames() { + when(mockProvider1.getMetadata()).thenReturn(() -> "provider"); + when(mockProvider2.getMetadata()).thenReturn(() -> "provider"); List providers = new ArrayList<>(2); - providers.add(provider1); - providers.add(provider2); - + providers.add(mockProvider1); + providers.add(mockProvider2); assertDoesNotThrow(() -> new MultiProvider(providers, null).initialize(null)); } @SneakyThrows @Test - public void testRetrieveMetadataName() { + void shouldRetrieveCorrectMetadataName() { List providers = new ArrayList<>(); - FeatureProvider mockProvider = mock(FeatureProvider.class); - when(mockProvider.getMetadata()).thenReturn(() -> "MockProvider"); - providers.add(mockProvider); + providers.add(mockProvider1); Strategy mockStrategy = mock(Strategy.class); MultiProvider multiProvider = new MultiProvider(providers, mockStrategy); multiProvider.initialize(null); - - assertEquals( - "{\"originalMetadata\":{\"MockProvider\":{\"name\":\"MockProvider\"}}," + "\"name\":\"multiprovider\"}", - multiProvider.getMetadata().getName()); + assertNotNull(multiProvider); + MultiProviderMetadata metadata = (MultiProviderMetadata) multiProvider.getMetadata(); + Map map = metadata.getOriginalMetadata(); + assertEquals(mockMetaData1, map.get(mockProvider1.getMetadata().getName())); } @SneakyThrows @Test - public void testEvaluations() { - Map> flags1 = new HashMap<>(); - flags1.put( - "b1", - Flag.builder() - .variant("true", true) - .variant("false", false) - .defaultVariant("true") - .build()); - flags1.put("i1", Flag.builder().variant("v", 1).defaultVariant("v").build()); - flags1.put("d1", Flag.builder().variant("v", 1.0).defaultVariant("v").build()); - flags1.put("s1", Flag.builder().variant("v", "str1").defaultVariant("v").build()); - flags1.put( - "o1", - Flag.builder().variant("v", new Value("v1")).defaultVariant("v").build()); - InMemoryProvider provider1 = new InMemoryProvider(flags1) { - public Metadata getMetadata() { - return () -> "old-provider"; - } - }; - Map> flags2 = new HashMap<>(); - flags2.put( - "b1", - Flag.builder() - .variant("true", true) - .variant("false", false) - .defaultVariant("false") - .build()); - flags2.put("i1", Flag.builder().variant("v", 2).defaultVariant("v").build()); - flags2.put("d1", Flag.builder().variant("v", 2.0).defaultVariant("v").build()); - flags2.put("s1", Flag.builder().variant("v", "str2").defaultVariant("v").build()); - flags2.put( - "o1", - Flag.builder().variant("v", new Value("v2")).defaultVariant("v").build()); - - flags2.put( - "s2", Flag.builder().variant("v", "s2str2").defaultVariant("v").build()); - InMemoryProvider provider2 = new InMemoryProvider(flags2) { - public Metadata getMetadata() { - return () -> "new-provider"; - } - }; + void shouldUseDefaultFirstMatchStrategy() { List providers = new ArrayList<>(2); - providers.add(provider1); - providers.add(provider2); + providers.add(inMemoryProvider1); + providers.add(inMemoryProvider2); MultiProvider multiProvider = new MultiProvider(providers); multiProvider.initialize(null); - assertEquals(true, multiProvider.getBooleanEvaluation("b1", false, null).getValue()); assertEquals(1, multiProvider.getIntegerEvaluation("i1", 0, null).getValue()); assertEquals(1.0, multiProvider.getDoubleEvaluation("d1", 0.0, null).getValue()); @@ -164,28 +102,12 @@ public Metadata getMetadata() { assertEquals( "v1", multiProvider.getObjectEvaluation("o1", null, null).getValue().asString()); + assertThrows(FlagNotFoundError.class, () -> multiProvider.getStringEvaluation("non-existing", "", null)); + } - assertEquals("s2str2", multiProvider.getStringEvaluation("s2", "", null).getValue()); - MultiProvider finalMultiProvider1 = multiProvider; - assertThrows(FlagNotFoundError.class, () -> finalMultiProvider1.getStringEvaluation("non-existing", "", null)); - - multiProvider.shutdown(); - multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy()); - multiProvider.initialize(null); - - assertEquals(true, multiProvider.getBooleanEvaluation("b1", false, null).getValue()); - assertEquals(1, multiProvider.getIntegerEvaluation("i1", 0, null).getValue()); - assertEquals(1.0, multiProvider.getDoubleEvaluation("d1", 0.0, null).getValue()); - assertEquals("str1", multiProvider.getStringEvaluation("s1", "", null).getValue()); - assertEquals( - "v1", - multiProvider.getObjectEvaluation("o1", null, null).getValue().asString()); - - assertEquals("s2str2", multiProvider.getStringEvaluation("s2", "", null).getValue()); - MultiProvider finalMultiProvider2 = multiProvider; - assertThrows(GeneralError.class, () -> finalMultiProvider2.getStringEvaluation("non-existing", "", null)); - - multiProvider.shutdown(); + @SneakyThrows + @Test + void shouldWorkWithCustomStrategy() { Strategy customStrategy = new Strategy() { final FirstMatchStrategy fallbackStrategy = new FirstMatchStrategy(); @@ -196,80 +118,41 @@ public ProviderEvaluation evaluate( T defaultValue, EvaluationContext ctx, Function> providerFunction) { + Value contextProvider = null; if (ctx != null) { contextProvider = ctx.getValue("provider"); } + if (contextProvider != null && "new-provider".equals(contextProvider.asString())) { return providerFunction.apply(providers.get("new-provider")); } return fallbackStrategy.evaluate(providers, key, defaultValue, ctx, providerFunction); } }; - multiProvider = new MultiProvider(providers, customStrategy); - multiProvider.initialize(null); + List providers = new ArrayList<>(2); + providers.add(inMemoryProvider1); + providers.add(inMemoryProvider2); + MultiProvider multiProvider = new MultiProvider(providers, customStrategy); + multiProvider.initialize(null); EvaluationContext context = new MutableContext().add("provider", "new-provider"); assertEquals( false, multiProvider.getBooleanEvaluation("b1", true, context).getValue()); assertEquals(true, multiProvider.getBooleanEvaluation("b1", true, null).getValue()); } + @SneakyThrows @Test - public void testFirstMatchStrategyErrorCode() { - FeatureProvider provider1 = mock(FeatureProvider.class); - FeatureProvider provider2 = mock(FeatureProvider.class); - FeatureProvider provider3 = mock(FeatureProvider.class); - - when(provider1.getMetadata()).thenReturn(() -> "provider1"); - when(provider2.getMetadata()).thenReturn(() -> "provider2"); - when(provider3.getMetadata()).thenReturn(() -> "provider3"); - - ProviderEvaluation flagNotFoundResult = mock(ProviderEvaluation.class); - when(flagNotFoundResult.getErrorCode()).thenReturn(FLAG_NOT_FOUND); - - ProviderEvaluation successResult = mock(ProviderEvaluation.class); - when(successResult.getErrorCode()).thenReturn(null); - when(successResult.getValue()).thenReturn("success"); - - when(provider1.getStringEvaluation("test", "default", null)).thenReturn(flagNotFoundResult); - when(provider2.getStringEvaluation("test", "default", null)).thenReturn(successResult); - - Map providers = new LinkedHashMap<>(); - providers.put("provider1", provider1); - providers.put("provider2", provider2); - providers.put("provider3", provider3); - FirstMatchStrategy strategy = new FirstMatchStrategy(); - ProviderEvaluation result = strategy.evaluate( - providers, "test", "default", null, p -> p.getStringEvaluation("test", "default", null)); - - assertEquals("success", result.getValue()); - } - - @Test - public void testFirstSuccessfulStrategyErrorCode() { - FeatureProvider provider1 = mock(FeatureProvider.class); - FeatureProvider provider2 = mock(FeatureProvider.class); - when(provider1.getMetadata()).thenReturn(() -> "provider1"); - when(provider2.getMetadata()).thenReturn(() -> "provider2"); - - ProviderEvaluation flagNotFoundResult = mock(ProviderEvaluation.class); - when(flagNotFoundResult.getErrorCode()).thenReturn(FLAG_NOT_FOUND); - - ProviderEvaluation successResult = mock(ProviderEvaluation.class); - when(successResult.getErrorCode()).thenReturn(null); - when(successResult.getValue()).thenReturn("success"); - - when(provider1.getStringEvaluation("test", "default", null)).thenReturn(flagNotFoundResult); - when(provider2.getStringEvaluation("test", "default", null)).thenReturn(successResult); - - Map providers = new LinkedHashMap<>(); - providers.put("provider1", provider1); - providers.put("provider2", provider2); - FirstSuccessfulStrategy strategy = new FirstSuccessfulStrategy(); - ProviderEvaluation result = strategy.evaluate( - providers, "test", "default", null, p -> p.getStringEvaluation("test", "default", null)); - - assertEquals("success", result.getValue()); + void shouldSupportAllEvaluationTypes() { + List providers = new ArrayList<>(1); + providers.add(inMemoryProvider1); + MultiProvider multiProvider = new MultiProvider(providers); + multiProvider.initialize(null); + assertNotNull(multiProvider.getBooleanEvaluation("b1", false, null)); + assertNotNull(multiProvider.getIntegerEvaluation("i1", 0, null)); + assertNotNull(multiProvider.getDoubleEvaluation("d1", 0.0, null)); + assertNotNull(multiProvider.getStringEvaluation("s1", "", null)); + assertNotNull(multiProvider.getObjectEvaluation("o1", null, null)); } } From dae672ed64a4c811d0d58dfffd977947bf6758f6 Mon Sep 17 00:00:00 2001 From: suvaidkhan Date: Thu, 17 Jul 2025 17:50:22 -0500 Subject: [PATCH 3/3] Multiprovider: review comments incorporated Signed-off-by: suvaidkhan --- .../java/dev/openfeature/sdk/multiprovider/MultiProvider.java | 1 + .../dev/openfeature/sdk/multiProvider/MultiProviderTest.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java index ff9372412..d23015e14 100644 --- a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java +++ b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java @@ -95,6 +95,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { List> results = initPool.invokeAll(tasks); for (Future result : results) { if (!result.get()) { + initPool.shutdown(); throw new GeneralError("init failed"); } } diff --git a/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java b/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java index 624721124..d9c8998cb 100644 --- a/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/multiProvider/MultiProviderTest.java @@ -41,7 +41,6 @@ void shouldInitializeSuccessfully() { MultiProvider multiProvider = new MultiProvider(providers, strategy); multiProvider.initialize(null); - assertNotNull(multiProvider); MultiProviderMetadata metadata = (MultiProviderMetadata) multiProvider.getMetadata(); Map map = metadata.getOriginalMetadata(); assertEquals(mockMetaData1, map.get(mockProvider1.getMetadata().getName()));