feat: add multi-provider to SDK (#1765) · open-feature/java-sdk@2d03c15 · GitHub
Skip to content

Commit 2d03c15

Browse files
Vishalup29vishalup29suvaidkhan
authored
feat: add multi-provider to SDK (#1765)
Issue #1486 Move multi-provider into SDK, mark as experimental, and deprecate contrib implementation. Signed-off-by: vishalup29 <vishalupadhyay977@gmail.com> Co-authored-by: vishalup29 <vishalupadhyay977@gmail.com> Co-authored-by: suvaidkhan <khansuvaid@yahoo.com>
1 parent 796e7c3 commit 2d03c15

10 files changed

Lines changed: 937 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 35 additions & 1 deletion
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package dev.openfeature.sdk.multiprovider;
2+
3+
import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND;
4+
5+
import dev.openfeature.sdk.ErrorCode;
6+
import dev.openfeature.sdk.EvaluationContext;
7+
import dev.openfeature.sdk.FeatureProvider;
8+
import dev.openfeature.sdk.ProviderEvaluation;
9+
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
10+
import java.util.Map;
11+
import java.util.function.Function;
12+
import lombok.NoArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
14+
15+
/**
16+
* First match strategy.
17+
*
18+
* <p>Return the first result returned by a provider.
19+
* <ul>
20+
* <li>Skip providers that indicate they had no value due to {@code FLAG_NOT_FOUND}.</li>
21+
* <li>On any other error code, return that error result.</li>
22+
* <li>If a provider throws {@link FlagNotFoundError}, it is treated like {@code FLAG_NOT_FOUND}.</li>
23+
* <li>If all providers report {@code FLAG_NOT_FOUND}, return a {@code FLAG_NOT_FOUND} error.</li>
24+
* </ul>
25+
* As soon as a non-{@code FLAG_NOT_FOUND} result is returned by a provider (success or other error),
26+
* the rest of the operation short-circuits and does not call the remaining providers.
27+
*/
28+
@Slf4j
29+
@NoArgsConstructor
30+
public class FirstMatchStrategy implements Strategy {
31+
32+
@Override
33+
public <T> ProviderEvaluation<T> evaluate(
34+
Map<String, FeatureProvider> providers,
35+
String key,
36+
T defaultValue,
37+
EvaluationContext ctx,
38+
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
39+
for (FeatureProvider provider : providers.values()) {
40+
try {
41+
ProviderEvaluation<T> res = providerFunction.apply(provider);
42+
ErrorCode errorCode = res.getErrorCode();
43+
if (errorCode == null) {
44+
// Successful evaluation
45+
return res;
46+
}
47+
if (!FLAG_NOT_FOUND.equals(errorCode)) {
48+
// Any non-FLAG_NOT_FOUND error bubbles up
49+
return res;
50+
}
51+
// else FLAG_NOT_FOUND: skip to next provider
52+
} catch (FlagNotFoundError ignored) {
53+
// do not log in hot path, just skip
54+
}
55+
}
56+
57+
// All providers either threw or returned FLAG_NOT_FOUND
58+
return ProviderEvaluation.<T>builder()
59+
.errorMessage("Flag not found in any provider")
60+
.errorCode(FLAG_NOT_FOUND)
61+
.build();
62+
}
63+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package dev.openfeature.sdk.multiprovider;
2+
3+
import dev.openfeature.sdk.ErrorCode;
4+
import dev.openfeature.sdk.EvaluationContext;
5+
import dev.openfeature.sdk.FeatureProvider;
6+
import dev.openfeature.sdk.ProviderEvaluation;
7+
import java.util.Map;
8+
import java.util.function.Function;
9+
import lombok.NoArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
12+
/**
13+
* First Successful Strategy.
14+
*
15+
* <p>Similar to “First Match”, except that errors from evaluated providers do not halt execution.
16+
* Instead, it returns the first successful result from a provider. If no provider successfully
17+
* responds, it returns a {@code GENERAL} error result.
18+
*/
19+
@Slf4j
20+
@NoArgsConstructor
21+
public class FirstSuccessfulStrategy implements Strategy {
22+
23+
@Override
24+
public <T> ProviderEvaluation<T> evaluate(
25+
Map<String, FeatureProvider> providers,
26+
String key,
27+
T defaultValue,
28+
EvaluationContext ctx,
29+
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
30+
for (FeatureProvider provider : providers.values()) {
31+
try {
32+
ProviderEvaluation<T> res = providerFunction.apply(provider);
33+
if (res.getErrorCode() == null) {
34+
// First successful result (no error code)
35+
return res;
36+
}
37+
} catch (Exception ignored) {
38+
// swallow and continue; errors from individual providers
39+
// are not fatal for this strategy
40+
}
41+
}
42+
43+
return ProviderEvaluation.<T>builder()
44+
.errorMessage("No provider successfully responded")
45+
.errorCode(ErrorCode.GENERAL)
46+
.build();
47+
}
48+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package dev.openfeature.sdk.multiprovider;
2+
3+
import dev.openfeature.sdk.EvaluationContext;
4+
import dev.openfeature.sdk.EventProvider;
5+
import dev.openfeature.sdk.FeatureProvider;
6+
import dev.openfeature.sdk.Metadata;
7+
import dev.openfeature.sdk.ProviderEvaluation;
8+
import dev.openfeature.sdk.Value;
9+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
10+
import java.util.ArrayList;
11+
import java.util.Collection;
12+
import java.util.Collections;
13+
import java.util.HashMap;
14+
import java.util.LinkedHashMap;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.Objects;
18+
import java.util.concurrent.Callable;
19+
import java.util.concurrent.ExecutorService;
20+
import java.util.concurrent.Executors;
21+
import java.util.concurrent.Future;
22+
import lombok.Getter;
23+
import lombok.extern.slf4j.Slf4j;
24+
25+
/**
26+
* <b>Experimental:</b> Provider implementation for multi-provider.
27+
*
28+
* <p>This provider delegates flag evaluations to multiple underlying providers using a configurable
29+
* {@link Strategy}. It also exposes combined metadata containing the original metadata of each
30+
* underlying provider.
31+
*/
32+
@Slf4j
33+
public class MultiProvider extends EventProvider {
34+
35+
@Getter
36+
private static final String NAME = "multiprovider";
37+
38+
// Use CPU count as upper bound for init threads.
39+
public static final int INIT_THREADS_COUNT = Runtime.getRuntime().availableProcessors();
40+
41+
private final Map<String, FeatureProvider> providers;
42+
private final Strategy strategy;
43+
private MultiProviderMetadata metadata;
44+
45+
/**
46+
* Constructs a MultiProvider with the given list of FeatureProviders, by default uses
47+
* {@link FirstMatchStrategy}.
48+
*
49+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
50+
*/
51+
public MultiProvider(List<FeatureProvider> providers) {
52+
this(providers, new FirstMatchStrategy());
53+
}
54+
55+
/**
56+
* Constructs a MultiProvider with the given list of FeatureProviders and a strategy.
57+
*
58+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
59+
* @param strategy the strategy (if {@code null}, {@link FirstMatchStrategy} is used)
60+
*/
61+
public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
62+
this.providers = buildProviders(providers);
63+
this.strategy = Objects.requireNonNull(strategy, "strategy must not be null");
64+
}
65+
66+
protected static Map<String, FeatureProvider> buildProviders(List<FeatureProvider> providers) {
67+
Map<String, FeatureProvider> providersMap = new LinkedHashMap<>(providers.size());
68+
for (FeatureProvider provider : providers) {
69+
FeatureProvider prevProvider =
70+
providersMap.put(provider.getMetadata().getName(), provider);
71+
if (prevProvider != null) {
72+
log.info("duplicated provider name: {}", provider.getMetadata().getName());
73+
}
74+
}
75+
return Collections.unmodifiableMap(providersMap);
76+
}
77+
78+
/**
79+
* Initialize the provider.
80+
*
81+
* @param evaluationContext evaluation context
82+
* @throws Exception on error (e.g. wrapped {@link java.util.concurrent.ExecutionException}
83+
* from a failing provider)
84+
*/
85+
@Override
86+
public void initialize(EvaluationContext evaluationContext) throws Exception {
87+
var metadataBuilder = MultiProviderMetadata.builder().name(NAME);
88+
HashMap<String, Metadata> providersMetadata = new HashMap<>();
89+
90+
if (providers.isEmpty()) {
91+
metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));
92+
metadata = metadataBuilder.build();
93+
return;
94+
}
95+
96+
ExecutorService executorService = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size()));
97+
try {
98+
Collection<Callable<Void>> tasks = new ArrayList<>(providers.size());
99+
for (FeatureProvider provider : providers.values()) {
100+
tasks.add(() -> {
101+
provider.initialize(evaluationContext);
102+
return null;
103+
});
104+
Metadata providerMetadata = provider.getMetadata();
105+
providersMetadata.put(providerMetadata.getName(), providerMetadata);
106+
}
107+
108+
metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));
109+
110+
List<Future<Void>> results = executorService.invokeAll(tasks);
111+
for (Future<Void> result : results) {
112+
// This will re-throw any exception from the provider's initialize method,
113+
// wrapped in an ExecutionException.
114+
result.get();
115+
}
116+
} catch (Exception e) {
117+
// If initialization fails for any provider, attempt to shut down via the
118+
// standard shutdown path to avoid a partial/limbo state.
119+
try {
120+
shutdown();
121+
} catch (Exception shutdownEx) {
122+
log.error("error during shutdown after failed initialize", shutdownEx);
123+
}
124+
throw e;
125+
} finally {
126+
executorService.shutdown();
127+
}
128+
129+
metadata = metadataBuilder.build();
130+
}
131+
132+
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
133+
@Override
134+
public Metadata getMetadata() {
135+
return metadata;
136+
}
137+
138+
@Override
139+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
140+
return strategy.evaluate(
141+
providers, key, defaultValue, ctx, p -> p.getBooleanEvaluation(key, defaultValue, ctx));
142+
}
143+
144+
@Override
145+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
146+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getStringEvaluation(key, defaultValue, ctx));
147+
}
148+
149+
@Override
150+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
151+
return strategy.evaluate(
152+
providers, key, defaultValue, ctx, p -> p.getIntegerEvaluation(key, defaultValue, ctx));
153+
}
154+
155+
@Override
156+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
157+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getDoubleEvaluation(key, defaultValue, ctx));
158+
}
159+
160+
@Override
161+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
162+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getObjectEvaluation(key, defaultValue, ctx));
163+
}
164+
165+
@Override
166+
public void shutdown() {
167+
log.debug("shutdown begin");
168+
for (FeatureProvider provider : providers.values()) {
169+
try {
170+
provider.shutdown();
171+
} catch (Exception e) {
172+
log.error("error shutdown provider {}", provider.getMetadata().getName(), e);
173+
}
174+
}
175+
log.debug("shutdown end");
176+
// Important: ensure EventProvider's executor is also shut down
177+
super.shutdown();
178+
}
179+
}
Lines changed: 20 additions & 0 deletions

0 commit comments

Comments
 (0)