Skip to content

Commit 5a44360

Browse files
liran2000matheusverissimo
authored andcommitted
feat: multi-provider implementation (open-feature#1028)
Signed-off-by: liran2000 <[email protected]> Signed-off-by: Liran M <[email protected]> Signed-off-by: Matheus Veríssimo <[email protected]>
1 parent f8c4d79 commit 5a44360

File tree

14 files changed

+614
-0
lines changed

14 files changed

+614
-0
lines changed

.github/component_owners.yml

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ components:
3535
- novalisdenahi
3636
providers/statsig:
3737
- liran2000
38+
providers/multiprovider:
39+
- liran2000
3840

3941
ignored-authors:
4042
- renovate-bot

.release-please-manifest.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
"providers/flipt": "0.1.1",
1010
"providers/configcat": "0.1.0",
1111
"providers/statsig": "0.1.0",
12+
"providers/multiprovider": "0.0.1",
1213
"tools/junit-openfeature": "0.1.1"
1314
}

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<module>providers/flipt</module>
3939
<module>providers/configcat</module>
4040
<module>providers/statsig</module>
41+
<module>providers/multiprovider</module>
4142
</modules>
4243

4344
<scm>

providers/multiprovider/README.md

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# OpenFeature Multi-Provider for Java
2+
3+
The OpenFeature Multi-Provider wraps multiple underlying providers in a unified interface, allowing the SDK client to transparently interact with all those providers at once.
4+
This allows use cases where a single client and evaluation interface is desired, but where the flag data should come from more than one source.
5+
6+
Some examples:
7+
8+
- A migration from one feature flagging provider to another.
9+
During that process, you may have some flags that have been ported to the new system and others that haven’t.
10+
Therefore, you’d want the Multi-Provider to return the result of the “new” system if available otherwise, return the "old" system’s result.
11+
- Long-term use of multiple sources for flags.
12+
For example, someone might want to be able to combine environment variables, database entries, and vendor feature flag results together in a single interface, and define the precedence order in which those sources should be consulted.
13+
- Setting a fallback for cloud providers.
14+
You can use the Multi-Provider to automatically fall back to a local configuration if an external vendor provider goes down, rather than using the default values.
15+
By using the FirstSuccessfulStrategy, the Multi-Provider will move on to the next provider in the list if an error is thrown.
16+
17+
## Strategies
18+
19+
The Multi-Provider supports multiple ways of deciding how to evaluate the set of providers it is managing, and how to deal with any errors that are thrown.
20+
21+
Strategies must be adaptable to the various requirements that might be faced in a multi-provider situation.
22+
In some cases, the strategy may want to ignore errors from individual providers as long as one of them successfully responds.
23+
In other cases, it may want to evaluate providers in order and skip the rest if a successful result is obtained.
24+
In still other scenarios, it may be required to always call every provider and decide what to do with the set of results.
25+
26+
The strategy to use is passed in to the Multi-Provider.
27+
28+
By default, the Multi-Provider uses the “FirstMatchStrategy”.
29+
30+
Here are some standard strategies that come with the Multi-Provider:
31+
32+
### First Match
33+
34+
Return the first result returned by a provider.
35+
Skip providers that indicate they had no value due to `FLAG_NOT_FOUND`.
36+
In all other cases, use the value returned by the provider.
37+
If any provider returns an error result other than `FLAG_NOT_FOUND`, the whole evaluation should error and “bubble up” the individual provider’s error in the result.
38+
39+
As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call the rest of the providers.
40+
41+
### First Successful
42+
43+
Similar to “First Match”, except that errors from evaluated providers do not halt execution.
44+
Instead, it will return the first successful result from a provider. If no provider successfully responds, it will throw an error result.
45+
46+
### User Defined
47+
48+
Rather than making assumptions about when to use a provider’s result and when not to (which may not hold across all providers) there is also a way for the user to define their own strategy that determines whether to use a result or fall through to the next one.
49+
50+
## Installation
51+
52+
<!-- x-release-please-start-version -->
53+
54+
```xml
55+
56+
<dependency>
57+
<groupId>dev.openfeature.contrib.providers</groupId>
58+
<artifactId>multi-provider</artifactId>
59+
<version>0.0.1</version>
60+
</dependency>
61+
```
62+
63+
<!-- x-release-please-end-version -->
64+
65+
## Usage
66+
67+
Usage example:
68+
69+
```
70+
...
71+
List<FeatureProvider> providers = new ArrayList<>(2);
72+
providers.add(provider1);
73+
providers.add(provider2);
74+
75+
// initialize using default strategy (first match)
76+
MultiProvider multiProvider = new MultiProvider(providers);
77+
OpenFeatureAPI.getInstance().setProviderAndWait(multiProvider);
78+
79+
// initialize using a different strategy
80+
multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy());
81+
...
82+
```
83+
84+
See [MultiProviderTest](./src/test/java/dev/openfeature/contrib/providers/multiprovider/MultiProviderTest.java)
85+
for more information.
86+

