Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.
| ------ |---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
| ✅ | [Multi-provider (experimental)](#multi-provider-experimental) | Combine multiple providers and delegate evaluations according to a strategy. |
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| ✅ | [Tracking](#tracking) | Associate user actions with feature flag evaluations. |
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
Expand All @@ -147,7 +148,40 @@ Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D
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.

Once you've added a provider as a dependency, it can be registered with OpenFeature like this:


In some situations, it may be beneficial to register multiple providers in the same application.
This is possible using [domains](#domains), which is covered in more detail below.

#### Multi-provider (experimental)

In addition to domains, you may want to delegate flag evaluation across multiple providers using a configurable strategy.
The multi-provider allows you to compose several `FeatureProvider` implementations and determine which provider's result to use.

> **Experimental:** This API is experimental and may change in future releases.

```java
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.multiprovider.MultiProvider;

import java.util.List;

public void multiProviderExample() throws Exception {
FeatureProvider primaryProvider = new MyPrimaryProvider();
FeatureProvider fallbackProvider = new MyFallbackProvider();

MultiProvider multiProvider = new MultiProvider(List.of(primaryProvider, fallbackProvider));

OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProviderAndWait(multiProvider);

Client client = api.getClient();
boolean value = client.getBooleanValue("some-flag", false);
}
```


#### Synchronous

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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package dev.openfeature.sdk.multiprovider;

import static dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND;

import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import java.util.Map;
import java.util.function.Function;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* First match strategy.
*
* <p>Return the first result returned by a provider.
* <ul>
* <li>Skip providers that indicate they had no value due to {@code FLAG_NOT_FOUND}.</li>
* <li>On any other error code, return that error result.</li>
* <li>If a provider throws {@link FlagNotFoundError}, it is treated like {@code FLAG_NOT_FOUND}.</li>
* <li>If all providers report {@code FLAG_NOT_FOUND}, return a {@code FLAG_NOT_FOUND} error.</li>
* </ul>
* As soon as a non-{@code FLAG_NOT_FOUND} result is returned by a provider (success or other error),
* the rest of the operation short-circuits and does not call the remaining providers.
*/
@Slf4j
@NoArgsConstructor
public class FirstMatchStrategy implements Strategy {

@Override
public <T> ProviderEvaluation<T> evaluate(
Map<String, FeatureProvider> providers,
String key,
T defaultValue,
EvaluationContext ctx,
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
for (FeatureProvider provider : providers.values()) {
try {
ProviderEvaluation<T> res = providerFunction.apply(provider);
ErrorCode errorCode = res.getErrorCode();
if (errorCode == null) {
// Successful evaluation
return res;
}
if (!FLAG_NOT_FOUND.equals(errorCode)) {
// Any non-FLAG_NOT_FOUND error bubbles up
return res;
}
// else FLAG_NOT_FOUND: skip to next provider
} catch (FlagNotFoundError e) {
log.debug(
"flag not found {} in provider {}",
key,
provider.getMetadata().getName(),
e);
}
}

// All providers either threw or returned FLAG_NOT_FOUND
return ProviderEvaluation.<T>builder()
.errorMessage("Flag not found in any provider")
.errorCode(FLAG_NOT_FOUND)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package dev.openfeature.sdk.multiprovider;

import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.ProviderEvaluation;
import java.util.Map;
import java.util.function.Function;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* First Successful Strategy.
*
* <p>Similar to “First Match”, except that errors from evaluated providers do not halt execution.
* Instead, it returns the first successful result from a provider. If no provider successfully
* responds, it returns a {@code GENERAL} error result.
*/
@Slf4j
@NoArgsConstructor
public class FirstSuccessfulStrategy implements Strategy {

@Override
public <T> ProviderEvaluation<T> evaluate(
Map<String, FeatureProvider> providers,
String key,
T defaultValue,
EvaluationContext ctx,
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
for (FeatureProvider provider : providers.values()) {
try {
ProviderEvaluation<T> res = providerFunction.apply(provider);
if (res.getErrorCode() == null) {
// First successful result (no error code)
return res;
}
} catch (Exception e) {
log.debug(
"evaluation exception for key {} in provider {}",
key,
provider.getMetadata().getName(),
e);
}
}

return ProviderEvaluation.<T>builder()
.errorMessage("No provider successfully responded")
.errorCode(ErrorCode.GENERAL)
.build();
}
}
186 changes: 186 additions & 0 deletions src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package dev.openfeature.sdk.multiprovider;

import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.EventProvider;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Value;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

/**
* <b>Experimental:</b> Provider implementation for multi-provider.
*
* <p>This provider delegates flag evaluations to multiple underlying providers using a configurable
* {@link Strategy}. It also exposes combined metadata containing the original metadata of each
* underlying provider.
*/
@Slf4j
public class MultiProvider extends EventProvider {

@Getter
private static final String NAME = "multiprovider";

public static final int INIT_THREADS_COUNT = 8;

private final Map<String, FeatureProvider> providers;
private final Strategy strategy;
private MultiProviderMetadata metadata;

/**
* Constructs a MultiProvider with the given list of FeatureProviders, by default uses
* {@link FirstMatchStrategy}.
*
* @param providers the list of FeatureProviders to initialize the MultiProvider with
*/
public MultiProvider(List<FeatureProvider> providers) {
this(providers, null);
}

/**
* Constructs a MultiProvider with the given list of FeatureProviders and a strategy.
*
* @param providers the list of FeatureProviders to initialize the MultiProvider with
* @param strategy the strategy (if {@code null}, {@link FirstMatchStrategy} is used)
*/
public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
this.providers = buildProviders(providers);
if (strategy != null) {
this.strategy = strategy;
} else {
this.strategy = new FirstMatchStrategy();
}
}

protected static Map<String, FeatureProvider> buildProviders(List<FeatureProvider> providers) {
Map<String, FeatureProvider> providersMap = new LinkedHashMap<>(providers.size());
for (FeatureProvider provider : providers) {
FeatureProvider prevProvider =
providersMap.put(provider.getMetadata().getName(), provider);
if (prevProvider != null) {
log.warn("duplicated provider name: {}", provider.getMetadata().getName());
}
}
return Collections.unmodifiableMap(providersMap);
}

/**
* Initialize the provider.
*
* @param evaluationContext evaluation context
* @throws Exception on error (e.g. wrapped {@link java.util.concurrent.ExecutionException}
* from a failing provider)
*/
@Override
public void initialize(EvaluationContext evaluationContext) throws Exception {
var metadataBuilder = MultiProviderMetadata.builder().name(NAME);
HashMap<String, Metadata> providersMetadata = new HashMap<>();

if (providers.isEmpty()) {
metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));
metadata = metadataBuilder.build();
return;
}

ExecutorService executorService = Executors.newFixedThreadPool(Math.min(INIT_THREADS_COUNT, providers.size()));
try {
Collection<Callable<Void>> tasks = new ArrayList<>(providers.size());
for (FeatureProvider provider : providers.values()) {
tasks.add(() -> {
provider.initialize(evaluationContext);
return null;
});
Metadata providerMetadata = provider.getMetadata();
providersMetadata.put(providerMetadata.getName(), providerMetadata);
}

metadataBuilder.originalMetadata(Collections.unmodifiableMap(providersMetadata));

List<Future<Void>> results = executorService.invokeAll(tasks);
for (Future<Void> result : results) {
// This will re-throw any exception from the provider's initialize method,
// wrapped in an ExecutionException.
result.get();
}
} catch (Exception e) {
// If initialization fails for any provider, attempt to shut down all providers
// to avoid a partial/limbo state.
for (FeatureProvider provider : providers.values()) {
try {
provider.shutdown();
} catch (Exception shutdownEx) {
log.error(
"error shutting down provider {} after failed initialize",
provider.getMetadata().getName(),
shutdownEx);
}
}
throw e;
} finally {
executorService.shutdown();
}

metadata = metadataBuilder.build();
}

@SuppressFBWarnings(value = "EI_EXPOSE_REP")
@Override
public Metadata getMetadata() {
return metadata;
}

@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
return strategy.evaluate(
providers, key, defaultValue, ctx, p -> p.getBooleanEvaluation(key, defaultValue, ctx));
}

@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getStringEvaluation(key, defaultValue, ctx));
}

@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
return strategy.evaluate(
providers, key, defaultValue, ctx, p -> p.getIntegerEvaluation(key, defaultValue, ctx));
}

@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getDoubleEvaluation(key, defaultValue, ctx));
}

@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getObjectEvaluation(key, defaultValue, ctx));
}

@Override
public void shutdown() {
log.debug("shutdown begin");
for (FeatureProvider provider : providers.values()) {
try {
provider.shutdown();
} catch (Exception e) {
log.error("error shutdown provider {}", provider.getMetadata().getName(), e);
}
}
log.debug("shutdown end");
// Important: ensure EventProvider's executor is also shut down
super.shutdown();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dev.openfeature.sdk.multiprovider;

import dev.openfeature.sdk.Metadata;
import java.util.Map;
import lombok.Builder;
import lombok.Value;

/**
* Metadata for {@link MultiProvider}.
*
* <p>Contains the multiprovider's own name and a map of the original metadata from each underlying
* provider.
*/
@Value
@Builder
public class MultiProviderMetadata implements Metadata {

String name;
Map<String, Metadata> originalMetadata;
}
Loading
Loading