From c2cb2b9c3a7f006f64aa53f0657e18103d36efed Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 10 Feb 2026 15:54:16 +0100 Subject: [PATCH] Allow for providers with equal name in the multiprovider Signed-off-by: christian.lutnik --- .../sdk/multiprovider/FirstMatchStrategy.java | 6 ++-- .../FirstSuccessfulStrategy.java | 6 ++-- .../sdk/multiprovider/MultiProvider.java | 36 ++++++------------- .../multiprovider/MultiProviderMetadata.java | 6 ++-- .../sdk/multiprovider/Strategy.java | 4 +-- .../sdk/multiprovider/BaseStrategyTest.java | 13 +++---- .../FirstSuccessfulStrategyTest.java | 4 +-- .../sdk/multiprovider/MultiProviderTest.java | 19 +++++----- 8 files changed, 41 insertions(+), 53 deletions(-) diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategy.java b/src/main/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategy.java index 8bfdec76d..795504d93 100644 --- a/src/main/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategy.java +++ b/src/main/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategy.java @@ -7,7 +7,7 @@ import dev.openfeature.sdk.FeatureProvider; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.exceptions.FlagNotFoundError; -import java.util.Map; +import java.util.List; import java.util.function.Function; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,12 +31,12 @@ public class FirstMatchStrategy implements Strategy { @Override public ProviderEvaluation evaluate( - Map providers, + List providers, String key, T defaultValue, EvaluationContext ctx, Function> providerFunction) { - for (FeatureProvider provider : providers.values()) { + for (FeatureProvider provider : providers) { try { ProviderEvaluation res = providerFunction.apply(provider); ErrorCode errorCode = res.getErrorCode(); diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java b/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java index 6a3fc4433..ed99049d8 100644 --- a/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java +++ b/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java @@ -4,7 +4,7 @@ import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.FeatureProvider; import dev.openfeature.sdk.ProviderEvaluation; -import java.util.Map; +import java.util.List; import java.util.function.Function; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -22,12 +22,12 @@ public class FirstSuccessfulStrategy implements Strategy { @Override public ProviderEvaluation evaluate( - Map providers, + List providers, String key, T defaultValue, EvaluationContext ctx, Function> providerFunction) { - for (FeatureProvider provider : providers.values()) { + for (FeatureProvider provider : providers) { try { ProviderEvaluation res = providerFunction.apply(provider); if (res.getErrorCode() == null) { diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java index cc6fb8db2..c7dbfc024 100644 --- a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java +++ b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java @@ -10,13 +10,9 @@ 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; import java.util.Objects; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import lombok.Getter; @@ -38,7 +34,7 @@ public class MultiProvider extends EventProvider { // Use CPU count as upper bound for init threads. public static final int INIT_THREADS_COUNT = Runtime.getRuntime().availableProcessors(); - private final Map providers; + private final List providers; private final Strategy strategy; private MultiProviderMetadata metadata; @@ -59,22 +55,10 @@ public MultiProvider(List providers) { * @param strategy the strategy (if {@code null}, {@link FirstMatchStrategy} is used) */ public MultiProvider(List providers, Strategy strategy) { - this.providers = buildProviders(providers); + this.providers = providers; this.strategy = Objects.requireNonNull(strategy, "strategy must not be null"); } - 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.info("duplicated provider name: {}", provider.getMetadata().getName()); - } - } - return Collections.unmodifiableMap(providersMap); - } - /** * Initialize the provider. * @@ -85,27 +69,27 @@ protected static Map buildProviders(List providersMetadata = new HashMap<>(); if (providers.isEmpty()) { - metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata)); + metadataBuilder.originalMetadata(Collections.emptyList()); metadata = metadataBuilder.build(); return; } - ExecutorService executorService = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size())); + List providersMetadata = new ArrayList<>(providers.size()); + + var executorService = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size())); try { Collection> tasks = new ArrayList<>(providers.size()); - for (FeatureProvider provider : providers.values()) { + for (FeatureProvider provider : providers) { tasks.add(() -> { provider.initialize(evaluationContext); return null; }); - Metadata providerMetadata = provider.getMetadata(); - providersMetadata.put(providerMetadata.getName(), providerMetadata); + providersMetadata.add(provider.getMetadata()); } - metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata)); + metadataBuilder.originalMetadata(Collections.unmodifiableList(providersMetadata)); List> results = executorService.invokeAll(tasks); for (Future result : results) { @@ -165,7 +149,7 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa @Override public void shutdown() { log.debug("shutdown begin"); - for (FeatureProvider provider : providers.values()) { + for (FeatureProvider provider : providers) { try { provider.shutdown(); } catch (Exception e) { diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderMetadata.java b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderMetadata.java index 2f1bbfd46..3103a79c8 100644 --- a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderMetadata.java +++ b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderMetadata.java @@ -1,14 +1,14 @@ package dev.openfeature.sdk.multiprovider; import dev.openfeature.sdk.Metadata; -import java.util.Map; +import java.util.List; import lombok.Builder; import lombok.Value; /** * Metadata for {@link MultiProvider}. * - *

Contains the multiprovider's own name and a map of the original metadata from each underlying + *

Contains the multiprovider's own name and a list of the original metadata from each underlying * provider. */ @Value @@ -16,5 +16,5 @@ public class MultiProviderMetadata implements Metadata { String name; - Map originalMetadata; + List originalMetadata; } diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java b/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java index 4c25fe8f0..db8b48fd4 100644 --- a/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java +++ b/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java @@ -3,7 +3,7 @@ import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.FeatureProvider; import dev.openfeature.sdk.ProviderEvaluation; -import java.util.Map; +import java.util.List; import java.util.function.Function; /** @@ -30,7 +30,7 @@ public interface Strategy { * @return the resolved {@link ProviderEvaluation} */ ProviderEvaluation evaluate( - Map providers, + List providers, String key, T defaultValue, EvaluationContext ctx, diff --git a/src/test/java/dev/openfeature/sdk/multiprovider/BaseStrategyTest.java b/src/test/java/dev/openfeature/sdk/multiprovider/BaseStrategyTest.java index 405a2f094..033c71a44 100644 --- a/src/test/java/dev/openfeature/sdk/multiprovider/BaseStrategyTest.java +++ b/src/test/java/dev/openfeature/sdk/multiprovider/BaseStrategyTest.java @@ -13,8 +13,9 @@ import dev.openfeature.sdk.Value; 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 org.junit.jupiter.api.BeforeEach; @@ -31,7 +32,7 @@ public abstract class BaseStrategyTest { protected InMemoryProvider inMemoryProvider1; protected InMemoryProvider inMemoryProvider2; - protected Map orderedProviders; + protected List orderedProviders; protected EvaluationContext contextWithNewProvider; @@ -84,10 +85,10 @@ public Metadata getMetadata() { } protected void setupOrderedProviders() { - orderedProviders = new LinkedHashMap<>(); - orderedProviders.put("provider1", mockProvider1); - orderedProviders.put("provider2", mockProvider2); - orderedProviders.put("provider3", mockProvider3); + orderedProviders = new ArrayList<>(3); + orderedProviders.add(mockProvider1); + orderedProviders.add(mockProvider2); + orderedProviders.add(mockProvider3); } protected void setupEvaluationContexts() { diff --git a/src/test/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategyTest.java b/src/test/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategyTest.java index a47af8a2f..0caf9f1a8 100644 --- a/src/test/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategyTest.java +++ b/src/test/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategyTest.java @@ -63,8 +63,8 @@ void shouldSkipProvidersThatOnlyReturnErrors() { @Test void shouldThrowGeneralErrorForNonExistentFlag() { orderedProviders.clear(); - orderedProviders.put("old-provider", inMemoryProvider1); - orderedProviders.put("new-provider", inMemoryProvider2); + orderedProviders.add(inMemoryProvider1); + orderedProviders.add(inMemoryProvider2); ProviderEvaluation providerEvaluation = strategy.evaluate( orderedProviders, FLAG_KEY, diff --git a/src/test/java/dev/openfeature/sdk/multiprovider/MultiProviderTest.java b/src/test/java/dev/openfeature/sdk/multiprovider/MultiProviderTest.java index 887b71d0a..58e01169a 100644 --- a/src/test/java/dev/openfeature/sdk/multiprovider/MultiProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/multiprovider/MultiProviderTest.java @@ -4,6 +4,7 @@ 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.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -19,7 +20,6 @@ import dev.openfeature.sdk.exceptions.GeneralError; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.function.Function; import lombok.SneakyThrows; @@ -46,9 +46,9 @@ void shouldInitializeSuccessfully() { multiProvider.initialize(null); MultiProviderMetadata metadata = (MultiProviderMetadata) multiProvider.getMetadata(); - Map map = metadata.getOriginalMetadata(); - assertEquals(mockMetaData1, map.get(mockProvider1.getMetadata().getName())); - assertEquals(mockMetaData2, map.get(mockProvider2.getMetadata().getName())); + List list = metadata.getOriginalMetadata(); + assertTrue(list.contains(mockMetaData1)); + assertTrue(list.contains(mockMetaData2)); assertEquals("multiprovider", multiProvider.getMetadata().getName()); } @@ -93,8 +93,8 @@ void shouldRetrieveCorrectMetadataName() { MultiProvider multiProvider = new MultiProvider(providers, mockStrategy); multiProvider.initialize(null); MultiProviderMetadata metadata = (MultiProviderMetadata) multiProvider.getMetadata(); - Map map = metadata.getOriginalMetadata(); - assertEquals(mockMetaData1, map.get(mockProvider1.getMetadata().getName())); + List list = metadata.getOriginalMetadata(); + assertTrue(list.contains(mockMetaData1)); } @SneakyThrows @@ -126,7 +126,7 @@ void shouldWorkWithCustomStrategy() { @Override public ProviderEvaluation evaluate( - Map providers, + List providers, String key, T defaultValue, EvaluationContext ctx, @@ -138,7 +138,10 @@ public ProviderEvaluation evaluate( } if (contextProvider != null && "new-provider".equals(contextProvider.asString())) { - return providerFunction.apply(providers.get("new-provider")); + return providerFunction.apply(providers.stream() + .filter(p -> "new-provider".equals(p.getMetadata().getName())) + .findFirst() + .orElseThrow()); } return fallbackStrategy.evaluate(providers, key, defaultValue, ctx, providerFunction); }