-
Notifications
You must be signed in to change notification settings - Fork 52
Provider hook evaluation order #1883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| 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.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.concurrent.Callable; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.Future; | ||
| import java.util.function.BiConsumer; | ||
| import java.util.function.Function; | ||
| import lombok.Getter; | ||
|
|
||
| /** | ||
| * Comparison strategy. | ||
| * | ||
| * <p>Evaluates all providers and compares successful results. | ||
| */ | ||
| public class ComparisonStrategy implements Strategy { | ||
|
|
||
| @Getter | ||
| private final String fallbackProvider; | ||
|
|
||
| private final BiConsumer<String, Map<String, ProviderEvaluation<?>>> onMismatch; | ||
|
|
||
| /** | ||
| * Constructs a comparison strategy with a fallback provider. | ||
| * | ||
| * @param fallbackProvider provider name to use as fallback when successful providers disagree | ||
| */ | ||
| public ComparisonStrategy(String fallbackProvider) { | ||
| this(fallbackProvider, null); | ||
| } | ||
|
|
||
| /** | ||
| * Constructs a comparison strategy with fallback provider and mismatch callback. | ||
| * | ||
| * @param fallbackProvider provider name to use as fallback when successful providers disagree | ||
| * @param onMismatch callback invoked with all successful evaluations when they disagree | ||
| */ | ||
| public ComparisonStrategy( | ||
| String fallbackProvider, | ||
| BiConsumer<String, Map<String, ProviderEvaluation<?>>> onMismatch) { | ||
| this.fallbackProvider = Objects.requireNonNull(fallbackProvider, "fallbackProvider must not be null"); | ||
| this.onMismatch = onMismatch; | ||
| } | ||
|
|
||
| @Override | ||
| public <T> ProviderEvaluation<T> evaluate( | ||
|
Check failure on line 57 in src/main/java/dev/openfeature/sdk/multiprovider/ComparisonStrategy.java
|
||
| Map<String, FeatureProvider> providers, | ||
| String key, | ||
| T defaultValue, | ||
| EvaluationContext ctx, | ||
| Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) { | ||
| if (providers.isEmpty()) { | ||
| return ProviderEvaluation.<T>builder() | ||
| .errorCode(ErrorCode.GENERAL) | ||
| .errorMessage("No providers configured") | ||
| .build(); | ||
| } | ||
| if (!providers.containsKey(fallbackProvider)) { | ||
| throw new IllegalArgumentException("fallbackProvider not found in providers: " + fallbackProvider); | ||
| } | ||
|
|
||
| Map<String, ProviderEvaluation<T>> successfulResults = new ConcurrentHashMap<>(providers.size()); | ||
| Map<String, String> providerErrors = new ConcurrentHashMap<>(providers.size()); | ||
| ExecutorService executorService = Executors.newFixedThreadPool(providers.size()); | ||
| try { | ||
| List<Callable<Void>> tasks = new ArrayList<>(providers.size()); | ||
| for (Map.Entry<String, FeatureProvider> entry : providers.entrySet()) { | ||
| String providerName = entry.getKey(); | ||
| FeatureProvider provider = entry.getValue(); | ||
| tasks.add(() -> { | ||
| try { | ||
| ProviderEvaluation<T> evaluation = providerFunction.apply(provider); | ||
| if (evaluation == null) { | ||
| providerErrors.put(providerName, "null evaluation"); | ||
| } else if (evaluation.getErrorCode() == null) { | ||
| successfulResults.put(providerName, evaluation); | ||
| } else { | ||
| providerErrors.put( | ||
| providerName, | ||
| evaluation.getErrorCode() + ": " + String.valueOf(evaluation.getErrorMessage())); | ||
|
Check warning on line 91 in src/main/java/dev/openfeature/sdk/multiprovider/ComparisonStrategy.java
|
||
| } | ||
| } catch (Exception e) { | ||
| providerErrors.put(providerName, e.getClass().getSimpleName() + ": " + e.getMessage()); | ||
| } | ||
| return null; | ||
| }); | ||
| } | ||
| List<Future<Void>> futures = executorService.invokeAll(tasks); | ||
| for (Future<Void> future : futures) { | ||
| future.get(); | ||
| } | ||
| } catch (Exception e) { | ||
|
Check warning on line 103 in src/main/java/dev/openfeature/sdk/multiprovider/ComparisonStrategy.java
|
||
| return ProviderEvaluation.<T>builder() | ||
| .errorCode(ErrorCode.GENERAL) | ||
| .errorMessage("Comparison strategy failed: " + e.getMessage()) | ||
| .build(); | ||
| } finally { | ||
| executorService.shutdown(); | ||
| } | ||
|
|
||
| if (!providerErrors.isEmpty()) { | ||
| return ProviderEvaluation.<T>builder() | ||
| .errorCode(ErrorCode.GENERAL) | ||
| .errorMessage("Provider errors: " + buildErrorSummary(providerErrors)) | ||
| .build(); | ||
| } | ||
|
|
||
| ProviderEvaluation<T> fallbackResult = successfulResults.get(fallbackProvider); | ||
| if (fallbackResult == null) { | ||
| return ProviderEvaluation.<T>builder() | ||
| .errorCode(ErrorCode.GENERAL) | ||
| .errorMessage("Fallback provider did not return a successful evaluation: " + fallbackProvider) | ||
| .build(); | ||
| } | ||
|
|
||
| if (allEvaluationsMatch(successfulResults)) { | ||
| return fallbackResult; | ||
| } | ||
|
|
||
| if (onMismatch != null) { | ||
| Map<String, ProviderEvaluation<?>> mismatchPayload = new LinkedHashMap<>(successfulResults); | ||
| onMismatch.accept(key, Collections.unmodifiableMap(mismatchPayload)); | ||
| } | ||
| return fallbackResult; | ||
| } | ||
|
|
||
| private String buildErrorSummary(Map<String, String> providerErrors) { | ||
| StringBuilder builder = new StringBuilder(); | ||
| boolean first = true; | ||
| for (Map.Entry<String, String> entry : providerErrors.entrySet()) { | ||
| if (!first) { | ||
| builder.append("; "); | ||
| } | ||
| first = false; | ||
| builder.append(entry.getKey()).append(" -> ").append(entry.getValue()); | ||
| } | ||
| return builder.toString(); | ||
| } | ||
|
|
||
| private <T> boolean allEvaluationsMatch(Map<String, ProviderEvaluation<T>> results) { | ||
| ProviderEvaluation<T> baseline = null; | ||
| for (ProviderEvaluation<T> evaluation : results.values()) { | ||
| if (baseline == null) { | ||
| baseline = evaluation; | ||
| continue; | ||
| } | ||
| if (!Objects.equals(baseline.getValue(), evaluation.getValue())) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
evaluatemethod creates a newFixedThreadPoolfor every flag evaluation, which is inefficient and can lead to a Denial of Service (DoS) vulnerability. High traffic could exhaust system resources, causing anOutOfMemoryErrorand a DoS for the application. It is highly recommended to use a shared, managedExecutorServiceinstead of creating a new one for every evaluation to prevent performance issues and resource exhaustion.