providers/multiprovider/lombok.config

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# This file is needed to avoid errors throw by findbugs when working with lombok.
2+
lombok.addSuppressWarnings = true
3+
lombok.addLombokGeneratedAnnotation = true
4+
config.stopBubbling = true
5+
lombok.extern.findbugs.addSuppressFBWarnings = true

providers/multiprovider/pom.xml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>dev.openfeature.contrib</groupId>
7+
<artifactId>parent</artifactId>
8+
<version>0.1.0</version>
9+
<relativePath>../../pom.xml</relativePath>
10+
</parent>
11+
<groupId>dev.openfeature.contrib.providers</groupId>
12+
<artifactId>multiprovider</artifactId>
13+
<version>0.0.1</version> <!--x-release-please-version -->
14+
15+
<name>multiprovider</name>
16+
<description>OpenFeature Multi-Provider</description>
17+
<url>https://github.com/open-feature/java-sdk-contrib/tree/main/providers/multiprovider</url>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.json</groupId>
22+
<artifactId>json</artifactId>
23+
<version>20240303</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>org.apache.logging.log4j</groupId>
27+
<artifactId>log4j-slf4j2-impl</artifactId>
28+
<version>2.24.1</version>
29+
<scope>test</scope>
30+
</dependency>
31+
</dependencies>
32+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package dev.openfeature.contrib.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.FlagNotFoundError;
7+
import lombok.NoArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
import java.util.Map;
11+
import java.util.function.Function;
12+
13+
import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND;
14+
15+
/**
16+
* First match strategy.
17+
* Return the first result returned by a provider. Skip providers that indicate they had no value due to
18+
* FLAG_NOT_FOUND.
19+
* In all other cases, use the value returned by the provider.
20+
* If any provider returns an error result other than FLAG_NOT_FOUND, the whole evaluation should error and
21+
* “bubble up” the individual provider’s error in the result.
22+
* As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call
23+
* the rest of the providers.
24+
*/
25+
@Slf4j
26+
@NoArgsConstructor
27+
public class FirstMatchStrategy implements Strategy {
28+
29+
/**
30+
* Represents a strategy that evaluates providers based on a first-match approach.
31+
* Provides a method to evaluate providers using a specified function and return the evaluation result.
32+
*
33+
* @param providerFunction provider function
34+
* @param <T> ProviderEvaluation type
35+
* @return the provider evaluation
36+
*/
37+
@Override
38+
public <T> ProviderEvaluation<T> evaluate(Map<String, FeatureProvider> providers, String key, T defaultValue,
39+
EvaluationContext ctx, Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
40+
for (FeatureProvider provider: providers.values()) {
41+
try {
42+
ProviderEvaluation<T> res = providerFunction.apply(provider);
43+
if (!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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dev.openfeature.contrib.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 lombok.NoArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
import java.util.Map;
11+
import java.util.function.Function;
12+
13+
/**
14+
* First Successful Strategy.
15+
* Similar to “First Match”, except that errors from evaluated providers do not halt execution.
16+
* Instead, it will return the first successful result from a provider.
17+
* If no provider successfully responds, it will throw an error result.
18+
*/
19+
@Slf4j
20+
@NoArgsConstructor
21+
public class FirstSuccessfulStrategy implements Strategy {
22+
23+
@Override
24+
public <T> ProviderEvaluation<T> evaluate(Map<String, FeatureProvider> providers, String key, T defaultValue,
25+
EvaluationContext ctx, Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
26+
for (FeatureProvider provider: providers.values()) {
27+
try {
28+
ProviderEvaluation<T> res = providerFunction.apply(provider);
29+
if (res.getErrorCode() == null) {
30+
return res;
31+
}
32+
} catch (Exception e) {
33+
log.debug("evaluation exception {}", e.getMessage());
34+
}
35+
}
36+
37+
throw new GeneralError("evaluation error");
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package dev.openfeature.contrib.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 lombok.Getter;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.json.JSONObject;
13+
14+
import java.util.ArrayList;
15+
import java.util.Collection;
16+
import java.util.Collections;
17+
import java.util.LinkedHashMap;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.concurrent.Callable;
21+
import java.util.concurrent.ExecutorService;
22+
import java.util.concurrent.Executors;
23+
import java.util.concurrent.Future;
24+
25+
/**
26+
* Provider implementation for Multi-provider.
27+
*/
28+
@Slf4j
29+
public class MultiProvider extends EventProvider {
30+
31+
@Getter
32+
private static final String NAME = "multiprovider";
33+
public static final int INIT_THREADS_COUNT = 8;
34+
private final Map<String, FeatureProvider> providers;
35+
private final Strategy strategy;
36+
private String metadataName;
37+
38+
/**
39+
* Constructs a MultiProvider with the given list of FeatureProviders, using a default strategy.
40+
*
41+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
42+
*/
43+
public MultiProvider(List<FeatureProvider> providers) {
44+
this(providers, null);
45+
}
46+
47+
/**
48+
* Constructs a MultiProvider with the given list of FeatureProviders and a strategy.
49+
*
50+
* @param providers the list of FeatureProviders to initialize the MultiProvider with
51+
* @param strategy the strategy
52+
*/
53+
public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
54+
this.providers = buildProviders(providers);
55+
if (strategy != null) {
56+
this.strategy = strategy;
57+
} else {
58+
this.strategy = new FirstMatchStrategy();
59+
}
60+
}
61+
62+
protected static Map<String, FeatureProvider> buildProviders(List<FeatureProvider> providers) {
63+
Map<String, FeatureProvider> providersMap = new LinkedHashMap<>(providers.size());
64+
for (FeatureProvider provider: providers) {
65+
FeatureProvider prevProvider = providersMap.put(provider.getMetadata().getName(), provider);
66+
if (prevProvider != null) {
67+
log.warn("duplicated provider name: {}", provider.getMetadata().getName());
68+
}
69+
}
70+
return Collections.unmodifiableMap(providersMap);
71+
}
72+
73+
/**
74+
* Initialize the provider.
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(providers, key, defaultValue, ctx,
112+
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,
118+
p -> p.getStringEvaluation(key, defaultValue, ctx));
119+
}
120+
121+
@Override
122+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
123+
return strategy.evaluate(providers, key, defaultValue, ctx,
124+
p -> p.getIntegerEvaluation(key, defaultValue, ctx));
125+
}
126+
127+
@Override
128+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
129+
return strategy.evaluate(providers, key, defaultValue, ctx,
130+
p -> p.getDoubleEvaluation(key, defaultValue, ctx));
131+
}
132+
133+
@Override
134+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
135+
return strategy.evaluate(providers, key, defaultValue, ctx,
136+
p -> p.getObjectEvaluation(key, defaultValue, ctx));
137+
}
138+
139+
@Override
140+
public void shutdown() {
141+
log.debug("shutdown begin");
142+
for (FeatureProvider provider: providers.values()) {
143+
try {
144+
provider.shutdown();
145+
} catch (Exception e) {
146+
log.error("error shutdown provider {}", provider.getMetadata().getName(), e);
147+
}
148+
}
149+
log.debug("shutdown end");
150+
}
151+
152+
}

0 commit comments

Comments
 (0)