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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.NoArgsConstructor;
Expand All @@ -20,7 +22,8 @@
* <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>
* <li>If all providers report {@code FLAG_NOT_FOUND}, return a {@code FLAG_NOT_FOUND} error
* with per-provider error details.</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.
Expand All @@ -36,7 +39,11 @@ public <T> ProviderEvaluation<T> evaluate(
T defaultValue,
EvaluationContext ctx,
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
for (FeatureProvider provider : providers.values()) {
List<ProviderError> collectedErrors = new ArrayList<>();

for (Map.Entry<String, FeatureProvider> entry : providers.entrySet()) {
String providerName = entry.getKey();
FeatureProvider provider = entry.getValue();
try {
ProviderEvaluation<T> res = providerFunction.apply(provider);
ErrorCode errorCode = res.getErrorCode();
Expand All @@ -45,19 +52,22 @@ public <T> ProviderEvaluation<T> evaluate(
return res;
}
if (!FLAG_NOT_FOUND.equals(errorCode)) {
// Any non-FLAG_NOT_FOUND error bubbles up
// Any non-FLAG_NOT_FOUND error bubbles up immediately
return res;
}
// else FLAG_NOT_FOUND: skip to next provider
} catch (FlagNotFoundError ignored) {
// do not log in hot path, just skip
// FLAG_NOT_FOUND: record and skip to next provider
collectedErrors.add(ProviderError.fromResult(providerName, FLAG_NOT_FOUND, res.getErrorMessage()));
} catch (FlagNotFoundError e) {
// Treat thrown FlagNotFoundError like a FLAG_NOT_FOUND result
collectedErrors.add(ProviderError.fromException(providerName, e));
}
}

// All providers either threw or returned FLAG_NOT_FOUND
return ProviderEvaluation.<T>builder()
.errorMessage("Flag not found in any provider")
return MultiProviderEvaluation.<T>multiProviderBuilder()
.errorMessage(ProviderError.buildAggregateMessage("Flag not found in any provider", collectedErrors))
.errorCode(FLAG_NOT_FOUND)
.providerErrors(collectedErrors)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.ProviderEvaluation;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.NoArgsConstructor;
Expand All @@ -12,9 +14,10 @@
/**
* First Successful Strategy.
*
* <p>Similar to First Match, except that errors from evaluated providers do not halt execution.
* <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.
* responds, it returns a {@code GENERAL} error result that includes per-provider error details
* describing why each provider failed.
*/
@Slf4j
@NoArgsConstructor
Expand All @@ -27,22 +30,30 @@ public <T> ProviderEvaluation<T> evaluate(
T defaultValue,
EvaluationContext ctx,
Function<FeatureProvider, ProviderEvaluation<T>> providerFunction) {
for (FeatureProvider provider : providers.values()) {
List<ProviderError> collectedErrors = new ArrayList<>();

for (Map.Entry<String, FeatureProvider> entry : providers.entrySet()) {
String providerName = entry.getKey();
FeatureProvider provider = entry.getValue();
try {
ProviderEvaluation<T> res = providerFunction.apply(provider);
if (res.getErrorCode() == null) {
// First successful result (no error code)
return res;
}
} catch (Exception ignored) {
// swallow and continue; errors from individual providers
// are not fatal for this strategy
// Record error-coded result
collectedErrors.add(ProviderError.fromResult(providerName, res.getErrorCode(), res.getErrorMessage()));
} catch (Exception e) {
// Record thrown exception
collectedErrors.add(ProviderError.fromException(providerName, e));
}
}

return ProviderEvaluation.<T>builder()
.errorMessage("No provider successfully responded")
return MultiProviderEvaluation.<T>multiProviderBuilder()
.errorMessage(
ProviderError.buildAggregateMessage("No provider successfully responded", collectedErrors))
.errorCode(ErrorCode.GENERAL)
.providerErrors(collectedErrors)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package dev.openfeature.sdk.multiprovider;

import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.ProviderEvaluation;
import java.util.Collections;
import java.util.List;

/**
* A {@link ProviderEvaluation} subtype returned by multi-provider strategies that carries
* per-provider error details.
*
* <p>This type can represent both successful and failed evaluations. When a strategy exhausts
* all providers without a successful result, the per-provider errors describe why each provider
* failed. Custom strategies may also use this type for successful results to surface information
* about providers that were skipped or failed before the successful one.
*
* <p>Usage:
* <pre>{@code
* ProviderEvaluation<String> result = strategy.evaluate(...);
* if (result instanceof MultiProviderEvaluation<String> multiResult) {
* for (ProviderError error : multiResult.getProviderErrors()) {
* log.warn("Provider {} failed: {} - {}",
* error.getProviderName(), error.getErrorCode(), error.getErrorMessage());
* }
* }
* }</pre>
*
* @param <T> the type of the flag being evaluated
*/
public class MultiProviderEvaluation<T> extends ProviderEvaluation<T> {

private final List<ProviderError> providerErrors;

private MultiProviderEvaluation(
T value,
String variant,
String reason,
ErrorCode errorCode,
String errorMessage,
ImmutableMetadata flagMetadata,
List<ProviderError> providerErrors) {
super(value, variant, reason, errorCode, errorMessage, flagMetadata);
this.providerErrors =
providerErrors != null ? Collections.unmodifiableList(providerErrors) : Collections.emptyList();
}

/**
* Returns the per-provider error details.
*
* <p>Each entry describes why a specific provider failed during multi-provider evaluation.
*
* @return an unmodifiable list of per-provider errors, never {@code null}
*/
public List<ProviderError> getProviderErrors() {
return providerErrors;
}

/**
* Create a new builder for {@link MultiProviderEvaluation}.
*
* @param <T> the flag value type
* @return a new builder
*/
public static <T> Builder<T> multiProviderBuilder() {
return new Builder<>();
}

/**
* Builder for {@link MultiProviderEvaluation}.
*
* @param <T> the flag value type
*/
public static class Builder<T> {
private T value;
private String variant;
private String reason;
private ErrorCode errorCode;
private String errorMessage;
private ImmutableMetadata flagMetadata;
private List<ProviderError> providerErrors;

public Builder<T> value(T value) {
this.value = value;
return this;
}

public Builder<T> variant(String variant) {
this.variant = variant;
return this;
}

public Builder<T> reason(String reason) {
this.reason = reason;
return this;
}

public Builder<T> errorCode(ErrorCode errorCode) {
this.errorCode = errorCode;
return this;
}

public Builder<T> errorMessage(String errorMessage) {
this.errorMessage = errorMessage;
return this;
}

public Builder<T> flagMetadata(ImmutableMetadata flagMetadata) {
this.flagMetadata = flagMetadata;
return this;
}

public Builder<T> providerErrors(List<ProviderError> providerErrors) {
this.providerErrors = providerErrors;
return this;
}

public MultiProviderEvaluation<T> build() {
return new MultiProviderEvaluation<>(
value, variant, reason, errorCode, errorMessage, flagMetadata, providerErrors);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dev.openfeature.sdk.multiprovider;

import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import java.util.List;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

/**
* Represents an error from a single provider during multi-provider evaluation.
*
* <p>Captures the provider name, error code, error message, and optionally the original exception
* that occurred during flag evaluation. This allows callers to inspect per-provider error details
* when a multi-provider strategy exhausts all providers without a successful result.
*/
@Data
@Builder
@AllArgsConstructor
public class ProviderError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error code will always be not found, right? So should we maybe convert this to a NotFound class, or maybe even just save the names of the providers that did not find the flag?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProviderError is used across the strategies and we need the full error codes in the other strategies,
if you believe it is a better approach to simplify FirstMatchStrategy to just get the name of the providers I can implement that.

private String providerName;
private ErrorCode errorCode;
private String errorMessage;
private Exception exception;

/**
* Create a ProviderError from an error-coded {@code ProviderEvaluation} result.
*
* @param providerName the name of the provider that returned the error
* @param errorCode the error code from the evaluation result
* @param errorMessage the error message from the evaluation result (may be {@code null})
* @return a new ProviderError
*/
public static ProviderError fromResult(String providerName, ErrorCode errorCode, String errorMessage) {
return new ProviderError(providerName, errorCode, errorMessage, null);
}

/**
* Create a ProviderError from a thrown exception.
*
* @param providerName the name of the provider that threw the exception
* @param exception the exception that was thrown
* @return a new ProviderError
*/
public static ProviderError fromException(String providerName, Exception exception) {
ErrorCode code = ErrorCode.GENERAL;
if (exception instanceof OpenFeatureError) {
code = ((OpenFeatureError) exception).getErrorCode();
}
return new ProviderError(providerName, code, exception.getMessage(), exception);
}

/**
* Build an aggregate error message from a list of provider errors.
*
* @param baseMessage the base message to use (e.g. "No provider successfully responded")
* @param errors the list of per-provider errors
* @return an aggregate message including per-provider details
*/
public static String buildAggregateMessage(String baseMessage, List<ProviderError> errors) {
String details = errors.stream().map(ProviderError::toString).collect(Collectors.joining(", "));
return baseMessage + ". Provider errors: [" + details + "]";
}

@Override
public String toString() {
return providerName + ": " + errorCode + " (" + (errorMessage != null ? errorMessage : "unknown") + ")";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
* <li>Order or select providers</li>
* <li>Handle {@code FLAG_NOT_FOUND} results</li>
* <li>Handle errors and exceptions from providers</li>
* <li>Collect per-provider error details when no provider returns a successful result.
* Implementations should return a {@link MultiProviderEvaluation} populated with
* a {@link ProviderError} for each failed provider, so that callers can inspect individual
* failure reasons.</li>
* </ul>
*/
public interface Strategy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,17 @@ protected void setupProviderSuccess(FeatureProvider provider, String value) {
when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null))
.thenReturn(result);
}

protected void setupProviderErrorWithMessage(FeatureProvider provider, ErrorCode errorCode, String errorMessage) {
ProviderEvaluation<String> result = mock(ProviderEvaluation.class);
when(result.getErrorCode()).thenReturn(errorCode);
when(result.getErrorMessage()).thenReturn(errorMessage);
when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null))
.thenReturn(result);
}

protected void setupProviderException(FeatureProvider provider, RuntimeException exception) {
when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null))
.thenThrow(exception);
}
}
Loading
Loading