Skip to content

Commit f07dd25

Browse files
committed
MultiProvider: Added strategies, unit tests and documentation
Signed-off-by: suvaidkhan <[email protected]>
1 parent e67f598 commit f07dd25

File tree

6 files changed

+540
-0
lines changed

6 files changed

+540
-0
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@
7070
<version>2.0.17</version>
7171
</dependency>
7272

73+
<dependency>
74+
<groupId>org.json</groupId>
75+
<artifactId>json</artifactId>
76+
<version>20250517</version>
77+
</dependency>
78+
7379
<!-- test -->
7480
<dependency>
7581
<groupId>com.tngtech.archunit</groupId>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package dev.openfeature.sdk.multiprovider;
2+
3+
import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND;
4+
5+
import dev.openfeature.sdk.EvaluationContext;
6+
import dev.openfeature.sdk.FeatureProvider;
7+
import dev.openfeature.sdk.ProviderEvaluation;
8+
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
9+
import java.util.Map;
10+
import java.util.function.Function;
11+
import lombok.NoArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
14+
/**
15+
* First match strategy. Return the first result returned by a provider. Skip providers that
16+
* indicate they had no value due to FLAG_NOT_FOUND. In all other cases, use the value returned by
17+
* the provider. If any provider returns an error result other than FLAG_NOT_FOUND, the whole
18+
* evaluation should error and “bubble up” the individual provider’s error in the result. As soon as
19+
* a value is returned by a provider, the rest of the operation should short-circuit and not call
20+
* the rest of the providers.
21+
*/
22+
@Slf4j
23+
@NoArgsConstructor
24+
public class FirstMatchStrategy implements Strategy {
25+
26+
/**
27+
* Represents a strategy that evaluates providers based on a first-match approach. Provides a
28+
* method to evaluate providers using a specified function and return the evaluation result.
29+
*
30+
* @param providerFunction provider function
31+
* @param <T> ProviderEvaluation type
32+
* @return the provider evaluation
33+
*/
34+
@Override
35+
public <T> ProviderEvaluation<T> evaluate(
36+
Map<String, FeatureProvider> providers,
37+
String key,
38+
T defaultValue,
39+
EvaluationContext ctx,
40+
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
41+
for (FeatureProvider provider : providers.values()) {
42+
try {
43+
ProviderEvaluation<T> res = providerFunction.apply(provider);
44+
if (!FLAG_NOT_FOUND.equals(res.getErrorCode())) {
45+
return res;
46+
}
47+
} catch (FlagNotFoundError e) {
48+
log.debug("flag not found {}", e.getMessage());
49+
}
50+
}
51+
throw new FlagNotFoundError("flag not found");
52+
}
53+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dev.openfeature.sdk.multiprovider;
2+
3+
import dev.openfeature.sdk.EvaluationContext;
4+
import dev.openfeature.sdk.FeatureProvider;
5+
import dev.openfeature.sdk.ProviderEvaluation;
6+
import dev.openfeature.sdk.exceptions.GeneralError;
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. Similar to “First Match”, except that errors from evaluated providers
14+
* do not halt execution. Instead, it will return the first successful result from a provider. If no
15+
* provider successfully responds, it will throw an error result.
16+
*/
17+
@Slf4j
18+
@NoArgsConstructor
19+
public class FirstSuccessfulStrategy implements Strategy {
20+
21+
@Override
22+
public <T> ProviderEvaluation<T> evaluate(
23+
Map<String, FeatureProvider> providers,
24+
String key,
25+
T defaultValue,
26+
EvaluationContext ctx,
27+
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
28+
for (FeatureProvider provider : providers.values()) {
29+
try {
30+
ProviderEvaluation<T> res = providerFunction.apply(provider);
31+
if (res.getErrorCode() == null) {
32+
return res;
33+
}
34+
} catch (Exception e) {
35+
log.debug("evaluation exception {}", e.getMessage());
36+
}
37+
}
38+
39+
throw new GeneralError("evaluation error");
40+
}
41+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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 dev.openfeature.sdk.exceptions.GeneralError;
10+
import java.util.ArrayList;
11+
import java.util.Collection;
12+
import java.util.Collections;
13+
import java.util.LinkedHashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.concurrent.Callable;
17+
import java.util.concurrent.ExecutorService;
18+
import java.util.concurrent.Executors;
19+
import java.util.concurrent.Future;
20+
import lombok.Getter;
21+
import lombok.extern.slf4j.Slf4j;
22+
import org.json.JSONObject;
23+
24+
/** <b>Experimental:</b> Provider implementation for Multi-provider. */
25+
@Slf4j
26+
public class MultiProvider extends EventProvider {
27+
28+
@Getter
29+
private static final String NAME = "multiprovider";
30+
31+
public static final int INIT_THREADS_COUNT = 8;
32+
private final Map<String, FeatureProvider> providers;
33+
private final Strategy strategy;
34+
private String metadataName;
35+
36+
/**
37+
* Constructs a MultiProvider with the given list of FeatureProviders, using a default strategy.
38+
*
39+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
40+
*/
41+
public MultiProvider(List<FeatureProvider> providers) {
42+
this(providers, null);
43+
}
44+
45+
/**
46+
* Constructs a MultiProvider with the given list of FeatureProviders and a strategy.
47+
*
48+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
49+
* @param strategy the strategy
50+
*/
51+
public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
52+
this.providers = buildProviders(providers);
53+
if (strategy != null) {
54+
this.strategy = strategy;
55+
} else {
56+
this.strategy = new FirstMatchStrategy();
57+
}
58+
}
59+
60+
protected static Map<String, FeatureProvider> buildProviders(List<FeatureProvider> providers) {
61+
Map<String, FeatureProvider> providersMap = new LinkedHashMap<>(providers.size());
62+
for (FeatureProvider provider : providers) {
63+
FeatureProvider prevProvider =
64+
providersMap.put(provider.getMetadata().getName(), provider);
65+
if (prevProvider != null) {
66+
log.warn("duplicated provider name: {}", provider.getMetadata().getName());
67+
}
68+
}
69+
return Collections.unmodifiableMap(providersMap);
70+
}
71+
72+
/**
73+
* Initialize the provider.
74+
*
75+
* @param evaluationContext evaluation context
76+
* @throws Exception on error
77+
*/
78+
@Override
79+
public void initialize(EvaluationContext evaluationContext) throws Exception {
80+
JSONObject json = new JSONObject();
81+
json.put("name", NAME);
82+
JSONObject providersMetadata = new JSONObject();
83+
json.put("originalMetadata", providersMetadata);
84+
ExecutorService initPool = Executors.newFixedThreadPool(INIT_THREADS_COUNT);
85+
Collection<Callable<Boolean>> tasks = new ArrayList<>(providers.size());
86+
for (FeatureProvider provider : providers.values()) {
87+
tasks.add(() -> {
88+
provider.initialize(evaluationContext);
89+
return true;
90+
});
91+
JSONObject providerMetadata = new JSONObject();
92+
providerMetadata.put("name", provider.getMetadata().getName());
93+
providersMetadata.put(provider.getMetadata().getName(), providerMetadata);
94+
}
95+
List<Future<Boolean>> results = initPool.invokeAll(tasks);
96+
for (Future<Boolean> result : results) {
97+
if (!result.get()) {
98+
throw new GeneralError("init failed");
99+
}
100+
}
101+
metadataName = json.toString();
102+
}
103+
104+
@Override
105+
public Metadata getMetadata() {
106+
return () -> metadataName;
107+
}
108+
109+
@Override
110+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
111+
return strategy.evaluate(
112+
providers, key, defaultValue, ctx, p -> p.getBooleanEvaluation(key, defaultValue, ctx));
113+
}
114+
115+
@Override
116+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
117+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getStringEvaluation(key, defaultValue, ctx));
118+
}
119+
120+
@Override
121+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
122+
return strategy.evaluate(
123+
providers, key, defaultValue, ctx, p -> p.getIntegerEvaluation(key, defaultValue, ctx));
124+
}
125+
126+
@Override
127+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
128+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getDoubleEvaluation(key, defaultValue, ctx));
129+
}
130+
131+
@Override
132+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
133+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getObjectEvaluation(key, defaultValue, ctx));
134+
}
135+
136+
@Override
137+
public void shutdown() {
138+
log.debug("shutdown begin");
139+
for (FeatureProvider provider : providers.values()) {
140+
try {
141+
provider.shutdown();
142+
} catch (Exception e) {
143+
log.error("error shutdown provider {}", provider.getMetadata().getName(), e);
144+
}
145+
}
146+
log.debug("shutdown end");
147+
}
148+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package dev.openfeature.sdk.multiprovider;
2+
3+
import dev.openfeature.sdk.EvaluationContext;
4+
import dev.openfeature.sdk.FeatureProvider;
5+
import dev.openfeature.sdk.ProviderEvaluation;
6+
import java.util.Map;
7+
import java.util.function.Function;
8+
9+
/** strategy. */
10+
public interface Strategy {
11+
<T> ProviderEvaluation<T> evaluate(
12+
Map<String, FeatureProvider> providers,
13+
String key,
14+
T defaultValue,
15+
EvaluationContext ctx,
16+
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction);
17+
}

0 commit comments

Comments
 (0)