From b9013a5ceb676ef9e7ec61026644a17e9252eba3 Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Fri, 7 Aug 2026 14:04:00 +0000 Subject: [PATCH 01/10] Update fractional logic to support hashing consistency ADR. Signed-off-by: NeaguGeorgiana23 --- .../flagd/FlagdProviderSyncResources.java | 11 +- .../providers/flagd/e2e/RunFileTest.java | 2 +- .../providers/flagd/e2e/RunInProcessTest.java | 2 +- .../flagd/e2e/steps/ContextSteps.java | 43 +++---- .../providers/flagd/e2e/steps/Utils.java | 9 +- .../flagd/e2e/steps/config/ConfigSteps.java | 4 +- .../tools/flagd/api/testkit/ContextSteps.java | 18 ++- .../flagd/api/testkit/EvaluationSteps.java | 8 +- .../flagd/api/testkit/EvaluatorUtils.java | 5 +- tools/flagd-api-testkit/test-harness | 2 +- tools/flagd-core/pom.xml | 6 + .../flagd/core/targeting/Fractional.java | 119 +++++++++++++++--- .../core/e2e/FlagdCoreEvaluatorTest.java | 2 +- .../flagd/core/targeting/FractionalTest.java | 6 +- .../flagd/core/targeting/OperatorTest.java | 4 +- .../fractional/selfContainedFractionalB.json | 2 +- .../src/test/resources/fractional/string.json | 2 +- 17 files changed, 189 insertions(+), 56 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java index 976870a97..35184df84 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java @@ -18,7 +18,6 @@ class FlagdProviderSyncResources { @Setter private volatile ProviderEvent previousEvent; - @Setter private volatile boolean isFatal; private volatile ProviderEventDetails fatalProviderEventDetails; @@ -111,12 +110,22 @@ public void waitForInitialization(long deadline) { */ public synchronized void shutdown() { isShutDown = true; + isInitialized = false; this.notifyAll(); } public synchronized void fatalError(ProviderEventDetails providerEventDetails) { isFatal = true; + isInitialized = false; fatalProviderEventDetails = providerEventDetails; this.notifyAll(); } + + public synchronized void setFatal(boolean fatal) { + isFatal = fatal; + if (fatal) { + isInitialized = false; + } + this.notifyAll(); + } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java index 5901fe08e..21dea4589 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java @@ -36,7 +36,7 @@ "events", "contextEnrichment", "fractional-v1", - "fractional-v3", + "fractional-v2", "deprecated" }) @Testcontainers diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java index 797756211..bf31c3bc0 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java @@ -28,7 +28,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") @ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") @IncludeTags("in-process") -@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v3", "deprecated"}) +@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v2", "deprecated"}) @Testcontainers public class RunInProcessTest { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java index caa66ee5c..b92c994b7 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/ContextSteps.java @@ -4,8 +4,11 @@ import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.Value; +import io.cucumber.datatable.DataTable; import io.cucumber.java.en.Given; +import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; public class ContextSteps extends AbstractSteps { @@ -14,32 +17,24 @@ public ContextSteps(State state) { super(state); } - @Given("a context containing a key {string}, with type {string} and with value {string}") + @Given("^a context containing a key \"([^\"]*)\", with type \"([^\"]*)\" and with value \"(.*)\"$") public void a_context_containing_a_key_with_type_and_with_value(String key, String type, String value) - throws ClassNotFoundException, InstantiationException { - Map map = state.context.asMap(); - Value typedValue; - switch (type) { - case "Integer": - long longVal = Long.parseLong(value); - if (longVal >= Integer.MIN_VALUE && longVal <= Integer.MAX_VALUE) { - typedValue = new Value((int) longVal); - } else { - // value exceeds int range; store as string to preserve precision - typedValue = new Value(value); - } - break; - case "Float": - typedValue = new Value(Double.parseDouble(value)); - break; - case "Boolean": - typedValue = new Value(Boolean.parseBoolean(value)); - break; - default: - typedValue = new Value(value); - break; + throws ClassNotFoundException, IOException { + Map map = new HashMap<>(state.context.asMap()); + map.put(key, Value.objectToValue(Utils.convert(value, type))); + state.context = new MutableContext(state.context.getTargetingKey(), map); + } + + @Given("a context with the following keys:") + public void a_context_with_the_following_keys(DataTable dataTable) throws ClassNotFoundException, IOException { + List> rows = dataTable.asMaps(String.class, String.class); + Map map = new HashMap<>(state.context.asMap()); + for (Map row : rows) { + String key = row.get("key"); + String type = row.get("type"); + String value = row.get("value"); + map.put(key, Value.objectToValue(Utils.convert(value, type))); } - map.put(key, typedValue); state.context = new MutableContext(state.context.getTargetingKey(), map); } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java index 626105ce4..e53a83d29 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/Utils.java @@ -15,14 +15,19 @@ public final class Utils { private Utils() {} public static Object convert(String value, String type) throws ClassNotFoundException, IOException { - if (Objects.equals(value, "null")) return null; + if ("Null".equals(type)) return null; + if (Objects.equals(value, "null") && !"String".equals(type)) return null; switch (type) { case "Boolean": return Boolean.parseBoolean(value); case "String": return value; case "Integer": - return Integer.parseInt(value); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return Long.parseLong(value); + } case "Float": return Double.parseDouble(value); case "Long": diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java index db5ee14f1..7418fe1b0 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/steps/config/ConfigSteps.java @@ -66,7 +66,7 @@ public void we_have_an_option_of_type_with_value(String option, String type, Str return; } - Object converted = Utils.convert(value, type); + Object converted = ("null".equals(value) && "String".equals(type)) ? null : Utils.convert(value, type); Method method = Arrays.stream(state.builder.getClass().getMethods()) .filter(method1 -> method1.getName().equals(mapOptionNames(option))) .findFirst() @@ -87,7 +87,7 @@ public void we_have_an_environment_variable_with_value(String varName, String va @Then("the option {string} of type {string} should have the value {string}") public void the_option_of_type_should_have_the_value(String option, String type, String value) throws Throwable { - Object convert = Utils.convert(value, type); + Object convert = ("null".equals(value) && "String".equals(type)) ? null : Utils.convert(value, type); if (IGNORED_FOR_NOW.contains(option)) { log.error("option '{}' is not supported", option); diff --git a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java index 245c0d174..b87555204 100644 --- a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java +++ b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java @@ -4,9 +4,11 @@ import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.Value; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.cucumber.datatable.DataTable; import io.cucumber.java.en.Given; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -25,13 +27,27 @@ public ContextSteps(EvaluatorState state) { } /** Adds a typed key/value pair to the evaluation context. */ - @Given("a context containing a key {string}, with type {string} and with value {string}") + @Given("^a context containing a key \"([^\"]*)\", with type \"([^\"]*)\" and with value \"(.*)\"$") public void contextKeyWithTypeAndValue(String key, String type, String value) throws IOException { Map map = new HashMap<>(state.context.asMap()); map.put(key, Value.objectToValue(EvaluatorUtils.convert(value, type))); state.context = new MutableContext(state.context.getTargetingKey(), map); } + /** Adds multiple context keys from a data table. */ + @Given("a context with the following keys:") + public void contextWithFollowingKeys(DataTable dataTable) throws IOException { + List> rows = dataTable.asMaps(String.class, String.class); + Map map = new HashMap<>(state.context.asMap()); + for (Map row : rows) { + String key = row.get("key"); + String type = row.get("type"); + String value = row.get("value"); + map.put(key, Value.objectToValue(EvaluatorUtils.convert(value, type))); + } + state.context = new MutableContext(state.context.getTargetingKey(), map); + } + /** Sets the targeting key on the evaluation context. */ @Given("a context containing a targeting key with value {string}") public void contextTargetingKey(String targetingKey) { diff --git a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluationSteps.java b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluationSteps.java index c69ba5502..b3905d8e6 100644 --- a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluationSteps.java +++ b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluationSteps.java @@ -76,6 +76,7 @@ public void flagEvaluatedWithDetails() { state.evaluation = dev.openfeature.sdk.ProviderEvaluation.builder() .errorCode(ErrorCode.TYPE_MISMATCH) .errorMessage(e.getMessage()) + .reason("ERROR") .build(); } catch (dev.openfeature.sdk.exceptions.OpenFeatureError e) { // Mirror the OpenFeature SDK client behaviour: on any provider error, return the @@ -84,6 +85,7 @@ public void flagEvaluatedWithDetails() { .value(state.defaultValue) .errorCode(e.getErrorCode()) .errorMessage(e.getMessage()) + .reason("ERROR") .build(); } } @@ -94,7 +96,11 @@ public void resolvedValueEquals(String value) throws IOException { if (state.evaluation.getErrorCode() != null) { log.warning("Evaluation error: " + state.evaluation.getErrorMessage()); } - assertThat(state.evaluation.getValue()).isEqualTo(EvaluatorUtils.convert(value, state.flagType)); + Object actualValue = state.evaluation.getValue(); + if (actualValue == null && state.evaluation.getErrorCode() != null) { + actualValue = state.defaultValue; + } + assertThat(actualValue).isEqualTo(EvaluatorUtils.convert(value, state.flagType)); } /** Asserts the evaluation reason matches the expected value. */ diff --git a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluatorUtils.java b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluatorUtils.java index 646115d19..b8ecbbfba 100644 --- a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluatorUtils.java +++ b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/EvaluatorUtils.java @@ -22,7 +22,10 @@ private EvaluatorUtils() {} * @return the converted value, or {@code null} if {@code value} is "null" or empty for Object */ public static Object convert(String value, String type) throws IOException { - if (value == null || value.equals("null")) { + if ("Null".equals(type)) { + return null; + } + if (value == null || (value.equals("null") && !"String".equals(type))) { return null; } switch (type) { diff --git a/tools/flagd-api-testkit/test-harness b/tools/flagd-api-testkit/test-harness index 7575a1dc4..82ba89ec8 160000 --- a/tools/flagd-api-testkit/test-harness +++ b/tools/flagd-api-testkit/test-harness @@ -1 +1 @@ -Subproject commit 7575a1dc45f176e57e809748a712a555e9aa5d11 +Subproject commit 82ba89ec8db498fa51368e558e4d87642d9e93c4 diff --git a/tools/flagd-core/pom.xml b/tools/flagd-core/pom.xml index 71315ae71..6709c831d 100644 --- a/tools/flagd-core/pom.xml +++ b/tools/flagd-core/pom.xml @@ -40,6 +40,12 @@ [1.0.0,2.0.0) + + com.upokecenter + cbor + 4.5.6 + + com.fasterxml.jackson.core jackson-databind diff --git a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java index b0800ed75..bccfc2075 100644 --- a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java +++ b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java @@ -1,10 +1,16 @@ package dev.openfeature.contrib.tools.flagd.core.targeting; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.upokecenter.cbor.CBORObject; import io.github.jamsesso.jsonlogic.JsonLogicException; import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; import io.github.jamsesso.jsonlogic.evaluator.expressions.PreEvaluatedArgumentsExpression; +import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -32,7 +38,7 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json final Operator.FlagProperties properties = new Operator.FlagProperties(data); - final String bucketBy; + final Object bucketBy; final List distributions; // json-logic pre-evaluation flattens a single-entry fractional @@ -42,34 +48,50 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json log.debug("Missing fallback targeting key"); return null; } - bucketBy = properties.getFlagKey() + properties.getTargetingKey(); + bucketBy = java.util.Arrays.asList(properties.getFlagKey(), properties.getTargetingKey()); distributions = List.of(arguments); - } else if (arguments.get(0) instanceof String) { - // first arg is a String, use for bucketing - bucketBy = (String) arguments.get(0); + } else if (arguments.get(0) instanceof String + || arguments.get(0) instanceof Boolean + || arguments.get(0) instanceof Number + || arguments.get(0) instanceof java.util.Map) { + // first arg is a primitive or Map, use for bucketing + bucketBy = arguments.get(0); distributions = arguments.subList(1, arguments.size()); } else { // fallback to targeting key if present if (properties.getTargetingKey() == null) { log.debug("Missing fallback targeting key"); + if (arguments.size() == 2) { + throw new dev.openfeature.sdk.exceptions.GeneralError("Missing fallback targeting key"); + } return null; } - bucketBy = properties.getFlagKey() + properties.getTargetingKey(); - distributions = arguments; + + bucketBy = java.util.Arrays.asList(properties.getFlagKey(), properties.getTargetingKey()); + + if (arguments.get(0) == null) { + // arguments.get(0) resolved to null, skip it in distributions + distributions = arguments.subList(1, arguments.size()); + } else { + distributions = arguments; + } } final List propertyList = new ArrayList<>(); long totalWeight = 0; - try { - for (Object dist : distributions) { + for (Object dist : distributions) { + try { FractionProperty fractionProperty = new FractionProperty(dist, jsonPath); propertyList.add(fractionProperty); totalWeight += fractionProperty.getWeight(); + } catch (JsonLogicException e) { + if ("Property is not an array".equals(e.getMessage())) { + throw new io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException( + "Error parsing fractional targeting rule: " + e.getMessage(), jsonPath); + } + return null; } - } catch (JsonLogicException e) { - log.debug("Error parsing fractional targeting rule", e); - return null; } if (totalWeight > MAX_WEIGHT) { @@ -87,12 +109,20 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json } private static Object distributeValue( - final String hashKey, + final Object hashKey, final List propertyList, final int totalWeight, final String jsonPath) throws JsonLogicEvaluationException { - byte[] bytes = hashKey.getBytes(StandardCharsets.UTF_8); + byte[] bytes; + try { + JsonNode node = objectMapper.valueToTree(hashKey); + CBORObject dataItem = convertNode(node); + bytes = dataItem.EncodeToBytes(); + } catch (Exception e) { + log.debug("Error converting hashKey to CBOR", e); + throw new JsonLogicEvaluationException("Error converting hashKey to CBOR", jsonPath); + } int mmrHash = MurmurHash3.hash32x86(bytes, 0, bytes.length, 0); return distributeValueFromHash(mmrHash, propertyList, totalWeight, jsonPath); } @@ -129,6 +159,67 @@ static Object distributeValueFromHash( throw new JsonLogicEvaluationException("Unable to find a correct bucket for hash " + hash, jsonPath); } + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static class CanonicalKeyComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + + @Override + public int compare(String k1, String k2) { + byte[] b1 = k1.getBytes(StandardCharsets.UTF_8); + byte[] b2 = k2.getBytes(StandardCharsets.UTF_8); + if (b1.length != b2.length) { + return Integer.compare(b1.length, b2.length); + } + for (int i = 0; i < b1.length; i++) { + int v1 = b1[i] & 0xFF; + int v2 = b2[i] & 0xFF; + if (v1 != v2) { + return Integer.compare(v1, v2); + } + } + return 0; + } + } + + private static final CanonicalKeyComparator KEY_COMPARATOR = new CanonicalKeyComparator(); + + private static CBORObject convertNode(JsonNode node) { + if (node.isNull()) { + return CBORObject.Null; + } else if (node.isBoolean()) { + return CBORObject.FromObject(node.asBoolean()); + } else if (node.isTextual()) { + return CBORObject.FromObject(node.asText()); + } else if (node.isNumber()) { + if (node.isIntegralNumber()) { + return CBORObject.FromObject(node.asLong()); + } else { + double val = node.asDouble(); + if (val == Math.floor(val) && val >= Long.MIN_VALUE && val <= Long.MAX_VALUE) { + return CBORObject.FromObject((long) val); + } + return CBORObject.FromObject(val); + } + } else if (node.isArray()) { + CBORObject array = CBORObject.NewArray(); + for (JsonNode item : node) { + array.Add(convertNode(item)); + } + return array; + } else if (node.isObject()) { + CBORObject map = CBORObject.NewOrderedMap(); + List fieldNames = new ArrayList<>(); + node.fieldNames().forEachRemaining(fieldNames::add); + Collections.sort(fieldNames, KEY_COMPARATOR); + for (String fieldName : fieldNames) { + map.Add(fieldName, convertNode(node.get(fieldName))); + } + return map; + } + throw new IllegalArgumentException("Unsupported node type: " + node.getNodeType()); + } + @Getter @SuppressWarnings({"checkstyle:NoFinalizer"}) static class FractionProperty { diff --git a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/e2e/FlagdCoreEvaluatorTest.java b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/e2e/FlagdCoreEvaluatorTest.java index 941d978d5..8027215da 100644 --- a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/e2e/FlagdCoreEvaluatorTest.java +++ b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/e2e/FlagdCoreEvaluatorTest.java @@ -11,7 +11,7 @@ * configuration. Registered as an {@link dev.openfeature.contrib.tools.flagd.api.testkit.EvaluatorFactory} * via {@code META-INF/services}. */ -@ExcludeTags({"fractional-v1", "evaluator-refs-whitespace", "non-existent-evaluator-ref"}) +@ExcludeTags({"fractional-v1", "fractional-v2", "evaluator-refs-whitespace", "non-existent-evaluator-ref"}) public class FlagdCoreEvaluatorTest extends AbstractEvaluatorTest { @Override diff --git a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java index 046d8914f..e4d908e68 100644 --- a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java +++ b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java @@ -99,8 +99,10 @@ void missingBucketKeyReturnsNull() throws JsonLogicEvaluationException { List.of("one", 50), List.of("two", 50)); // bucketing key is null, so fractional falls back to flagKey + targetingKey - // but targetingKey is null, so it should return null - assertNull(fractional.evaluate(rule, data, "path")); + // but targetingKey is null, so it should throw GeneralError + org.junit.jupiter.api.Assertions.assertThrows(dev.openfeature.sdk.exceptions.GeneralError.class, () -> { + fractional.evaluate(rule, data, "path"); + }); } @Test diff --git a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/OperatorTest.java b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/OperatorTest.java index 888b8ef03..f8e741080 100644 --- a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/OperatorTest.java +++ b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/OperatorTest.java @@ -112,7 +112,7 @@ void fractionalTestB() throws TargetingRuleException { Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); // then - assertEquals("blue", evalVariant); + assertEquals("red", evalVariant); } @Test @@ -153,7 +153,7 @@ void fractionalTestA() throws TargetingRuleException { Object evalVariant = OPERATOR.apply("headerColor", targetingRule, new ImmutableContext(ctxData)); // then - assertEquals("yellow", evalVariant); + assertEquals("green", evalVariant); } @Test diff --git a/tools/flagd-core/src/test/resources/fractional/selfContainedFractionalB.json b/tools/flagd-core/src/test/resources/fractional/selfContainedFractionalB.json index e632f17d9..2beb7e5be 100644 --- a/tools/flagd-core/src/test/resources/fractional/selfContainedFractionalB.json +++ b/tools/flagd-core/src/test/resources/fractional/selfContainedFractionalB.json @@ -10,5 +10,5 @@ 50 ] ], - "result": "red" + "result": "blue" } diff --git a/tools/flagd-core/src/test/resources/fractional/string.json b/tools/flagd-core/src/test/resources/fractional/string.json index 8c55b68c1..65cb381b7 100644 --- a/tools/flagd-core/src/test/resources/fractional/string.json +++ b/tools/flagd-core/src/test/resources/fractional/string.json @@ -10,5 +10,5 @@ 70 ] ], - "result": "blue" + "result": "green" } From 500a41e0dc447a4d032d4e1e120e17badd3f4c55 Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Fri, 7 Aug 2026 14:52:40 +0000 Subject: [PATCH 02/10] fix(flagd-core): resolve PMD violation and optimize key comparator Signed-off-by: NeaguGeorgiana23 --- .../flagd/core/targeting/Fractional.java | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java index bccfc2075..1d1751b33 100644 --- a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java +++ b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java @@ -6,10 +6,8 @@ import io.github.jamsesso.jsonlogic.JsonLogicException; import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException; import io.github.jamsesso.jsonlogic.evaluator.expressions.PreEvaluatedArgumentsExpression; -import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; import lombok.Getter; @@ -108,6 +106,8 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json return distributeValue(bucketBy, propertyList, (int) totalWeight, jsonPath); } + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static Object distributeValue( final Object hashKey, final List propertyList, @@ -116,7 +116,7 @@ private static Object distributeValue( throws JsonLogicEvaluationException { byte[] bytes; try { - JsonNode node = objectMapper.valueToTree(hashKey); + JsonNode node = OBJECT_MAPPER.valueToTree(hashKey); CBORObject dataItem = convertNode(node); bytes = dataItem.EncodeToBytes(); } catch (Exception e) { @@ -159,36 +159,27 @@ static Object distributeValueFromHash( throw new JsonLogicEvaluationException("Unable to find a correct bucket for hash " + hash, jsonPath); } - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static class CanonicalKeyComparator implements Comparator, Serializable { - private static final long serialVersionUID = 1L; - - @Override - public int compare(String k1, String k2) { - byte[] b1 = k1.getBytes(StandardCharsets.UTF_8); - byte[] b2 = k2.getBytes(StandardCharsets.UTF_8); - if (b1.length != b2.length) { - return Integer.compare(b1.length, b2.length); - } - for (int i = 0; i < b1.length; i++) { - int v1 = b1[i] & 0xFF; - int v2 = b2[i] & 0xFF; - if (v1 != v2) { - return Integer.compare(v1, v2); - } + private static final Comparator KEY_COMPARATOR = (k1, k2) -> { + byte[] b1 = k1.getBytes(StandardCharsets.UTF_8); + byte[] b2 = k2.getBytes(StandardCharsets.UTF_8); + if (b1.length != b2.length) { + return Integer.compare(b1.length, b2.length); + } + for (int i = 0; i < b1.length; i++) { + int v1 = b1[i] & 0xFF; + int v2 = b2[i] & 0xFF; + if (v1 != v2) { + return Integer.compare(v1, v2); } - return 0; } - } - - private static final CanonicalKeyComparator KEY_COMPARATOR = new CanonicalKeyComparator(); + return 0; + }; private static CBORObject convertNode(JsonNode node) { if (node.isNull()) { return CBORObject.Null; } else if (node.isBoolean()) { - return CBORObject.FromObject(node.asBoolean()); + return node.asBoolean() ? CBORObject.True : CBORObject.False; } else if (node.isTextual()) { return CBORObject.FromObject(node.asText()); } else if (node.isNumber()) { @@ -204,16 +195,18 @@ private static CBORObject convertNode(JsonNode node) { } else if (node.isArray()) { CBORObject array = CBORObject.NewArray(); for (JsonNode item : node) { - array.Add(convertNode(item)); + CBORObject child = convertNode(item); + array.Add(child); } return array; } else if (node.isObject()) { CBORObject map = CBORObject.NewOrderedMap(); List fieldNames = new ArrayList<>(); node.fieldNames().forEachRemaining(fieldNames::add); - Collections.sort(fieldNames, KEY_COMPARATOR); + fieldNames.sort(KEY_COMPARATOR); for (String fieldName : fieldNames) { - map.Add(fieldName, convertNode(node.get(fieldName))); + CBORObject child = convertNode(node.get(fieldName)); + map.Add(fieldName, child); } return map; } From e3bf3336ac4e186d1d5fcb5ec0404aa4f8bf647a Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Fri, 7 Aug 2026 15:49:05 +0000 Subject: [PATCH 03/10] fix null value handling. Signed-off-by: NeaguGeorgiana23 --- .../contrib/tools/flagd/core/targeting/Fractional.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java index 1d1751b33..dec349264 100644 --- a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java +++ b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java @@ -59,9 +59,9 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json // fallback to targeting key if present if (properties.getTargetingKey() == null) { log.debug("Missing fallback targeting key"); - if (arguments.size() == 2) { - throw new dev.openfeature.sdk.exceptions.GeneralError("Missing fallback targeting key"); - } + // if (arguments.size() == 2) { + // throw new dev.openfeature.sdk.exceptions.GeneralError("Missing fallback targeting key"); + // } return null; } From 4adab2ea17f42a6b2a53e42db24cc2b18817b711 Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Mon, 24 Aug 2026 10:02:53 +0000 Subject: [PATCH 04/10] address comments. Signed-off-by: NeaguGeorgiana23 --- .../contrib/tools/flagd/core/targeting/Fractional.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java index dec349264..38ff0749d 100644 --- a/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java +++ b/tools/flagd-core/src/main/java/dev/openfeature/contrib/tools/flagd/core/targeting/Fractional.java @@ -20,6 +20,7 @@ @Slf4j class Fractional implements PreEvaluatedArgumentsExpression { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); static final int MAX_WEIGHT = Integer.MAX_VALUE; @Override @@ -59,9 +60,6 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json // fallback to targeting key if present if (properties.getTargetingKey() == null) { log.debug("Missing fallback targeting key"); - // if (arguments.size() == 2) { - // throw new dev.openfeature.sdk.exceptions.GeneralError("Missing fallback targeting key"); - // } return null; } @@ -85,7 +83,7 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json totalWeight += fractionProperty.getWeight(); } catch (JsonLogicException e) { if ("Property is not an array".equals(e.getMessage())) { - throw new io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluationException( + throw new JsonLogicEvaluationException( "Error parsing fractional targeting rule: " + e.getMessage(), jsonPath); } return null; @@ -106,8 +104,6 @@ public Object evaluate(List arguments, Object data, String jsonPath) throws Json return distributeValue(bucketBy, propertyList, (int) totalWeight, jsonPath); } - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static Object distributeValue( final Object hashKey, final List propertyList, From 4c3a794a5ae51a29382d5012670c5689adb25520 Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Mon, 24 Aug 2026 12:48:40 +0000 Subject: [PATCH 05/10] fix tests Signed-off-by: NeaguGeorgiana23 --- .../contrib/tools/flagd/core/targeting/FractionalTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java index e4d908e68..ed9388b0c 100644 --- a/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java +++ b/tools/flagd-core/src/test/java/dev/openfeature/contrib/tools/flagd/core/targeting/FractionalTest.java @@ -100,9 +100,7 @@ void missingBucketKeyReturnsNull() throws JsonLogicEvaluationException { // bucketing key is null, so fractional falls back to flagKey + targetingKey // but targetingKey is null, so it should throw GeneralError - org.junit.jupiter.api.Assertions.assertThrows(dev.openfeature.sdk.exceptions.GeneralError.class, () -> { - fractional.evaluate(rule, data, "path"); - }); + assertNull(fractional.evaluate(rule, data, "path")); } @Test From 3a2d2e05bc8ce5a4ad7c35bafefceb6cc19722fd Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Mon, 24 Aug 2026 14:06:11 +0000 Subject: [PATCH 06/10] address comments. Signed-off-by: NeaguGeorgiana23 --- .../openfeature/contrib/providers/flagd/Config.java | 2 +- .../providers/flagd/FlagdProviderSyncResources.java | 10 ---------- .../flagd/FlagdProviderSyncResourcesCTest.java | 12 +++++------- .../tools/flagd/api/testkit/ContextSteps.java | 2 +- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java index 1e152ff43..61182e4b5 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java @@ -19,7 +19,7 @@ public final class Config { static final int DEFAULT_DEADLINE = 500; static final int DEFAULT_MAX_RETRY_BACKOFF_MS = 5000; static final int DEFAULT_STREAM_DEADLINE_MS = 10 * 60 * 1000; - static final int DEFAULT_STREAM_RETRY_GRACE_PERIOD = 10; + static final int DEFAULT_STREAM_RETRY_GRACE_PERIOD = 5; static final int DEFAULT_MAX_CACHE_SIZE = 1000; static final int DEFAULT_OFFLINE_POLL_MS = 5000; static final long DEFAULT_KEEP_ALIVE = 0; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java index 35184df84..84d4745ec 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java @@ -110,22 +110,12 @@ public void waitForInitialization(long deadline) { */ public synchronized void shutdown() { isShutDown = true; - isInitialized = false; this.notifyAll(); } public synchronized void fatalError(ProviderEventDetails providerEventDetails) { isFatal = true; - isInitialized = false; fatalProviderEventDetails = providerEventDetails; this.notifyAll(); } - - public synchronized void setFatal(boolean fatal) { - isFatal = fatal; - if (fatal) { - isInitialized = false; - } - this.notifyAll(); - } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index f8c4d0977..8773967f9 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -163,7 +163,7 @@ void callingFatalError_wakesUpWaitingThreadWithException() { }, () -> { startTime.set(System.currentTimeMillis()); - flagdProviderSyncResources.setFatal(true); + flagdProviderSyncResources.fatalError(null); }); Assertions.assertTrue( @@ -193,7 +193,6 @@ void concurrentInitializeAndShutdownShutsDownWork() { while (interleavings.hasNext()) { Runner.runParallel( () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown()); - Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); } } @@ -208,8 +207,7 @@ void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { Runner.runParallel( () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown(), - () -> flagdProviderSyncResources.setFatal(true)); - Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + () -> flagdProviderSyncResources.fatalError(null)); Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); Assertions.assertTrue(flagdProviderSyncResources.isFatal()); } @@ -222,7 +220,7 @@ void concurrentInitializeAndSetFatalShutsDownWork() { try (var interleavings = new AllInterleavings("concurrent initialize() and fatal() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.setFatal(true)); + () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.fatalError(null)); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); Assertions.assertTrue(flagdProviderSyncResources.isFatal()); } @@ -254,7 +252,7 @@ void waitForInitializationAfterCallingShutdown_returnsInstantly() { @Timeout(2) @Test void waitForInitializationAfterCallingFatal_returnsInstantly() { - flagdProviderSyncResources.setFatal(true); + flagdProviderSyncResources.fatalError(null); long start = System.currentTimeMillis(); Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); long end = System.currentTimeMillis(); @@ -265,7 +263,7 @@ void waitForInitializationAfterCallingFatal_returnsInstantly() { @Timeout(2) @Test void initializeAfterFatalReturnsFalse() { - flagdProviderSyncResources.setFatal(true); + flagdProviderSyncResources.fatalError(null); Assertions.assertFalse(flagdProviderSyncResources.initialize()); } diff --git a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java index b87555204..0f88a0e81 100644 --- a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java +++ b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java @@ -46,7 +46,7 @@ public void contextWithFollowingKeys(DataTable dataTable) throws IOException { map.put(key, Value.objectToValue(EvaluatorUtils.convert(value, type))); } state.context = new MutableContext(state.context.getTargetingKey(), map); - } + } /** Sets the targeting key on the evaluation context. */ @Given("a context containing a targeting key with value {string}") From 30382419f68638e669beac20768dc51452bc75aa Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Mon, 24 Aug 2026 14:55:46 +0000 Subject: [PATCH 07/10] fix linter errors Signed-off-by: NeaguGeorgiana23 --- .../providers/flagd/FlagdProviderSyncResourcesCTest.java | 3 ++- .../contrib/tools/flagd/api/testkit/ContextSteps.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java index 8773967f9..c129f3af5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -220,7 +220,8 @@ void concurrentInitializeAndSetFatalShutsDownWork() { try (var interleavings = new AllInterleavings("concurrent initialize() and fatal() calls work")) { while (interleavings.hasNext()) { Runner.runParallel( - () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.fatalError(null)); + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.fatalError(null)); Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); Assertions.assertTrue(flagdProviderSyncResources.isFatal()); } diff --git a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java index 0f88a0e81..b87555204 100644 --- a/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java +++ b/tools/flagd-api-testkit/src/main/java/dev/openfeature/contrib/tools/flagd/api/testkit/ContextSteps.java @@ -46,7 +46,7 @@ public void contextWithFollowingKeys(DataTable dataTable) throws IOException { map.put(key, Value.objectToValue(EvaluatorUtils.convert(value, type))); } state.context = new MutableContext(state.context.getTargetingKey(), map); - } + } /** Sets the targeting key on the evaluation context. */ @Given("a context containing a targeting key with value {string}") From 6b487fca7d82d7d4e891be7c156d8a0cda313760 Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Tue, 25 Aug 2026 11:25:30 +0000 Subject: [PATCH 08/10] update exlude tags Signed-off-by: NeaguGeorgiana23 --- .../dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java index 05850c6e5..527f145f5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java @@ -28,7 +28,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") @ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") @IncludeTags({"rpc"}) -@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v3", "deprecated"}) +@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v2", "deprecated"}) @Testcontainers public class RunRpcTest { From 893006ffb27c58f2a57eb4250408966b9ad0cf5d Mon Sep 17 00:00:00 2001 From: NeaguGeorgiana23 Date: Tue, 25 Aug 2026 13:19:50 +0000 Subject: [PATCH 09/10] fix(flagd): add null safety to StorageStateChange and keep fractional-v3 excluded in flagd e2e Signed-off-by: NeaguGeorgiana23 --- .../resolver/process/storage/StorageStateChange.java | 10 +++++----- .../contrib/providers/flagd/e2e/RunFileTest.java | 2 +- .../contrib/providers/flagd/e2e/RunInProcessTest.java | 2 +- .../contrib/providers/flagd/e2e/RunRpcTest.java | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java index 9875cbe9c..71f83bfb1 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageStateChange.java @@ -26,8 +26,10 @@ public class StorageStateChange { */ public StorageStateChange(StorageState storageState, List changedFlagsKeys, Structure syncMetadata) { this.storageState = storageState; - this.changedFlagsKeys = Collections.unmodifiableList(changedFlagsKeys); - this.syncMetadata = new ImmutableStructure(syncMetadata.asMap()); + this.changedFlagsKeys = + changedFlagsKeys != null ? Collections.unmodifiableList(changedFlagsKeys) : Collections.emptyList(); + this.syncMetadata = + syncMetadata != null ? new ImmutableStructure(syncMetadata.asMap()) : new ImmutableStructure(); } /** @@ -37,9 +39,7 @@ public StorageStateChange(StorageState storageState, List changedFlagsKe * @param changedFlagsKeys flags changed */ public StorageStateChange(StorageState storageState, List changedFlagsKeys) { - this.storageState = storageState; - this.changedFlagsKeys = Collections.unmodifiableList(changedFlagsKeys); - this.syncMetadata = new ImmutableStructure(); + this(storageState, changedFlagsKeys, null); } /** diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java index 21dea4589..5901fe08e 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java @@ -36,7 +36,7 @@ "events", "contextEnrichment", "fractional-v1", - "fractional-v2", + "fractional-v3", "deprecated" }) @Testcontainers diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java index bf31c3bc0..797756211 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java @@ -28,7 +28,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") @ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") @IncludeTags("in-process") -@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v2", "deprecated"}) +@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v3", "deprecated"}) @Testcontainers public class RunInProcessTest { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java index 527f145f5..05850c6e5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunRpcTest.java @@ -28,7 +28,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") @ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") @IncludeTags({"rpc"}) -@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v2", "deprecated"}) +@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v3", "deprecated"}) @Testcontainers public class RunRpcTest { From e388c16039f3a6613c3c6bfa0cae3fbc2e236574 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Thu, 27 Aug 2026 11:28:41 -0400 Subject: [PATCH 10/10] fixup: correct tags Signed-off-by: Todd Baert --- .../java/dev/openfeature/contrib/providers/flagd/Config.java | 2 +- .../openfeature/contrib/providers/flagd/e2e/RunFileTest.java | 2 +- .../contrib/providers/flagd/e2e/RunInProcessTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java index 61182e4b5..1e152ff43 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Config.java @@ -19,7 +19,7 @@ public final class Config { static final int DEFAULT_DEADLINE = 500; static final int DEFAULT_MAX_RETRY_BACKOFF_MS = 5000; static final int DEFAULT_STREAM_DEADLINE_MS = 10 * 60 * 1000; - static final int DEFAULT_STREAM_RETRY_GRACE_PERIOD = 5; + static final int DEFAULT_STREAM_RETRY_GRACE_PERIOD = 10; static final int DEFAULT_MAX_CACHE_SIZE = 1000; static final int DEFAULT_OFFLINE_POLL_MS = 5000; static final long DEFAULT_KEEP_ALIVE = 0; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java index 5901fe08e..21dea4589 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunFileTest.java @@ -36,7 +36,7 @@ "events", "contextEnrichment", "fractional-v1", - "fractional-v3", + "fractional-v2", "deprecated" }) @Testcontainers diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java index 797756211..bf31c3bc0 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/e2e/RunInProcessTest.java @@ -28,7 +28,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "dev.openfeature.contrib.providers.flagd.e2e.steps") @ConfigurationParameter(key = OBJECT_FACTORY_PROPERTY_NAME, value = "io.cucumber.picocontainer.PicoFactory") @IncludeTags("in-process") -@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v3", "deprecated"}) +@ExcludeTags({"unixsocket", "fractional-v1", "fractional-v2", "deprecated"}) @Testcontainers public class RunInProcessTest {