From f65384cc11f6cdff1b9ec2fc1046658a631f4639 Mon Sep 17 00:00:00 2001 From: "flipt-release-bot[bot]" <119936948+flipt-release-bot[bot]@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:57:50 +0000 Subject: [PATCH] refactor(java): resilient to network/http errors Signed-off-by: Roman Dmytrenko --- .../main/java/io/flipt/api/FliptClient.java | 14 +++ .../io/flipt/api/evaluation/Evaluation.java | 95 +++++++++++++++++-- flipt-java/src/test/java/TestFliptClient.java | 35 ++++--- 3 files changed, 123 insertions(+), 21 deletions(-) diff --git a/flipt-java/src/main/java/io/flipt/api/FliptClient.java b/flipt-java/src/main/java/io/flipt/api/FliptClient.java index 204c252..a684f5e 100644 --- a/flipt-java/src/main/java/io/flipt/api/FliptClient.java +++ b/flipt-java/src/main/java/io/flipt/api/FliptClient.java @@ -2,9 +2,11 @@ import io.flipt.api.authentication.AuthenticationStrategy; import io.flipt.api.evaluation.Evaluation; +import io.flipt.api.evaluation.EvaluationException; import java.time.Duration; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import okhttp3.OkHttpClient; public class FliptClient { @@ -18,6 +20,7 @@ private FliptClient(FliptClientBuilder builder) { .baseURL(builder.baseURL) .authenticationStrategy(builder.authenticationStrategy) .headers(builder.headers) + .setUnhandledExceptionProcessor(builder.unhandledExceptionProcessor) .build(); } @@ -31,10 +34,15 @@ public static FliptClientBuilder builder() { public static final class FliptClientBuilder { + private static final Consumer nullProcessor = + new Consumer() { + public void accept(EvaluationException e) {} + }; private String baseURL = "http://localhost:8080"; private AuthenticationStrategy authenticationStrategy; private Map headers = new HashMap<>(); private Duration timeout = Duration.ofSeconds(60); + private Consumer unhandledExceptionProcessor = nullProcessor; private FliptClientBuilder() {} @@ -63,6 +71,12 @@ public FliptClientBuilder timeout(int timeout) { return this; } + public FliptClientBuilder setUnhandledExceptionProcessor( + Consumer processor) { + this.unhandledExceptionProcessor = processor; + return this; + } + public FliptClient build() { return new FliptClient(this); } diff --git a/flipt-java/src/main/java/io/flipt/api/evaluation/Evaluation.java b/flipt-java/src/main/java/io/flipt/api/evaluation/Evaluation.java index 35d5671..8757b24 100644 --- a/flipt-java/src/main/java/io/flipt/api/evaluation/Evaluation.java +++ b/flipt-java/src/main/java/io/flipt/api/evaluation/Evaluation.java @@ -13,6 +13,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.Map; +import java.util.function.Consumer; import okhttp3.*; public class Evaluation { @@ -21,17 +22,18 @@ public class Evaluation { private final AuthenticationStrategy authenticationStrategy; private final Map headers; private final ObjectMapper objectMapper; + private final Consumer unhandledExceptionProcessor; private Evaluation(EvaluationBuilder builder) { this.httpClient = builder.httpClient; this.baseURL = builder.baseURL; this.authenticationStrategy = builder.authenticationStrategy; this.headers = builder.headers; - this.objectMapper = - JsonMapper.builder() - .addModule(new Jdk8Module()) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .build(); + this.objectMapper = JsonMapper.builder() + .addModule(new Jdk8Module()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + this.unhandledExceptionProcessor = builder.unhandledExceptionProcessor; } public static EvaluationBuilder builder() { @@ -43,8 +45,10 @@ public static class EvaluationBuilder { private String baseURL; private AuthenticationStrategy authenticationStrategy; private Map headers; + private Consumer unhandledExceptionProcessor; - private EvaluationBuilder() {} + private EvaluationBuilder() { + } public EvaluationBuilder httpClient(OkHttpClient httpClient) { this.httpClient = httpClient; @@ -66,21 +70,95 @@ public EvaluationBuilder headers(Map headers) { return this; } + public EvaluationBuilder setUnhandledExceptionProcessor( + Consumer processor) { + this.unhandledExceptionProcessor = processor; + return this; + } + public Evaluation build() { return new Evaluation(this); } } + /** + * Evaluates a variant flag based on the {@link EvaluationRequest}. + * + * @param request the {@link EvaluationRequest} + * @return a {@link VariantEvaluationResponse} containing the evaluation result + * @throws EvaluationException if an error occurs during the process, such as a + * network issue or invalid request + */ public VariantEvaluationResponse evaluateVariant(EvaluationRequest request) throws EvaluationException { return this.makeRequest(request, "/evaluate/v1/variant", VariantEvaluationResponse.class); } + /** + * Evaluates a variant flag based on the {@link EvaluationRequest}. + * If the evaluation fails due to an {@link EvaluationException}, the specified + * fallback string will be returned. In the event of an exception, the exception + * is passed to an unhandled exception processor for further handling or + * logging. + * + * @param request the {@link EvaluationRequest} + * @param fallback the string value to return in case of an evaluation error + * @return the variant key if the evaluation is successful; otherwise, returns + * the fallback value + */ + public String variantValue(EvaluationRequest request, String fallback) { + try { + return this.evaluateVariant(request).getVariantKey(); + } catch (EvaluationException e) { + this.unhandledExceptionProcessor.accept(e); + } + + return fallback; + } + + /** + * Evaluates a boolean flag based on the {@link EvaluationRequest}. + * + * @param request the {@link EvaluationRequest} + * @return a {@link BooleanEvaluationResponse} containing the evaluation result + * @throws EvaluationException if an error occurs during the process, such as a + * network issue or invalid request + */ public BooleanEvaluationResponse evaluateBoolean(EvaluationRequest request) throws EvaluationException { return this.makeRequest(request, "/evaluate/v1/boolean", BooleanEvaluationResponse.class); } + /** + * Evaluates a boolean value based on the {@link EvaluationRequest}. + * If the evaluation fails due to an {@link EvaluationException}, the specified + * fallback boolean value will be returned. In the event of an exception, the + * exception is passed to an unhandled exception processor for further handling + * or logging. + * + * @param request the {@link EvaluationRequest} + * @param fallback the boolean value to return in case of an evaluation error + * @return the value if the evaluation is successful; otherwise, returns + * the fallback value + */ + public boolean booleanValue(EvaluationRequest request, boolean fallback) { + try { + return this.evaluateBoolean(request).isEnabled(); + } catch (EvaluationException e) { + this.unhandledExceptionProcessor.accept(e); + } + return fallback; + } + + /** + * Evaluates a batch of flags based on the {@link BatchEvaluationRequest}. + * + * @param request the {@link BatchEvaluationRequest} + * @return a {@link BatchEvaluationResponse} containing the evaluation results + * @throws EvaluationException if an error occurs during the evaluation + * process, such as a network issue or invalid + * request + */ public BatchEvaluationResponse evaluateBatch(BatchEvaluationRequest request) throws EvaluationException { return this.makeRequest(request, "/evaluate/v1/batch", BatchEvaluationResponse.class); @@ -97,9 +175,8 @@ private T makeRequest(Object request, String path, Class clazz) Response response = null; try { - RequestBody body = - RequestBody.create( - this.objectMapper.writeValueAsString(request), MediaType.parse("application/json")); + RequestBody body = RequestBody.create( + this.objectMapper.writeValueAsString(request), MediaType.parse("application/json")); Request.Builder httpRequest = new Request.Builder().url(url).method("POST", body); diff --git a/flipt-java/src/test/java/TestFliptClient.java b/flipt-java/src/test/java/TestFliptClient.java index 6c861ff..8f8cf24 100644 --- a/flipt-java/src/test/java/TestFliptClient.java +++ b/flipt-java/src/test/java/TestFliptClient.java @@ -30,18 +30,25 @@ void testVariantEvaluation() { Map context = new HashMap<>(); context.put("fizz", "buzz"); + EvaluationRequest req = + new EvaluationRequest("default", "flag1", "entity", context, Optional.empty()); + VariantEvaluationResponse variant = - Assertions.assertDoesNotThrow( - () -> - fc.evaluation() - .evaluateVariant( - new EvaluationRequest( - "default", "flag1", "entity", context, Optional.empty()))); + Assertions.assertDoesNotThrow(() -> fc.evaluation().evaluateVariant(req)); Assertions.assertTrue(variant.isMatch()); Assertions.assertEquals("flag1", variant.getFlagKey()); Assertions.assertEquals("MATCH_EVALUATION_REASON", variant.getReason().toString()); Assertions.assertEquals("variant1", variant.getVariantKey()); Assertions.assertEquals("segment1", variant.getSegmentKeys().get(0)); + + Assertions.assertEquals("variant1", fc.evaluation().evaluateVariantValue(req, "fallback")); + + Assertions.assertEquals( + "fallback", + fc.evaluation() + .evaluateVariantValue( + new EvaluationRequest("default", "flag-none", "entity", context, Optional.empty()), + "fallback")); } @Test @@ -67,17 +74,21 @@ void testBooleanEvaluation() { Map context = new HashMap<>(); context.put("fizz", "buzz"); + EvaluationRequest req = + new EvaluationRequest("default", "flag_boolean", "entity", context, Optional.empty()); BooleanEvaluationResponse booleanEvaluation = - Assertions.assertDoesNotThrow( - () -> - fc.evaluation() - .evaluateBoolean( - new EvaluationRequest( - "default", "flag_boolean", "entity", context, Optional.empty()))); + Assertions.assertDoesNotThrow(() -> fc.evaluation().evaluateBoolean(req)); Assertions.assertTrue(booleanEvaluation.isEnabled()); Assertions.assertEquals("flag_boolean", booleanEvaluation.getFlagKey()); Assertions.assertEquals("MATCH_EVALUATION_REASON", booleanEvaluation.getReason().toString()); + + Assertions.assertTrue(fc.evaluation().evaluateBooleanValue(req, false)); + Assertions.assertTrue( + fc.evaluation() + .evaluateBooleanValue( + new EvaluationRequest("default", "flag-none", "entity", context, Optional.empty()), + true)); } @Test