From 0ee03da856db1e9643bd6aeca4e2842074f86147 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 23 Oct 2024 10:59:21 -0500 Subject: [PATCH 1/5] wip --- .../src/main/java/sdktest/TestService.java | 3 +- .../launchdarkly/sdk/server/EvalResult.java | 24 ++++++++++ .../launchdarkly/sdk/server/Evaluator.java | 22 ++++++++- .../sdk/server/FeatureFlagsState.java | 48 +++++++++++++------ .../sdk/server/PrerequisiteEvalRecord.java | 13 +++++ .../com/launchdarkly/sdk/server/Version.java | 2 - .../sdk/server/FeatureFlagsStateTest.java | 40 +++++++++------- .../sdk/server/LDClientEvaluationTest.java | 43 +++++++++++++++++ 8 files changed, 159 insertions(+), 36 deletions(-) create mode 100644 lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PrerequisiteEvalRecord.java diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java index c08ba26..5e723ec 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java @@ -37,7 +37,8 @@ public class TestService { "event-sampling", "inline-context", "anonymous-redaction", - "evaluation-hooks" + "evaluation-hooks", + "client-prereq-events" }; static final Gson gson = new GsonBuilder().serializeNulls().create(); diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java index 0d0e879..2a6c326 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java @@ -6,6 +6,9 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; +import java.util.Collections; +import java.util.List; + import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; /** @@ -30,6 +33,9 @@ final class EvalResult { private final EvaluationDetail asString; private final boolean forceReasonTracking; + // A list of prerequisites evaluation records evaluated as part of obtaining this result. + private List prerequisiteEvalRecords; + /** * Constructs an instance that wraps the specified EvaluationDetail and also precomputes * any appropriate type-specific variants (asBoolean, etc.). @@ -100,6 +106,7 @@ private EvalResult(EvalResult from, EvaluationReason newReason) { this.asDouble = transformReason(from.asDouble, newReason); this.asString = transformReason(from.asString, newReason); this.forceReasonTracking = from.forceReasonTracking; + this.prerequisiteEvalRecords = from.prerequisiteEvalRecords; } private EvalResult(EvalResult from, boolean newForceTracking) { @@ -109,6 +116,17 @@ private EvalResult(EvalResult from, boolean newForceTracking) { this.asDouble = from.asDouble; this.asString = from.asString; this.forceReasonTracking = newForceTracking; + this.prerequisiteEvalRecords = from.prerequisiteEvalRecords; + } + + private EvalResult(EvalResult from, List prerequisiteEvalRecords) { + this.anyType = from.anyType; + this.asBoolean = from.asBoolean; + this.asInteger = from.asInteger; + this.asDouble = from.asDouble; + this.asString = from.asString; + this.forceReasonTracking = from.forceReasonTracking; + this.prerequisiteEvalRecords = prerequisiteEvalRecords; } /** @@ -208,6 +226,8 @@ public EvaluationDetail getAsString() { * @return true if reason tracking is required for this result */ public boolean isForceReasonTracking() { return forceReasonTracking; } + + public List getPrerequisiteEvalRecords() { return prerequisiteEvalRecords; } /** * Returns a transformed copy of this EvalResult with a different evaluation reason. @@ -226,6 +246,10 @@ public EvalResult withReason(EvaluationReason newReason) { public EvalResult withForceReasonTracking(boolean newValue) { return this.forceReasonTracking == newValue ? this : new EvalResult(this, newValue); } + + public EvalResult withPrerequisiteEvalRecords(List newValue) { + return this.prerequisiteEvalRecords == newValue ? this : new EvalResult(this, newValue); + } @Override public boolean equals(Object other) { diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java index b605c16..9816073 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java @@ -122,6 +122,7 @@ private static class EvaluatorState { private EvaluationReason.BigSegmentsStatus bigSegmentsStatus = null; private FeatureFlag originalFlag = null; private List prerequisiteStack = null; + private List prerequisiteEvalRecords = null; private List segmentStack = null; } @@ -145,15 +146,24 @@ EvalResult evaluate(FeatureFlag flag, LDContext context, @Nonnull EvaluationReco EvaluatorState state = new EvaluatorState(); state.originalFlag = flag; + // allocate list capacity to avoid size increase during evaluation + state.prerequisiteEvalRecords = new ArrayList<>(); // TODO: optimize when this is used, shouldn't allocate for flag with no prereqs try { EvalResult result = evaluateInternal(flag, context, recorder, state); if (state.bigSegmentsStatus != null) { - return result.withReason( + result = result.withReason( result.getReason().withBigSegmentsStatus(state.bigSegmentsStatus) ); } + + // TODO: these changes have reduced throughput, can we optimize this a bit. Perhaps by calling constructor + // with all parameters instead of using multiple calls in this immutable style + if (state.prerequisiteEvalRecords != null && !state.prerequisiteEvalRecords.isEmpty()) { + result = result.withPrerequisiteEvalRecords(state.prerequisiteEvalRecords); + } + return result; } catch (EvaluationException e) { logger.error("Could not evaluate flag \"{}\": {}", flag.getKey(), e.getMessage()); @@ -161,6 +171,15 @@ EvalResult evaluate(FeatureFlag flag, LDContext context, @Nonnull EvaluationReco } } + /** + * Internal evaluation function that may be called multiple times during a flag evaluation. + * + * @param flag that to evaluate + * @param context to use for evaluation + * @param recorder that will be used to record evaluation events + * @param state for mutable values needed during evaluation + * @return the evaluation result + */ private EvalResult evaluateInternal(FeatureFlag flag, LDContext context, @Nonnull EvaluationRecorder recorder, EvaluatorState state) { if (!flag.isOn()) { return EvaluatorHelpers.offResult(flag); @@ -237,6 +256,7 @@ private EvalResult checkPrerequisites(FeatureFlag flag, LDContext context, @Nonn if (!prereqFeatureFlag.isOn() || prereqEvalResult.getVariationIndex() != prereq.getVariation()) { prereqOk = false; } + state.prerequisiteEvalRecords.add(new PrerequisiteEvalRecord(prereqFeatureFlag, flag, prereqEvalResult)); recorder.recordPrerequisiteEvaluation(prereqFeatureFlag, flag, context, prereqEvalResult); } if (!prereqOk) { diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java index e48f037..2f3bba4 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java @@ -13,8 +13,10 @@ import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import static com.launchdarkly.sdk.server.JsonHelpers.gsonInstanceWithNullsAllowed; @@ -48,9 +50,10 @@ static class FlagMetadata { final boolean trackEvents; final boolean trackReason; final Long debugEventsUntilDate; + final List prerequisites; FlagMetadata(LDValue value, Integer variation, EvaluationReason reason, Integer version, - boolean trackEvents, boolean trackReason, Long debugEventsUntilDate) { + boolean trackEvents, boolean trackReason, Long debugEventsUntilDate, List prerequisites) { this.value = LDValue.normalize(value); this.variation = variation; this.reason = reason; @@ -58,8 +61,9 @@ static class FlagMetadata { this.trackEvents = trackEvents; this.trackReason = trackReason; this.debugEventsUntilDate = debugEventsUntilDate; + this.prerequisites = prerequisites; } - + @Override public boolean equals(Object other) { if (other instanceof FlagMetadata) { @@ -70,14 +74,15 @@ public boolean equals(Object other) { Objects.equals(version, o.version) && trackEvents == o.trackEvents && trackReason == o.trackReason && - Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate); + Objects.equals(debugEventsUntilDate, o.debugEventsUntilDate) && + Objects.equals(prerequisites, o.prerequisites); } return false; } - + @Override public int hashCode() { - return Objects.hash(variation, version, trackEvents, trackReason, debugEventsUntilDate); + return Objects.hash(value, variation, reason, version, trackEvents, trackReason, debugEventsUntilDate, prerequisites); } } @@ -216,9 +221,10 @@ public Builder add( EvaluationReason reason, int flagVersion, boolean trackEvents, - Long debugEventsUntilDate + Long debugEventsUntilDate, + List prerequisites ) { - return add(flagKey, value, variationIndex, reason, flagVersion, trackEvents, false, debugEventsUntilDate); + return add(flagKey, value, variationIndex, reason, flagVersion, trackEvents, false, debugEventsUntilDate, prerequisites); } /** @@ -236,9 +242,10 @@ public Builder add( * @param flagVersion the current flag version * @param trackEvents true if full event tracking is turned on for this flag * @param trackReason true if evaluation reasons must be included due to experimentation - * @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp) + * @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp) * @return the builder */ + // TODO: add param to docs public Builder add( String flagKey, LDValue value, @@ -247,7 +254,8 @@ public Builder add( int flagVersion, boolean trackEvents, boolean trackReason, - Long debugEventsUntilDate + Long debugEventsUntilDate, + List prerequisites ) { final boolean flagIsTracked = trackEvents || (debugEventsUntilDate != null && debugEventsUntilDate > System.currentTimeMillis()); @@ -259,8 +267,8 @@ public Builder add( wantDetails ? Integer.valueOf(flagVersion) : null, trackEvents, trackReason, - debugEventsUntilDate - ); + debugEventsUntilDate, + prerequisites); flagMetadata.put(flagKey, data); return this; } @@ -274,7 +282,11 @@ Builder addFlag(DataModel.FeatureFlag flag, EvalResult eval) { flag.getVersion(), flag.isTrackEvents() || eval.isForceReasonTracking(), eval.isForceReasonTracking(), - flag.getDebugEventsUntilDate() + flag.getDebugEventsUntilDate(), + eval.getPrerequisiteEvalRecords() == null ? null : eval.getPrerequisiteEvalRecords().stream() + .filter(record -> record.prereqOfFlag.getKey() == flag.getKey()) // only include top level prereqs + .map(record -> record.flag.getKey()) // map from prereq record to prereq key + .collect(Collectors.toList()) ); } @@ -331,6 +343,14 @@ public void write(JsonWriter out, FeatureFlagsState state) throws IOException { out.name("debugEventsUntilDate"); out.value(meta.debugEventsUntilDate.longValue()); } + if (meta.prerequisites != null && !meta.prerequisites.isEmpty()) { + out.name("prerequisites"); + out.beginArray(); + for (String s: meta.prerequisites) { + out.value(s); + } + out.endArray(); + } out.endObject(); } out.endObject(); @@ -377,8 +397,8 @@ public FeatureFlagsState read(JsonReader in) throws IOException { m0.version, m0.trackEvents, m0.trackReason, - m0.debugEventsUntilDate - ); + m0.debugEventsUntilDate, + m0.prerequisites); allFlagMetadata.put(e.getKey(), m1); } } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PrerequisiteEvalRecord.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PrerequisiteEvalRecord.java new file mode 100644 index 0000000..7bb57b0 --- /dev/null +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PrerequisiteEvalRecord.java @@ -0,0 +1,13 @@ +package com.launchdarkly.sdk.server; + +public class PrerequisiteEvalRecord { + public final DataModel.FeatureFlag flag; + public final DataModel.FeatureFlag prereqOfFlag; + public final EvalResult result; + + public PrerequisiteEvalRecord(DataModel.FeatureFlag flag, DataModel.FeatureFlag prereqOfFlag, EvalResult result) { + this.flag = flag; + this.prereqOfFlag = prereqOfFlag; + this.result = result; + } +} diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java index 880687e..2730275 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java @@ -4,7 +4,5 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed - // x-release-please-start-version static final String SDK_VERSION = "7.5.0"; - // x-release-please-end } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java index dc67b32..2da3a57 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java @@ -12,6 +12,7 @@ import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; @@ -30,7 +31,7 @@ public class FeatureFlagsStateTest { @Test public void canGetFlagValue() { FeatureFlagsState state = FeatureFlagsState.builder() - .add("key", LDValue.of("value"), 1, null, 10, false, null) + .add("key", LDValue.of("value"), 1, null, 10, false, null, null) .build(); assertEquals(LDValue.of("value"), state.getFlagValue("key")); @@ -46,7 +47,7 @@ public void unknownFlagReturnsNullValue() { @Test public void canGetFlagReason() { FeatureFlagsState state = FeatureFlagsState.builder(WITH_REASONS) - .add("key", LDValue.of("value"), 1, EvaluationReason.off(), 10, false, null) + .add("key", LDValue.of("value"), 1, EvaluationReason.off(), 10, false, null, null) .build(); assertEquals(EvaluationReason.off(), state.getFlagReason("key")); @@ -62,7 +63,7 @@ public void unknownFlagReturnsNullReason() { @Test public void reasonIsNullIfReasonsWereNotRecorded() { FeatureFlagsState state = FeatureFlagsState.builder() - .add("key", LDValue.of("value"), 1, EvaluationReason.off(), 10, false, null) + .add("key", LDValue.of("value"), 1, EvaluationReason.off(), 10, false, null, null) .build(); assertNull(state.getFlagReason("key")); @@ -71,7 +72,7 @@ public void reasonIsNullIfReasonsWereNotRecorded() { @Test public void flagIsTreatedAsTrackedIfDebugEventsUntilDateIsInFuture() { FeatureFlagsState state = FeatureFlagsState.builder(WITH_REASONS, DETAILS_ONLY_FOR_TRACKED_FLAGS) - .add("key", LDValue.of("value"), 1, EvaluationReason.off(), 10, false, System.currentTimeMillis() + 1000000) + .add("key", LDValue.of("value"), 1, EvaluationReason.off(), 10, false, System.currentTimeMillis() + 1000000, null) .build(); assertNotNull(state.getFlagReason("key")); @@ -80,7 +81,7 @@ public void flagIsTreatedAsTrackedIfDebugEventsUntilDateIsInFuture() { @Test public void flagIsNotTreatedAsTrackedIfDebugEventsUntilDateIsInPast() { FeatureFlagsState state = FeatureFlagsState.builder(WITH_REASONS, DETAILS_ONLY_FOR_TRACKED_FLAGS) - .add("key", LDValue.of("value"), 1, EvaluationReason.off(), 10, false, System.currentTimeMillis() - 1000000) + .add("key", LDValue.of("value"), 1, EvaluationReason.off(), 10, false, System.currentTimeMillis() - 1000000, null) .build(); assertNull(state.getFlagReason("key")); @@ -89,7 +90,7 @@ public void flagIsNotTreatedAsTrackedIfDebugEventsUntilDateIsInPast() { @Test public void flagCanHaveNullValue() { FeatureFlagsState state = FeatureFlagsState.builder() - .add("key", LDValue.ofNull(), 1, null, 10, false, null) + .add("key", LDValue.ofNull(), 1, null, 10, false, null, null) .build(); assertEquals(LDValue.ofNull(), state.getFlagValue("key")); @@ -98,8 +99,8 @@ public void flagCanHaveNullValue() { @Test public void canConvertToValuesMap() { FeatureFlagsState state = FeatureFlagsState.builder() - .add("key1", LDValue.of("value1"), 0, null, 10, false, null) - .add("key2", LDValue.of("value2"), 1, null, 10, false, null) + .add("key1", LDValue.of("value1"), 0, null, 10, false, null, null) + .add("key2", LDValue.of("value2"), 1, null, 10, false, null, null) .build(); ImmutableMap expected = ImmutableMap.of("key1", LDValue.of("value1"), "key2", LDValue.of("value2")); @@ -109,19 +110,19 @@ public void canConvertToValuesMap() { @Test public void equalInstancesAreEqual() { FeatureFlagsState justOneFlag = FeatureFlagsState.builder(WITH_REASONS) - .add("key1", LDValue.of("value1"), 0, EvaluationReason.off(), 10, false, null) + .add("key1", LDValue.of("value1"), 0, EvaluationReason.off(), 10, false, null, null) .build(); FeatureFlagsState sameFlagsDifferentInstances1 = FeatureFlagsState.builder(WITH_REASONS) - .add("key1", LDValue.of("value1"), 0, EvaluationReason.off(), 10, false, null) - .add("key2", LDValue.of("value2"), 1, EvaluationReason.fallthrough(), 10, false, null) + .add("key1", LDValue.of("value1"), 0, EvaluationReason.off(), 10, false, null, null) + .add("key2", LDValue.of("value2"), 1, EvaluationReason.fallthrough(), 10, false, null, null) .build(); FeatureFlagsState sameFlagsDifferentInstances2 = FeatureFlagsState.builder(WITH_REASONS) - .add("key1", LDValue.of("value1"), 0, EvaluationReason.off(), 10, false, null) - .add("key2", LDValue.of("value2"), 1, EvaluationReason.fallthrough(), 10, false, null) + .add("key1", LDValue.of("value1"), 0, EvaluationReason.off(), 10, false, null, null) + .add("key2", LDValue.of("value2"), 1, EvaluationReason.fallthrough(), 10, false, null, null) .build(); FeatureFlagsState sameFlagsDifferentMetadata = FeatureFlagsState.builder(WITH_REASONS) - .add("key1", LDValue.of("value1"), 1, EvaluationReason.off(), 10, false, null) - .add("key2", LDValue.of("value2"), 1, EvaluationReason.fallthrough(), 10, false, null) + .add("key1", LDValue.of("value1"), 1, EvaluationReason.off(), 10, false, null, null) + .add("key2", LDValue.of("value2"), 1, EvaluationReason.fallthrough(), 10, false, null, null) .build(); FeatureFlagsState noFlagsButValid = FeatureFlagsState.builder(WITH_REASONS).build(); FeatureFlagsState noFlagsAndNotValid = FeatureFlagsState.builder(WITH_REASONS).valid(false).build(); @@ -147,8 +148,10 @@ public void equalMetadataInstancesAreEqual() { for (boolean trackEvents: new boolean[] { false, true }) { for (boolean trackReason: new boolean[] { false, true }) { for (Long debugEventsUntilDate: new Long[] { null, 1000L, 1001L }) { - allPermutations.add(() -> new FeatureFlagsState.FlagMetadata( - value, variation, reason, version, trackEvents, trackReason, debugEventsUntilDate)); + for (List prerequisites: Arrays.asList(null, Arrays.asList("prereq1"), Arrays.asList("prereq2", "prereq3"))) { + allPermutations.add(() -> new FeatureFlagsState.FlagMetadata( + value, variation, reason, version, trackEvents, trackReason, debugEventsUntilDate, prerequisites)); + } } } } @@ -220,6 +223,7 @@ public void canSerializeAndDeserializeWithJackson() throws Exception { assertJsonEquals(makeExpectedJsonSerialization(), actualJsonString); FeatureFlagsState state = jacksonMapper.readValue(makeExpectedJsonSerialization(), FeatureFlagsState.class); - assertEquals(makeInstanceForSerialization(), state); + FeatureFlagsState expected = makeInstanceForSerialization(); + assertEquals(expected, state); } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java index b284afa..5dabd06 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java @@ -26,6 +26,7 @@ import static com.launchdarkly.sdk.server.ModelBuilders.fallthroughVariation; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.flagWithValue; +import static com.launchdarkly.sdk.server.ModelBuilders.prerequisite; import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; import static com.launchdarkly.sdk.server.TestComponents.dataStoreThatThrowsException; import static com.launchdarkly.sdk.server.TestComponents.failedDataSource; @@ -34,6 +35,7 @@ import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; import static com.launchdarkly.sdk.server.TestUtil.upsertSegment; import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonEquals; +import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonIncludes; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -653,4 +655,45 @@ public void allFlagsStateReturnsEmptyStateIfClientAndStoreAreNotInitialized() th assertFalse(state.isValid()); } } + + @Test + public void allFlagStateIncludesPrerequisites() throws Exception { + DataModel.FeatureFlag flag1 = flagBuilder("flagA") + .version(1) + .on(true) + .variations(LDValue.of("off"), LDValue.of("value1")) + .fallthrough(fallthroughVariation(0)) + .trackEvents(false) + .build(); + DataModel.FeatureFlag flag2 = flagBuilder("flagAB") + .version(2) + .on(true) + .variations(LDValue.of("off"), LDValue.of("value2")) + .fallthrough(fallthroughVariation(0)) + .prerequisites(prerequisite("flagA", 0)) + .trackEvents(false) + .build(); + DataModel.FeatureFlag flag3 = flagBuilder("flagABC") + .version(3) + .on(true) + .variations(LDValue.of("x"), LDValue.of("value3")) + .fallthrough(fallthroughVariation(0)) + .prerequisites(prerequisite("flagAB", 0)) + .trackEvents(false) + .build(); + upsertFlag(dataStore, flag1); + upsertFlag(dataStore, flag2); + upsertFlag(dataStore, flag3); + + FeatureFlagsState state = client.allFlagsState(context); + assertTrue(state.isValid()); + + String outputJson = gson.toJson(state); + String expectedPart1 = "{\"$flagsState\":{\"flagA\":{}}}"; + String expectedPart2 = "{\"$flagsState\":{\"flagAB\":{\"prerequisites\":[\"flagA\"]}}}"; + String expectedPart3 = "{\"$flagsState\":{\"flagABC\":{\"prerequisites\":[\"flagAB\"]}}}"; + assertJsonIncludes(expectedPart1, outputJson); + assertJsonIncludes(expectedPart2, outputJson); + assertJsonIncludes(expectedPart3, outputJson); + } } From 5d6f9eaa83dc35bd5566cb931cbeacb50fbdf37f Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 23 Oct 2024 11:02:47 -0500 Subject: [PATCH 2/5] reverting Version.java change --- .../src/main/java/com/launchdarkly/sdk/server/Version.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java index 2730275..880687e 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java @@ -4,5 +4,7 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed + // x-release-please-start-version static final String SDK_VERSION = "7.5.0"; + // x-release-please-end } From 2bf741fc95cd8c639df13e5a7fc9232dbda3c938 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 23 Oct 2024 11:26:34 -0500 Subject: [PATCH 3/5] fixing style issues --- .../java/com/launchdarkly/sdk/server/FeatureFlagsState.java | 5 +++-- .../src/main/java/com/launchdarkly/sdk/server/Version.java | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java index 2f3bba4..30fd980 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java @@ -211,7 +211,8 @@ public Builder valid(boolean valid) { * @param reason the evaluation reason * @param flagVersion the current flag version * @param trackEvents true if full event tracking is turned on for this flag - * @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp) + * @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp) + * @param prerequisites list of flag keys of the top level prerequisite flags evaluated as part of this evaluation * @return the builder */ public Builder add( @@ -243,9 +244,9 @@ public Builder add( * @param trackEvents true if full event tracking is turned on for this flag * @param trackReason true if evaluation reasons must be included due to experimentation * @param debugEventsUntilDate if set, event debugging is turned until this time (millisecond timestamp) + * @param prerequisites list of flag keys of the top level prerequisite flags evaluated as part of this evaluation * @return the builder */ - // TODO: add param to docs public Builder add( String flagKey, LDValue value, diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java index 880687e..2730275 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java @@ -4,7 +4,5 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed - // x-release-please-start-version static final String SDK_VERSION = "7.5.0"; - // x-release-please-end } From 5a6a15290c75b6172d28c4439ccd1325cbe27e1c Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 23 Oct 2024 15:15:51 -0500 Subject: [PATCH 4/5] adding more tests --- .../launchdarkly/sdk/server/EvalResult.java | 3 +- .../launchdarkly/sdk/server/Evaluator.java | 6 +- .../sdk/server/FeatureFlagsState.java | 5 +- .../sdk/server/EvaluatorPrerequisiteTest.java | 76 ++++++++++++++----- .../sdk/server/EvaluatorTest.java | 32 ++++---- .../sdk/server/EvaluatorTestUtil.java | 24 ------ .../sdk/server/FeatureFlagsStateTest.java | 3 +- .../sdk/server/LDClientEvaluationTest.java | 24 +++++- 8 files changed, 99 insertions(+), 74 deletions(-) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java index 2a6c326..8611afb 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvalResult.java @@ -6,6 +6,7 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,7 +35,7 @@ final class EvalResult { private final boolean forceReasonTracking; // A list of prerequisites evaluation records evaluated as part of obtaining this result. - private List prerequisiteEvalRecords; + private List prerequisiteEvalRecords = new ArrayList<>(0); // 0 initial capacity uses a static instance for performance /** * Constructs an instance that wraps the specified EvaluationDetail and also precomputes diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java index 9816073..845d4aa 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Evaluator.java @@ -122,7 +122,7 @@ private static class EvaluatorState { private EvaluationReason.BigSegmentsStatus bigSegmentsStatus = null; private FeatureFlag originalFlag = null; private List prerequisiteStack = null; - private List prerequisiteEvalRecords = null; + private List prerequisiteEvalRecords = new ArrayList<>(0); // 0 initial capacity uses a static instance for performance private List segmentStack = null; } @@ -146,8 +146,6 @@ EvalResult evaluate(FeatureFlag flag, LDContext context, @Nonnull EvaluationReco EvaluatorState state = new EvaluatorState(); state.originalFlag = flag; - // allocate list capacity to avoid size increase during evaluation - state.prerequisiteEvalRecords = new ArrayList<>(); // TODO: optimize when this is used, shouldn't allocate for flag with no prereqs try { EvalResult result = evaluateInternal(flag, context, recorder, state); @@ -158,8 +156,6 @@ EvalResult evaluate(FeatureFlag flag, LDContext context, @Nonnull EvaluationReco ); } - // TODO: these changes have reduced throughput, can we optimize this a bit. Perhaps by calling constructor - // with all parameters instead of using multiple calls in this immutable style if (state.prerequisiteEvalRecords != null && !state.prerequisiteEvalRecords.isEmpty()) { result = result.withPrerequisiteEvalRecords(state.prerequisiteEvalRecords); } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java index 30fd980..beafaae 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java @@ -12,6 +12,7 @@ import com.launchdarkly.sdk.server.interfaces.LDClientInterface; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -284,7 +285,7 @@ Builder addFlag(DataModel.FeatureFlag flag, EvalResult eval) { flag.isTrackEvents() || eval.isForceReasonTracking(), eval.isForceReasonTracking(), flag.getDebugEventsUntilDate(), - eval.getPrerequisiteEvalRecords() == null ? null : eval.getPrerequisiteEvalRecords().stream() + eval.getPrerequisiteEvalRecords().stream() .filter(record -> record.prereqOfFlag.getKey() == flag.getKey()) // only include top level prereqs .map(record -> record.flag.getKey()) // map from prereq record to prereq key .collect(Collectors.toList()) @@ -399,7 +400,7 @@ public FeatureFlagsState read(JsonReader in) throws IOException { m0.trackEvents, m0.trackReason, m0.debugEventsUntilDate, - m0.prerequisites); + m0.prerequisites != null ? m0.prerequisites : new ArrayList<>(0)); allFlagMetadata.put(e.getKey(), m1); } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorPrerequisiteTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorPrerequisiteTest.java index 38a716f..05f184b 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorPrerequisiteTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorPrerequisiteTest.java @@ -6,9 +6,6 @@ import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Prerequisite; -import com.launchdarkly.sdk.server.EvaluatorTestUtil.PrereqEval; -import com.launchdarkly.sdk.server.EvaluatorTestUtil.PrereqRecorder; - import org.junit.Test; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_USER; @@ -44,6 +41,7 @@ public void flagReturnsOffVariationIfPrerequisiteIsNotFound() throws Exception { EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); + assertEquals(0, result.getPrerequisiteEvalRecords().size()); } @Test @@ -58,18 +56,18 @@ public void flagReturnsOffVariationAndEventIfPrerequisiteIsOff() throws Exceptio // note that even though it returns the desired variation, it is still off and therefore not a match .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); - PrereqRecorder recordPrereqs = new PrereqRecorder(); - EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); - assertEquals(1, Iterables.size(recordPrereqs.evals)); - PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(1, Iterables.size(result.getPrerequisiteEvalRecords())); + PrerequisiteEvalRecord eval = result.getPrerequisiteEvalRecords().get(0); assertEquals(f1, eval.flag); assertEquals(f0, eval.prereqOfFlag); assertEquals(GREEN_VARIATION, eval.result.getVariationIndex()); assertEquals(GREEN_VALUE, eval.result.getValue()); + assertEquals(1, result.getPrerequisiteEvalRecords().size()); } @Test @@ -83,18 +81,18 @@ public void flagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() throws Excep .fallthroughVariation(RED_VARIATION) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); - PrereqRecorder recordPrereqs = new PrereqRecorder(); - EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); - - assertEquals(1, Iterables.size(recordPrereqs.evals)); - PrereqEval eval = recordPrereqs.evals.get(0); + + assertEquals(1, Iterables.size(result.getPrerequisiteEvalRecords())); + PrerequisiteEvalRecord eval = result.getPrerequisiteEvalRecords().get(0); assertEquals(f1, eval.flag); assertEquals(f0, eval.prereqOfFlag); assertEquals(RED_VARIATION, eval.result.getVariationIndex()); assertEquals(RED_VALUE, eval.result.getValue()); + assertEquals(1, result.getPrerequisiteEvalRecords().size()); } @Test @@ -145,13 +143,12 @@ public void flagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAr .version(2) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); - PrereqRecorder recordPrereqs = new PrereqRecorder(); - EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); - assertEquals(1, Iterables.size(recordPrereqs.evals)); - PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(1, Iterables.size(result.getPrerequisiteEvalRecords())); + PrerequisiteEvalRecord eval = result.getPrerequisiteEvalRecords().get(0); assertEquals(f1, eval.flag); assertEquals(f0, eval.prereqOfFlag); assertEquals(GREEN_VARIATION, eval.result.getVariationIndex()); @@ -174,26 +171,63 @@ public void multipleLevelsOfPrerequisitesProduceMultipleEvents() throws Exceptio .fallthroughVariation(GREEN_VARIATION) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1, f2).build(); - PrereqRecorder recordPrereqs = new PrereqRecorder(); - EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); - assertEquals(2, Iterables.size(recordPrereqs.evals)); + assertEquals(2, Iterables.size(result.getPrerequisiteEvalRecords())); - PrereqEval eval0 = recordPrereqs.evals.get(0); + PrerequisiteEvalRecord eval0 = result.getPrerequisiteEvalRecords().get(0); assertEquals(f2, eval0.flag); assertEquals(f1, eval0.prereqOfFlag); assertEquals(GREEN_VARIATION, eval0.result.getVariationIndex()); assertEquals(GREEN_VALUE, eval0.result.getValue()); - PrereqEval eval1 = recordPrereqs.evals.get(1); + PrerequisiteEvalRecord eval1 = result.getPrerequisiteEvalRecords().get(1); assertEquals(f1, eval1.flag); assertEquals(f0, eval1.prereqOfFlag); assertEquals(GREEN_VARIATION, eval1.result.getVariationIndex()); assertEquals(GREEN_VALUE, eval1.result.getValue()); } + @Test + public void prerequisitesListIsAccurateWhenShortCircuiting() throws Exception { + FeatureFlag f0 = buildThreeWayFlag("feature") + .on(true) + .prerequisites(prerequisite("prereq1", GREEN_VARIATION), prerequisite("prereq2", GREEN_VARIATION), prerequisite("prereq3", GREEN_VARIATION)) + .build(); + FeatureFlag f1 = buildRedGreenFlag("prereq1") + .on(true) + .fallthroughVariation(GREEN_VARIATION) + .build(); + FeatureFlag f2 = buildRedGreenFlag("prereq2") + .on(true) + .fallthroughVariation(RED_VARIATION) + .build(); + FeatureFlag f3 = buildRedGreenFlag("prereq3") + .on(true) + .fallthroughVariation(GREEN_VARIATION) + .build(); + + Evaluator e = evaluatorBuilder().withStoredFlags(f0, f1, f2, f3).build(); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); + + assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, EvaluationReason.prerequisiteFailed("prereq2")), result); + assertEquals(2, Iterables.size(result.getPrerequisiteEvalRecords())); // prereq 1 and 2 are reached, but 2 fails, so 3 is not checked. + + PrerequisiteEvalRecord prereq1Eval = result.getPrerequisiteEvalRecords().get(0); + assertEquals(f1, prereq1Eval.flag); + assertEquals(f0, prereq1Eval.prereqOfFlag); + assertEquals(GREEN_VARIATION, prereq1Eval.result.getVariationIndex()); + assertEquals(GREEN_VALUE, prereq1Eval.result.getValue()); + + PrerequisiteEvalRecord prereq2Eval = result.getPrerequisiteEvalRecords().get(1); + assertEquals(f2, prereq2Eval.flag); + assertEquals(f0, prereq2Eval.prereqOfFlag); + assertEquals(RED_VARIATION, prereq2Eval.result.getVariationIndex()); + assertEquals(RED_VALUE, prereq2Eval.result.getValue()); + } + @Test public void prerequisiteCycleDetection() { for (int depth = 1; depth <= 4; depth++) { diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java index 0ecafe1..217fd77 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java @@ -9,8 +9,6 @@ import com.launchdarkly.sdk.server.DataModel.RolloutKind; import com.launchdarkly.sdk.server.DataModel.VariationOrRollout; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; -import com.launchdarkly.sdk.server.EvaluatorTestUtil.PrereqEval; -import com.launchdarkly.sdk.server.EvaluatorTestUtil.PrereqRecorder; import org.junit.Test; @@ -298,14 +296,13 @@ public void flagReturnsOffVariationAndEventIfPrerequisiteIsOff() throws Exceptio // note that even though it returns the desired variation, it is still off and therefore not a match .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); - PrereqRecorder recordPrereqs = new PrereqRecorder(); - EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); - assertEquals(1, Iterables.size(recordPrereqs.evals)); - PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(1, Iterables.size(result.getPrerequisiteEvalRecords())); + PrerequisiteEvalRecord eval = result.getPrerequisiteEvalRecords().get(0); assertEquals(f1, eval.flag); assertEquals(f0, eval.prereqOfFlag); assertEquals(GREEN_VARIATION, eval.result.getVariationIndex()); @@ -323,14 +320,13 @@ public void flagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() throws Excep .fallthroughVariation(RED_VARIATION) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); - PrereqRecorder recordPrereqs = new PrereqRecorder(); - EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); - assertEquals(1, Iterables.size(recordPrereqs.evals)); - PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(1, Iterables.size(result.getPrerequisiteEvalRecords())); + PrerequisiteEvalRecord eval = result.getPrerequisiteEvalRecords().get(0); assertEquals(f1, eval.flag); assertEquals(f0, eval.prereqOfFlag); assertEquals(RED_VARIATION, eval.result.getVariationIndex()); @@ -385,13 +381,12 @@ public void flagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAr .version(2) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); - PrereqRecorder recordPrereqs = new PrereqRecorder(); - EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); - assertEquals(1, Iterables.size(recordPrereqs.evals)); - PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(1, Iterables.size(result.getPrerequisiteEvalRecords())); + PrerequisiteEvalRecord eval = result.getPrerequisiteEvalRecords().get(0); assertEquals(f1, eval.flag); assertEquals(f0, eval.prereqOfFlag); assertEquals(GREEN_VARIATION, eval.result.getVariationIndex()); @@ -414,20 +409,19 @@ public void multipleLevelsOfPrerequisitesProduceMultipleEvents() throws Exceptio .fallthroughVariation(GREEN_VARIATION) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1, f2).build(); - PrereqRecorder recordPrereqs = new PrereqRecorder(); - EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + EvalResult result = e.evaluate(f0, BASE_USER, new EvaluationRecorder(){}); assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); - assertEquals(2, Iterables.size(recordPrereqs.evals)); + assertEquals(2, Iterables.size(result.getPrerequisiteEvalRecords())); - PrereqEval eval0 = recordPrereqs.evals.get(0); + PrerequisiteEvalRecord eval0 = result.getPrerequisiteEvalRecords().get(0); assertEquals(f2, eval0.flag); assertEquals(f1, eval0.prereqOfFlag); assertEquals(GREEN_VARIATION, eval0.result.getVariationIndex()); assertEquals(GREEN_VALUE, eval0.result.getValue()); - PrereqEval eval1 = recordPrereqs.evals.get(1); + PrerequisiteEvalRecord eval1 = result.getPrerequisiteEvalRecords().get(1); assertEquals(f1, eval1.flag); assertEquals(f0, eval1.prereqOfFlag); assertEquals(GREEN_VARIATION, eval1.result.getVariationIndex()); diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java index 33849be..304b2d0 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java @@ -137,28 +137,4 @@ public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfF } }; } - - public static final class PrereqEval { - public final FeatureFlag flag; - public final FeatureFlag prereqOfFlag; - public final LDContext context; - public final EvalResult result; - - public PrereqEval(FeatureFlag flag, FeatureFlag prereqOfFlag, LDContext context, EvalResult result) { - this.flag = flag; - this.prereqOfFlag = prereqOfFlag; - this.context = context; - this.result = result; - } - } - - public static final class PrereqRecorder implements EvaluationRecorder { - public final List evals = new ArrayList<>(); - - @Override - public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDContext context, - EvalResult result) { - evals.add(new PrereqEval(flag, prereqOfFlag, context, result)); - } - } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java index 2da3a57..9ccab64 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java @@ -177,8 +177,9 @@ public void canConvertToJson() { @Test public void canConvertFromJson() throws SerializationException { + FeatureFlagsState expectedState = makeInstanceForSerialization(); FeatureFlagsState state = JsonSerialization.deserialize(makeExpectedJsonSerialization(), FeatureFlagsState.class); - assertEquals(makeInstanceForSerialization(), state); + assertEquals(expectedState, state); } private static FeatureFlagsState makeInstanceForSerialization() { diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java index 5dabd06..30080e6 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java @@ -676,14 +676,32 @@ public void allFlagStateIncludesPrerequisites() throws Exception { DataModel.FeatureFlag flag3 = flagBuilder("flagABC") .version(3) .on(true) - .variations(LDValue.of("x"), LDValue.of("value3")) + .variations(LDValue.of("off"), LDValue.of("value3")) .fallthrough(fallthroughVariation(0)) .prerequisites(prerequisite("flagAB", 0)) .trackEvents(false) .build(); + DataModel.FeatureFlag flag4 = flagBuilder("flagAD") + .version(4) + .on(true) + .variations(LDValue.of("off"), LDValue.of("value4")) + .fallthrough(fallthroughVariation(0)) + .prerequisites(prerequisite("flagA", 0)) + .trackEvents(false) + .build(); + DataModel.FeatureFlag flagTwoPrereqs = flagBuilder("flagTwoPrereqs") + .version(4) + .on(true) + .variations(LDValue.of("off"), LDValue.of("value5")) + .fallthrough(fallthroughVariation(0)) + .prerequisites(prerequisite("flagA", 0), prerequisite("flagAB", 0)) + .trackEvents(false) + .build(); upsertFlag(dataStore, flag1); upsertFlag(dataStore, flag2); upsertFlag(dataStore, flag3); + upsertFlag(dataStore, flag4); + upsertFlag(dataStore, flagTwoPrereqs); FeatureFlagsState state = client.allFlagsState(context); assertTrue(state.isValid()); @@ -692,8 +710,12 @@ public void allFlagStateIncludesPrerequisites() throws Exception { String expectedPart1 = "{\"$flagsState\":{\"flagA\":{}}}"; String expectedPart2 = "{\"$flagsState\":{\"flagAB\":{\"prerequisites\":[\"flagA\"]}}}"; String expectedPart3 = "{\"$flagsState\":{\"flagABC\":{\"prerequisites\":[\"flagAB\"]}}}"; + String expectedPart4 = "{\"$flagsState\":{\"flagAD\":{\"prerequisites\":[\"flagA\"]}}}"; + String expectedPart5 = "{\"$flagsState\":{\"flagTwoPrereqs\":{\"prerequisites\":[\"flagA\",\"flagAB\"]}}}"; assertJsonIncludes(expectedPart1, outputJson); assertJsonIncludes(expectedPart2, outputJson); assertJsonIncludes(expectedPart3, outputJson); + assertJsonIncludes(expectedPart4, outputJson); + assertJsonIncludes(expectedPart5, outputJson); } } From 26aeab0e895504cd6a8963c1f796b2ab6901d2d2 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Wed, 23 Oct 2024 15:27:09 -0500 Subject: [PATCH 5/5] reverting Version.java change --- .../src/main/java/com/launchdarkly/sdk/server/Version.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java index 2730275..880687e 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/Version.java @@ -4,5 +4,7 @@ abstract class Version { private Version() {} // This constant is updated automatically by our Gradle script during a release, if the project version has changed + // x-release-please-start-version static final String SDK_VERSION = "7.5.0"; + // x-release-please-end }