Skip to content

Commit 1a285f4

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 947c1e2 commit 1a285f4

File tree

10 files changed

+933
-1
lines changed

10 files changed

+933
-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: 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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
public static final int INIT_THREADS_COUNT = 8;
39+
40+
private final Map<String, FeatureProvider> providers;
41+
private final Strategy strategy;
42+
private MultiProviderMetadata metadata;
43+
44+
/**
45+
* Constructs a MultiProvider with the given list of FeatureProviders, by default uses
46+
* {@link FirstMatchStrategy}.
47+
*
48+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
49+
*/
50+
public MultiProvider(List<FeatureProvider> providers) {
51+
this(providers, new FirstMatchStrategy());
52+
}
53+
54+
/**
55+
* Constructs a MultiProvider with the given list of FeatureProviders and a strategy.
56+
*
57+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
58+
* @param strategy the strategy (if {@code null}, {@link FirstMatchStrategy} is used)
59+
*/
60+
public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
61+
this.providers = buildProviders(providers);
62+
this.strategy = Objects.requireNonNull(strategy, "strategy must not be null");
63+
}
64+
65+
protected static Map<String, FeatureProvider> buildProviders(List<FeatureProvider> providers) {
66+
Map<String, FeatureProvider> providersMap = new LinkedHashMap<>(providers.size());
67+
for (FeatureProvider provider : providers) {
68+
FeatureProvider prevProvider =
69+
providersMap.put(provider.getMetadata().getName(), provider);
70+
if (prevProvider != null) {
71+
log.warn("duplicated provider name: {}", provider.getMetadata().getName());
72+
}
73+
}
74+
return Collections.unmodifiableMap(providersMap);
75+
}
76+
77+
/**
78+
* Initialize the provider.
79+
*
80+
* @param evaluationContext evaluation context
81+
* @throws Exception on error (e.g. wrapped {@link java.util.concurrent.ExecutionException}
82+
* from a failing provider)
83+
*/
84+
@Override
85+
public void initialize(EvaluationContext evaluationContext) throws Exception {
86+
var metadataBuilder = MultiProviderMetadata.builder().name(NAME);
87+
HashMap<String, Metadata> providersMetadata = new HashMap<>();
88+
89+
if (providers.isEmpty()) {
90+
metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));
91+
metadata = metadataBuilder.build();
92+
return;
93+
}
94+
95+
ExecutorService executorService = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size()));
96+
try {
97+
Collection<Callable<Void>> tasks = new ArrayList<>(providers.size());
98+
for (FeatureProvider provider : providers.values()) {
99+
tasks.add(() -> {
100+
provider.initialize(evaluationContext);
101+
return null;
102+
});
103+
Metadata providerMetadata = provider.getMetadata();
104+
providersMetadata.put(providerMetadata.getName(), providerMetadata);
105+
}
106+
107+
metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));
108+
109+
List<Future<Void>> results = executorService.invokeAll(tasks);
110+
for (Future<Void> result : results) {
111+
// This will re-throw any exception from the provider's initialize method,
112+
// wrapped in an ExecutionException.
113+
result.get();
114+
}
115+
} catch (Exception e) {
116+
// If initialization fails for any provider, attempt to shut down all providers
117+
// to avoid a partial/limbo state.
118+
for (FeatureProvider provider : providers.values()) {
119+
try {
120+
provider.shutdown();
121+
} catch (Exception shutdownEx) {
122+
log.error(
123+
"error shutting down provider {} after failed initialize",
124+
provider.getMetadata().getName(),
125+
shutdownEx);
126+
}
127+
}
128+
throw e;
129+
} finally {
130+
executorService.shutdown();
131+
}
132+
133+
metadata = metadataBuilder.build();
134+
}
135+
136+
@SuppressFBWarnings(value = "EI_EXPOSE_REP")
137+
@Override
138+
public Metadata getMetadata() {
139+
return metadata;
140+
}
141+
142+
@Override
143+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
144+
return strategy.evaluate(
145+
providers, key, defaultValue, ctx, p -> p.getBooleanEvaluation(key, defaultValue, ctx));
146+
}
147+
148+
@Override
149+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
150+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getStringEvaluation(key, defaultValue, ctx));
151+
}
152+
153+
@Override
154+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
155+
return strategy.evaluate(
156+
providers, key, defaultValue, ctx, p -> p.getIntegerEvaluation(key, defaultValue, ctx));
157+
}
158+
159+
@Override
160+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
161+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getDoubleEvaluation(key, defaultValue, ctx));
162+
}
163+
164+
@Override
165+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
166+
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getObjectEvaluation(key, defaultValue, ctx));
167+
}
168+
169+
@Override
170+
public void shutdown() {
171+
log.debug("shutdown begin");
172+
for (FeatureProvider provider : providers.values()) {
173+
try {
174+
provider.shutdown();
175+
} catch (Exception e) {
176+
log.error("error shutdown provider {}", provider.getMetadata().getName(), e);
177+
}
178+
}
179+
log.debug("shutdown end");
180+
// Important: ensure EventProvider's executor is also shut down
181+
super.shutdown();
182+
}
183+
}
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)