Skip to content

Commit b5f371d

Browse files
author
vishalup29
committed
Issue #1486 Move multi-provider into SDK, mark as experimental, and deprecate contrib implementation.
Signed-off-by: vishalup29 <[email protected]>
1 parent 1506a10 commit b5f371d

File tree

6 files changed

+527
-1
lines changed

6 files changed

+527
-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:
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: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 java.util.ArrayList;
10+
import java.util.Collection;
11+
import java.util.Collections;
12+
import java.util.LinkedHashMap;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.concurrent.Callable;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.Future;
19+
import lombok.Getter;
20+
import lombok.extern.slf4j.Slf4j;
21+
22+
/**
23+
* A {@link dev.openfeature.sdk.FeatureProvider} that composes multiple providers.
24+
*
25+
* <p><strong>Experimental:</strong> This API is experimental and may change in future releases.</p>
26+
*/
27+
@Slf4j
28+
public class MultiProvider extends EventProvider {
29+
30+
@Getter
31+
private static final String NAME = "multiprovider";
32+
33+
public static final int INIT_THREADS_COUNT = 8;
34+
35+
private final Map<String, FeatureProvider> providers;
36+
private final Strategy strategy;
37+
private final String metadataName;
38+
39+
/**
40+
* Constructs a MultiProvider with the given list of FeatureProviders, using a default strategy.
41+
*
42+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
43+
*/
44+
public MultiProvider(List<FeatureProvider> providers) {
45+
this(providers, null);
46+
}
47+
48+
/**
49+
* Constructs a MultiProvider with the given list of FeatureProviders and a strategy.
50+
*
51+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
52+
* @param strategy the strategy
53+
*/
54+
public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
55+
this.providers = buildProviders(providers);
56+
if (strategy != null) {
57+
this.strategy = strategy;
58+
} else {
59+
this.strategy = new FirstMatchStrategy();
60+
}
61+
this.metadataName = buildMetadataName(this.providers);
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+
private static String buildMetadataName(Map<String, FeatureProvider> providers) {
77+
StringBuilder sb = new StringBuilder(NAME).append('[');
78+
boolean first = true;
79+
for (String providerName : providers.keySet()) {
80+
if (!first) {
81+
sb.append(',');
82+
}
83+
sb.append(providerName);
84+
first = false;
85+
}
86+
sb.append(']');
87+
return sb.toString();
88+
}
89+
90+
/**
91+
* Initialize the provider.
92+
*
93+
* @param evaluationContext evaluation context
94+
* @throws Exception on error
95+
*/
96+
@Override
97+
public void initialize(EvaluationContext evaluationContext) throws Exception {
98+
ExecutorService initPool = Executors.newFixedThreadPool(INIT_THREADS_COUNT);
99+
try {
100+
Collection<Callable<Boolean>> tasks = new ArrayList<>(providers.size());
101+
for (FeatureProvider provider : providers.values()) {
102+
tasks.add(() -> {
103+
provider.initialize(evaluationContext);
104+
return true;
105+
});
106+
}
107+
List<Future<Boolean>> results = initPool.invokeAll(tasks);
108+
for (Future<Boolean> result : results) {
109+
// result.get() will propagate any initialization exception
110+
result.get();
111+
}
112+
} finally {
113+
initPool.shutdown();
114+
}
115+
}
116+
117+
@Override
118+
public Metadata getMetadata() {
119+
return () -> metadataName;
120+
}
121+
122+
@Override
123+
public ProviderEvaluation<Boolean> getBooleanEvaluation(
124+
String key, Boolean defaultValue, EvaluationContext ctx) {
125+
return strategy.evaluate(
126+
providers, key, defaultValue, ctx, p -> p.getBooleanEvaluation(key, defaultValue, ctx));
127+
}
128+
129+
@Override
130+
public ProviderEvaluation<String> getStringEvaluation(
131+
String key, String defaultValue, EvaluationContext ctx) {
132+
return strategy.evaluate(
133+
providers, key, defaultValue, ctx, p -> p.getStringEvaluation(key, defaultValue, ctx));
134+
}
135+
136+
@Override
137+
public ProviderEvaluation<Integer> getIntegerEvaluation(
138+
String key, Integer defaultValue, EvaluationContext ctx) {
139+
return strategy.evaluate(
140+
providers, key, defaultValue, ctx, p -> p.getIntegerEvaluation(key, defaultValue, ctx));
141+
}
142+
143+
@Override
144+
public ProviderEvaluation<Double> getDoubleEvaluation(
145+
String key, Double defaultValue, EvaluationContext ctx) {
146+
return strategy.evaluate(
147+
providers, key, defaultValue, ctx, p -> p.getDoubleEvaluation(key, defaultValue, ctx));
148+
}
149+
150+
@Override
151+
public ProviderEvaluation<Value> getObjectEvaluation(
152+
String key, Value defaultValue, EvaluationContext ctx) {
153+
return strategy.evaluate(
154+
providers, key, defaultValue, ctx, p -> p.getObjectEvaluation(key, defaultValue, ctx));
155+
}
156+
157+
@Override
158+
public void shutdown() {
159+
log.debug("shutdown begin");
160+
for (FeatureProvider provider : providers.values()) {
161+
try {
162+
provider.shutdown();
163+
} catch (Exception e) {
164+
log.error("error shutdown provider {}", provider.getMetadata().getName(), e);
165+
}
166+
}
167+
log.debug("shutdown end");
168+
super.shutdown();
169+
}
170+
}
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+
/** Defines a strategy for evaluating flags across multiple providers. */
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)