Skip to content

Commit 0f1e6d2

Browse files
author
vishalup29
committed
Issue #1486 Move multi-provider into SDK, mark as experimental, and deprecate contrib implementation.
Signed-off-by: vishalup29 <vishalupadhyay977@gmail.com>
1 parent 1506a10 commit 0f1e6d2

File tree

7 files changed

+515
-1
lines changed

7 files changed

+515
-1
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.
129129
| ------ |---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
130130
|| [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
131131
|| [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
132+
|| [Multi-provider (experimental)](#multi-provider-experimental) | Combine multiple providers and delegate evaluations according to a strategy. |
132133
|| [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
133134
|| [Tracking](#tracking) | Associate user actions with feature flag evaluations. |
134135
|| [Logging](#logging) | Integrate with popular logging packages. |
@@ -147,7 +148,40 @@ Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D
147148
If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself.
148149

149150
Once you've added a provider as a dependency, it can be registered with OpenFeature like this:
150-
151+
152+
In some situations, it may be beneficial to register multiple providers in the same application.
153+
This is possible using [domains](#domains), which is covered in more detail below.
154+
155+
#### Multi-provider (experimental)
156+
157+
In addition to domains, you may want to delegate flag evaluation across multiple providers using a configurable strategy.
158+
The multi-provider allows you to compose several `FeatureProvider` implementations and determine which provider's result to use.
159+
160+
> **Experimental:** This API is experimental and may change in future releases.
161+
162+
```java
163+
import dev.openfeature.sdk.OpenFeatureAPI;
164+
import dev.openfeature.sdk.Client;
165+
import dev.openfeature.sdk.FeatureProvider;
166+
import dev.openfeature.sdk.providers.multiprovider.MultiProvider;
167+
168+
import java.util.List;
169+
170+
public void multiProviderExample() throws Exception {
171+
FeatureProvider primaryProvider = new MyPrimaryProvider();
172+
FeatureProvider fallbackProvider = new MyFallbackProvider();
173+
174+
MultiProvider multiProvider = new MultiProvider(List.of(primaryProvider, fallbackProvider));
175+
176+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
177+
api.setProviderAndWait(multiProvider);
178+
179+
Client client = api.getClient();
180+
boolean value = client.getBooleanValue("some-flag", false);
181+
}
182+
```
183+
184+
151185
#### Synchronous
152186

153187
To register a provider in a blocking manner to ensure it is ready before further actions are taken, you can use the `setProviderAndWait` method as shown below:

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
5252

5353
<dependencies>
5454

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