Skip to content

Commit ce61f2f

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 ce61f2f

File tree

10 files changed

+949
-1
lines changed

10 files changed

+949
-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.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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 e) {
53+
log.debug(
54+
"flag not found {} in provider {}",
55+
key,
56+
provider.getMetadata().getName(),
57+
e);
58+
}
59+
}
60+
61+
// All providers either threw or returned FLAG_NOT_FOUND
62+
return ProviderEvaluation.<T>builder()
63+
.errorMessage("Flag not found in any provider")
64+
.errorCode(FLAG_NOT_FOUND)
65+
.build();
66+
}
67+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 e) {
38+
log.debug(
39+
"evaluation exception for key {} in provider {}",
40+
key,
41+
provider.getMetadata().getName(),
42+
e);
43+
}
44+
}
45+
46+
return ProviderEvaluation.<T>builder()
47+
.errorMessage("No provider successfully responded")
48+
.errorCode(ErrorCode.GENERAL)
49+
.build();
50+
}
51+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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.concurrent.Callable;
18+
import java.util.concurrent.ExecutorService;
19+
import java.util.concurrent.Executors;
20+
import java.util.concurrent.Future;
21+
import lombok.Getter;
22+
import lombok.extern.slf4j.Slf4j;
23+
24+
/**
25+
* <b>Experimental:</b> Provider implementation for multi-provider.
26+
*
27+
* <p>This provider delegates flag evaluations to multiple underlying providers using a configurable
28+
* {@link Strategy}. It also exposes combined metadata containing the original metadata of each
29+
* underlying provider.
30+
*/
31+
@Slf4j
32+
public class MultiProvider extends EventProvider {
33+
34+
@Getter
35+
private static final String NAME = "multiprovider";
36+
37+
public static final int INIT_THREADS_COUNT = 8;
38+
39+
private final Map<String, FeatureProvider> providers;
40+
private final Strategy strategy;
41+
private MultiProviderMetadata metadata;
42+
43+
/**
44+
* Constructs a MultiProvider with the given list of FeatureProviders, by default uses
45+
* {@link FirstMatchStrategy}.
46+
*
47+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
48+
*/
49+
public MultiProvider(List<FeatureProvider> providers) {
50+
this(providers, null);
51+
}
52+
53+
/**
54+
* Constructs a MultiProvider with the given list of FeatureProviders and a strategy.
55+
*
56+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
57+
* @param strategy the strategy (if {@code null}, {@link FirstMatchStrategy} is used)
58+
*/
59+
public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
60+
this.providers = buildProviders(providers);
61+
if (strategy != null) {
62+
this.strategy = strategy;
63+
} else {
64+
this.strategy = new FirstMatchStrategy();
65+
}
66+
}
67+
68+
protected static Map<String, FeatureProvider> buildProviders(List<FeatureProvider> providers) {
69+
Map<String, FeatureProvider> providersMap = new LinkedHashMap<>(providers.size());
70+
for (FeatureProvider provider : providers) {
71+
FeatureProvider prevProvider =
72+
providersMap.put(provider.getMetadata().getName(), provider);
73+
if (prevProvider != null) {
74+
log.warn("duplicated provider name: {}", provider.getMetadata().getName());
75+
}
76+
}
77+
return Collections.unmodifiableMap(providersMap);
78+
}
79+
80+
/**
81+
* Initialize the provider.
82+
*
83+
* @param evaluationContext evaluation context
84+
* @throws Exception on error (e.g. wrapped {@link java.util.concurrent.ExecutionException}
85+
* from a failing provider)
86+
*/
87+
@Override
88+
public void initialize(EvaluationContext evaluationContext) throws Exception {
89+
var metadataBuilder = MultiProviderMetadata.builder().name(NAME);
90+
HashMap<String, Metadata> providersMetadata = new HashMap<>();
91+
92+
if (providers.isEmpty()) {
93+
metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));
94+
metadata = metadataBuilder.build();
95+
return;
96+
}
97+
98+
ExecutorService executorService = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size()));
99+
try {
100+
Collection<Callable<Void>> tasks = new ArrayList<>(providers.size());
101+
for (FeatureProvider provider : providers.values()) {
102+
tasks.add(() -> {
103+
provider.initialize(evaluationContext);
104+
return null;
105+
});
106+
Metadata providerMetadata = provider.getMetadata();
107+
providersMetadata.put(providerMetadata.getName(), providerMetadata);
108+
}
109+
110+
metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));
111+
112+
List<Future<Void>> results = executorService.invokeAll(tasks);
113+
for (Future<Void> result : results) {
114+
// This will re-throw any exception from the provider's initialize method,
115+
// wrapped in an ExecutionException.
116+
result.get();
117+
}
118+
} catch (Exception e) {
119+
// If initialization fails for any provider, attempt to shut down all providers
120+
// to avoid a partial/limbo state.
121+
for (FeatureProvider provider : providers.values()) {
122+
try {
123+
provider.shutdown();
124+
} catch (Exception shutdownEx) {
125+
log.error(
126+
"error shutting down provider {} after failed initialize",
127+
provider.getMetadata().getName(),
128+
shutdownEx);
129+
}
130+
}
131+
throw e;
132+
} finally {
133+
executorService.shutdown();
134+
}
135+
136+
metadata = metadataBuilder.build();
137+
}
138+
139+
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
140+
@Override
141+
public Metadata getMetadata() {
142+
return metadata;
143+
}
144+
145+
@Override
146+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
147+
return strategy.evaluate(
148+
providers, key, defaultValue, ctx, p -> p.getBooleanEvaluation(key, defaultValue, ctx));
149+
}
150+
151+
@Override
152+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
153+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getStringEvaluation(key, defaultValue, ctx));
154+
}
155+
156+
@Override
157+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
158+
return strategy.evaluate(
159+
providers, key, defaultValue, ctx, p -> p.getIntegerEvaluation(key, defaultValue, ctx));
160+
}
161+
162+
@Override
163+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
164+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getDoubleEvaluation(key, defaultValue, ctx));
165+
}
166+
167+
@Override
168+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
169+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getObjectEvaluation(key, defaultValue, ctx));
170+
}
171+
172+
@Override
173+
public void shutdown() {
174+
log.debug("shutdown begin");
175+
for (FeatureProvider provider : providers.values()) {
176+
try {
177+
provider.shutdown();
178+
} catch (Exception e) {
179+
log.error("error shutdown provider {}", provider.getMetadata().getName(), e);
180+
}
181+
}
182+
log.debug("shutdown end");
183+
// Important: ensure EventProvider's executor is also shut down
184+
super.shutdown();
185+
}
186+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package dev.openfeature.sdk.multiprovider;
2+
3+
import dev.openfeature.sdk.Metadata;
4+
import java.util.Map;
5+
import lombok.Builder;
6+
import lombok.Value;
7+
8+
/**
9+
* Metadata for {@link MultiProvider}.
10+
*
11+
* <p>Contains the multiprovider's own name and a map of the original metadata from each underlying
12+
* provider.
13+
*/
14+
@Value
15+
@Builder
16+
public class MultiProviderMetadata implements Metadata {
17+
18+
String name;
19+
Map<String, Metadata> originalMetadata;
20+
}

0 commit comments

Comments
 (0)