From df98fecfa40327066d05ea27b7c670decc1af1c6 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot <86431345+LaunchDarklyReleaseBot@users.noreply.github.com> Date: Wed, 20 Jul 2022 20:53:30 -0400 Subject: [PATCH] prepare 5.9.2 release (#270) * (5.0) final test coverage improvements, for now, with enforcement * re-simplify DataBuilder * increase timeouts * misc fixes * rm unnecessary override * indents * update benchmark code for API change * support loading file data from a classpath resource * update metadata so Releaser knows about 4.x branch * minor test fixes * make class final * rm beta changelog items * test data source * more info about coverage in CONTRIBUTING.md * misc fixes/tests * use java-sdk-common 1.0.0 * use okhttp-eventsource 2.3.0 * use okhttp-eventsource 2.3.1 for thread fix * fix flaky tests due to change in EventSource error reporting * remove support for indirect put and indirect patch * fix typo in javadoc example code * clean up polling logic, fix status updating after an outage, don't reinit store unnecessarily (#256) * slightly change semantics of boolean setters, improve tests, misc cleanup * avoid NPEs if LDUser was deserialized by Gson (#257) * avoid NPEs if LDUser was deserialized by Gson * add test * fix release metadata * prepare 4.14.1 release (#200) * Releasing version 4.14.1 * exclude Kotlin metadata from jar + fix misc Gradle problems * update CI and Gradle to test with newer JDKs (#259) * update okhttp to 3.14.9 (fixes incompatibility with OpenJDK 8.0.252) * prepare 4.14.2 release (#205) * Releasing version 4.14.2 * update okhttp to 4.8.1 (fixes incompatibility with OpenJDK 8.0.252) * gitignore * Bump SnakeYAML from 1.19 to 1.26 to address CVE-2017-18640 * prepare 4.14.3 release (#209) * Releasing version 4.14.3 * comments * only log initialization message once in polling mode * [ch89935] Correct some logging call format strings (#264) Also adds debug logs for full exception information in a couple locations. * [ch90109] Remove outdated trackMetric comment from before service support. (#265) * Fix compatibility with Java 7. * Remove import that is no longer used. * add Java 7 build (#267) * prepare 4.14.4 release (#214) * Releasing version 4.14.4 * add and use getSocketFactory * alignment * add socketFactory to builder * test socket factory builder * preserve dummy CI config file when pushing to gh-pages (#271) * fix concatenation when base URI has a context path (#270) * fix shaded jar builds to exclude Jackson classes and not modify Jackson return types (#268) * add test httpClientCanUseCustomSocketFactory for DefaultFeatureRequestor * add httpClientCanUseCustomSocketFactory() test for DefaultEventSenderTest * add httpClientCanUseCustomSocketFactory() test to StreamProcessorTest * pass URI to in customSocketFactory event test * make test less ambiguous * copy rules to new FlagBuilder instances (#273) * Bump guava version (#274) * Removed the guides link * increment versions when loading file data, so FlagTracker will work (#275) * increment versions when loading file data, so FlagTracker will work * update doc comment about flag change events with file data * add ability to ignore duplicate keys in file data (#276) * add alias events (#278) * add alias events and function * update tests for new functionality * update javadoc strings * add validation of javadoc build to CI * update commons-codec to 1.15 (#279) * Add support for experiment rollouts * add tests and use seed for allocating user to partition * test serialization and add check for isExperiment * fix PollingProcessorTest test race condition + other test issues (#282) * use launchdarkly-java-sdk-common 1.1.0-alpha-expalloc.2 * Update src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java Co-authored-by: Sam Stokes * Update src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java Co-authored-by: Sam Stokes * Update src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java Co-authored-by: Sam Stokes * Update src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java Co-authored-by: Sam Stokes * changes per code review comments * Please enter the commit message for your changes. Lines starting * fix null pointer exception * address code review comments * address more comments * missed a ! for isUntracked() * fix default boolean for json * make untracked FALSE by default * refactoring of bucketing logic to remove the need for an extra result object (#283) * add comment to enum * various JSON fixes, update common-sdk (#284) * simlpify the logic and make it match node/.Net sdks * Update src/main/java/com/launchdarkly/sdk/server/EventFactory.java Co-authored-by: Sam Stokes * add the same comment as the Node SDK * Remove outdated/meaningless doc comment. (#286) * protect against NPEs if flag/segment JSON contains a null value * use java-sdk-common 1.2.0 * fix Jackson-related build issues (again) (#288) * update to okhttp-eventsource patch for stream retry bug, improve tests (#289) * update to okhttp-eventsource patch for stream retry bug, improve test * add test for appropriate stream retry * add public builder for FeatureFlagsState (#290) * add public builder for FeatureFlagsState * javadoc fixes * clarify FileData doc comment to say you shouldn't use offline mode (#291) * improve validation of SDK key so we won't throw an exception that contains the key (#293) * fix javadoc link in FileData comment (#294) * fix PollingProcessor 401 behavior and use new HTTP test helpers (#292) * re-fix metadata to remove Jackson dependencies, also remove Class-Path from manifest (#295) * make FeatureFlagsState.Builder.build() public (#297) * clean up tests using java-test-helpers 1.1.0 (#296) * use Releaser v2 config + newer CI images (#298) * [ch123129] Fix `PollingDataSourceBuilder` example. (#299) * Updates docs URLs * always use US locale when parsing HTTP dates * use Gson 2.8.9 * don't try to send more diagnostic events after an unrecoverable HTTP error * ensure module-info file isn't copied into our jars during build * use Gradle 7 * update build for benchmarks * more Gradle 7 compatibility changes for benchmark job * test with Java 17 in CI (#307) * test with Java 17 in CI * also test in Java 17 for Windows * fix choco install command * do date comparisons as absolute times, regardless of time zone (#310) * fix suppression of nulls in JSON representations (#311) * fix suppression of nulls in JSON representations * distinguish between situations where we do or do not want to suppress nulls * fix identify/track null user key check, also don't create index event for alias * use latest java-sdk-common * fix setting of trackEvents/trackReason in allFlagsState data when there's an experiment * implement contract tests (#314) * Merge Big Segments feature branch for 5.7.0 release (#316) Includes Big Segments implementation and contract test support for the new behavior. * Fix for pom including SDK common library as a dependency. (#317) * Upload JUnit XML to CircleCI on failure (#320) Fix a bug in the CircleCI config that was only uploading JUnit XML on _success_, not failure. * Add application tag support (#319) * Enforce 64 character limit on application tag values (#323) * fix "wrong type" logic in evaluations when default value is null * Rename master to main in .ldrelease/config.yml (#325) * Simpler way of setting base URIs in Java (#322) Now supports the `ServiceEndpoints` config for setting custom URIs for endpoints in a single place * make BigSegmentStoreWrapper.pollingDetectsStaleStatus test less timing-sensitive * make LDEndToEndClientTest.test____SpecialHttpConfigurations less timing-sensitive * make data source status tests less timing-sensitive * use streaming JSON parsing for incoming LD data * fix tests * rm unused * rm unused * use okhttp-eventsource 2.6.0 * update eventsource to 2.6.1 to fix pom/manifest problem * increase efficiency of summary event data structures (#335) * make reusable EvaluationDetail instances as part of flag preprocessing (#336) * make evaluator result object immutable and reuse instances * comment * avoid creating List iterators during evaluations * remove unnecessary copy * fix allFlagsState to not generate prereq eval events Co-authored-by: Eli Bishop Co-authored-by: LaunchDarklyCI Co-authored-by: LaunchDarklyCI Co-authored-by: Gavin Whelan Co-authored-by: ssrm Co-authored-by: Harpo Roeder Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: Elliot <35050275+Apache-HB@users.noreply.github.com> Co-authored-by: Robert J. Neal Co-authored-by: Robert J. Neal Co-authored-by: Sam Stokes Co-authored-by: LaunchDarklyReleaseBot Co-authored-by: Ember Stevens Co-authored-by: ember-stevens <79482775+ember-stevens@users.noreply.github.com> Co-authored-by: Alex Engelberg Co-authored-by: Alex Engelberg --- .../launchdarkly/sdk/server/DataModel.java | 79 ++-- .../sdk/server/DataModelPreprocessing.java | 271 ++++++++++++++ .../launchdarkly/sdk/server/EvalResult.java | 256 +++++++++++++ .../launchdarkly/sdk/server/Evaluator.java | 345 +++++++++--------- .../sdk/server/EvaluatorHelpers.java | 67 ++++ .../sdk/server/EvaluatorOperators.java | 7 +- .../sdk/server/EvaluatorPreprocessing.java | 162 -------- .../launchdarkly/sdk/server/EventFactory.java | 61 +--- .../sdk/server/FeatureFlagsState.java | 9 +- .../com/launchdarkly/sdk/server/LDClient.java | 83 +++-- ...t.java => DataModelPreprocessingTest.java} | 159 +++++++- .../DefaultEventProcessorOutputTest.java | 2 +- .../sdk/server/EvalResultTest.java | 157 ++++++++ .../sdk/server/EvaluatorBigSegmentTest.java | 32 +- .../sdk/server/EvaluatorBucketingTest.java | 6 +- .../sdk/server/EvaluatorClauseTest.java | 5 +- .../EvaluatorOperatorsParameterizedTest.java | 2 +- .../sdk/server/EvaluatorRuleTest.java | 54 ++- .../sdk/server/EvaluatorSegmentMatchTest.java | 3 +- .../sdk/server/EvaluatorTest.java | 239 ++++++------ .../sdk/server/EvaluatorTestUtil.java | 34 ++ .../sdk/server/EventFactoryTest.java | 135 ++++--- .../sdk/server/EventOutputTest.java | 16 +- .../sdk/server/FeatureFlagsStateTest.java | 6 +- .../server/FlagModelDeserializationTest.java | 61 +++- .../sdk/server/LDClientEventTest.java | 48 +++ .../sdk/server/ModelBuilders.java | 9 + .../RolloutRandomizationConsistencyTest.java | 4 +- .../com/launchdarkly/sdk/server/TestUtil.java | 10 +- 29 files changed, 1599 insertions(+), 723 deletions(-) create mode 100644 src/main/java/com/launchdarkly/sdk/server/DataModelPreprocessing.java create mode 100644 src/main/java/com/launchdarkly/sdk/server/EvalResult.java create mode 100644 src/main/java/com/launchdarkly/sdk/server/EvaluatorHelpers.java delete mode 100644 src/main/java/com/launchdarkly/sdk/server/EvaluatorPreprocessing.java rename src/test/java/com/launchdarkly/sdk/server/{EvaluatorPreprocessingTest.java => DataModelPreprocessingTest.java} (52%) create mode 100644 src/test/java/com/launchdarkly/sdk/server/EvalResultTest.java diff --git a/src/main/java/com/launchdarkly/sdk/server/DataModel.java b/src/main/java/com/launchdarkly/sdk/server/DataModel.java index 37ac88987..fb32db264 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DataModel.java +++ b/src/main/java/com/launchdarkly/sdk/server/DataModel.java @@ -2,9 +2,13 @@ import com.google.common.collect.ImmutableList; import com.google.gson.annotations.JsonAdapter; -import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed; +import com.launchdarkly.sdk.server.DataModelPreprocessing.FlagPreprocessed; +import com.launchdarkly.sdk.server.DataModelPreprocessing.FlagRulePreprocessed; +import com.launchdarkly.sdk.server.DataModelPreprocessing.PrerequisitePreprocessed; +import com.launchdarkly.sdk.server.DataModelPreprocessing.TargetPreprocessed; import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; @@ -15,6 +19,35 @@ import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; +// IMPLEMENTATION NOTES: +// +// - FeatureFlag, Segment, and all other data model classes contained within them, must be package-private. +// We don't want application code to see these types, because we need to be free to change their details without +// breaking the application. +// +// - We expose our DataKind instances publicly because application code may need to reference them if it is +// implementing a custom component such as a data store. But beyond the mere fact of there being these kinds of +// data, applications should not be considered with their structure. +// +// - For all classes that can be deserialized from JSON, there must be an empty constructor, and the fields +// cannot be final. This is because of how Gson works: it creates an instance first, then sets the fields. If +// we are able to move away from using Gson reflective deserialization in the future, we can make them final. +// +// - There should also be a constructor that takes all the fields; we should use that whenever we need to +// create these objects programmatically (so that if we are able at some point to make the fields final, that +// won't break anything). +// +// - For properties that have a collection type such as List, the getter method should always include a null +// guard and return an empty collection if the field is null (so that we don't have to worry about null guards +// every time we might want to iterate over these collections). Semantically there is no difference in the data +// model between an empty list and a null list, and in some languages (particularly Go) it is easy for an +// uninitialized list to be serialized to JSON as null. +// +// - Some classes have a "preprocessed" field containing types defined in DataModelPreprocessing. These fields +// must always be marked transient, so Gson will not serialize them. They are populated when we deserialize a +// FeatureFlag or Segment, because those types implement JsonHelpers.PostProcessingDeserializable (the +// afterDeserialized() method). + /** * Contains information about the internal data model for feature flags and user segments. *

@@ -104,6 +137,8 @@ static final class FeatureFlag implements VersionedData, JsonHelpers.PostProcess private Long debugEventsUntilDate; private boolean deleted; + transient FlagPreprocessed preprocessed; + // We need this so Gson doesn't complain in certain java environments that restrict unsafe allocation FeatureFlag() {} @@ -191,9 +226,8 @@ boolean isClientSide() { return clientSide; } - // Precompute some invariant values for improved efficiency during evaluations - called from JsonHelpers.PostProcessingDeserializableTypeAdapter public void afterDeserialized() { - EvaluatorPreprocessing.preprocessFlag(this); + DataModelPreprocessing.preprocessFlag(this); } } @@ -201,7 +235,7 @@ static final class Prerequisite { private String key; private int variation; - private transient EvaluationReason prerequisiteFailedReason; + transient PrerequisitePreprocessed preprocessed; Prerequisite() {} @@ -217,21 +251,14 @@ String getKey() { int getVariation() { return variation; } - - // This value is precomputed when we deserialize a FeatureFlag from JSON - EvaluationReason getPrerequisiteFailedReason() { - return prerequisiteFailedReason; - } - - void setPrerequisiteFailedReason(EvaluationReason prerequisiteFailedReason) { - this.prerequisiteFailedReason = prerequisiteFailedReason; - } } static final class Target { private Set values; private int variation; + transient TargetPreprocessed preprocessed; + Target() {} Target(Set values, int variation) { @@ -259,7 +286,7 @@ static final class Rule extends VariationOrRollout { private List clauses; private boolean trackEvents; - private transient EvaluationReason ruleMatchReason; + transient FlagRulePreprocessed preprocessed; Rule() { super(); @@ -284,15 +311,6 @@ List getClauses() { boolean isTrackEvents() { return trackEvents; } - - // This value is precomputed when we deserialize a FeatureFlag from JSON - EvaluationReason getRuleMatchReason() { - return ruleMatchReason; - } - - void setRuleMatchReason(EvaluationReason ruleMatchReason) { - this.ruleMatchReason = ruleMatchReason; - } } static final class Clause { @@ -301,9 +319,7 @@ static final class Clause { private List values; //interpreted as an OR of values private boolean negate; - // The following property is marked transient because it is not to be serialized or deserialized; - // it is (if necessary) precomputed in FeatureFlag.afterDeserialized() to speed up evaluations. - transient EvaluatorPreprocessing.ClauseExtra preprocessed; + transient ClausePreprocessed preprocessed; Clause() { } @@ -331,14 +347,6 @@ List getValues() { boolean isNegate() { return negate; } - - EvaluatorPreprocessing.ClauseExtra getPreprocessed() { - return preprocessed; - } - - void setPreprocessed(EvaluatorPreprocessing.ClauseExtra preprocessed) { - this.preprocessed = preprocessed; - } } static final class Rollout { @@ -508,9 +516,8 @@ public Integer getGeneration() { return generation; } - // Precompute some invariant values for improved efficiency during evaluations - called from JsonHelpers.PostProcessingDeserializableTypeAdapter public void afterDeserialized() { - EvaluatorPreprocessing.preprocessSegment(this); + DataModelPreprocessing.preprocessSegment(this); } } diff --git a/src/main/java/com/launchdarkly/sdk/server/DataModelPreprocessing.java b/src/main/java/com/launchdarkly/sdk/server/DataModelPreprocessing.java new file mode 100644 index 000000000..af49227db --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/DataModelPreprocessing.java @@ -0,0 +1,271 @@ +package com.launchdarkly.sdk.server; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.EvaluationReason.ErrorKind; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.Clause; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Operator; +import com.launchdarkly.sdk.server.DataModel.Prerequisite; +import com.launchdarkly.sdk.server.DataModel.Rule; +import com.launchdarkly.sdk.server.DataModel.Segment; +import com.launchdarkly.sdk.server.DataModel.SegmentRule; +import com.launchdarkly.sdk.server.DataModel.Target; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; + +/** + * Additional information that we attach to our data model to reduce the overhead of feature flag + * evaluations. The methods that create these objects are called by the afterDeserialized() methods + * of FeatureFlag and Segment, after those objects have been deserialized from JSON but before they + * have been made available to any other code (so these methods do not need to be thread-safe). + *

+ * If for some reason these methods have not been called before an evaluation happens, the evaluation + * logic must still be able to work without the precomputed data. + */ +abstract class DataModelPreprocessing { + private DataModelPreprocessing() {} + + static final class EvalResultsForSingleVariation { + private final EvalResult regularResult; + private final EvalResult inExperimentResult; + + EvalResultsForSingleVariation( + LDValue value, + int variationIndex, + EvaluationReason regularReason, + EvaluationReason inExperimentReason, + boolean alwaysInExperiment + ) { + this.regularResult = EvalResult.of(value, variationIndex, regularReason).withForceReasonTracking(alwaysInExperiment); + this.inExperimentResult = EvalResult.of(value, variationIndex, inExperimentReason).withForceReasonTracking(true); + } + + EvalResult getResult(boolean inExperiment) { + return inExperiment ? inExperimentResult : regularResult; + } + } + + static final class EvalResultFactoryMultiVariations { + private final ImmutableList variations; + + EvalResultFactoryMultiVariations( + ImmutableList variations + ) { + this.variations = variations; + } + + EvalResult forVariation(int index, boolean inExperiment) { + if (index < 0 || index >= variations.size()) { + return EvalResult.error(ErrorKind.MALFORMED_FLAG); + } + return variations.get(index).getResult(inExperiment); + } + } + + static final class FlagPreprocessed { + EvalResult offResult; + EvalResultFactoryMultiVariations fallthroughResults; + + FlagPreprocessed(EvalResult offResult, + EvalResultFactoryMultiVariations fallthroughResults) { + this.offResult = offResult; + this.fallthroughResults = fallthroughResults; + } + } + + static final class PrerequisitePreprocessed { + final EvalResult prerequisiteFailedResult; + + PrerequisitePreprocessed(EvalResult prerequisiteFailedResult) { + this.prerequisiteFailedResult = prerequisiteFailedResult; + } + } + + static final class TargetPreprocessed { + final EvalResult targetMatchResult; + + TargetPreprocessed(EvalResult targetMatchResult) { + this.targetMatchResult = targetMatchResult; + } + } + + static final class FlagRulePreprocessed { + final EvalResultFactoryMultiVariations allPossibleResults; + + FlagRulePreprocessed( + EvalResultFactoryMultiVariations allPossibleResults + ) { + this.allPossibleResults = allPossibleResults; + } + } + + static final class ClausePreprocessed { + final Set valuesSet; + final List valuesExtra; + + ClausePreprocessed(Set valuesSet, List valuesExtra) { + this.valuesSet = valuesSet; + this.valuesExtra = valuesExtra; + } + + static final class ValueData { + final Instant parsedDate; + final Pattern parsedRegex; + final SemanticVersion parsedSemVer; + + ValueData(Instant parsedDate, Pattern parsedRegex, SemanticVersion parsedSemVer) { + this.parsedDate = parsedDate; + this.parsedRegex = parsedRegex; + this.parsedSemVer = parsedSemVer; + } + } + } + + static void preprocessFlag(FeatureFlag f) { + f.preprocessed = new FlagPreprocessed( + EvaluatorHelpers.offResult(f), + precomputeMultiVariationResults(f, EvaluationReason.fallthrough(false), + EvaluationReason.fallthrough(true), f.isTrackEventsFallthrough()) + ); + + for (Prerequisite p: f.getPrerequisites()) { + preprocessPrerequisite(p, f); + } + for (Target t: f.getTargets()) { + preprocessTarget(t, f); + } + List rules = f.getRules(); + int n = rules.size(); + for (int i = 0; i < n; i++) { + preprocessFlagRule(rules.get(i), i, f); + } + preprocessValueList(f.getVariations()); + } + + static void preprocessSegment(Segment s) { + List rules = s.getRules(); + int n = rules.size(); + for (int i = 0; i < n; i++) { + preprocessSegmentRule(rules.get(i), i); + } + } + + static void preprocessPrerequisite(Prerequisite p, FeatureFlag f) { + // Precompute an immutable EvaluationDetail instance that will be used if the prerequisite fails. + // This behaves the same as an "off" result except for the reason. + p.preprocessed = new PrerequisitePreprocessed(EvaluatorHelpers.prerequisiteFailedResult(f, p)); + } + + static void preprocessTarget(Target t, FeatureFlag f) { + // Precompute an immutable EvalResult instance that will be used if this target matches. + t.preprocessed = new TargetPreprocessed(EvaluatorHelpers.targetMatchResult(f, t)); + } + + static void preprocessFlagRule(Rule r, int ruleIndex, FeatureFlag f) { + EvaluationReason ruleMatchReason = EvaluationReason.ruleMatch(ruleIndex, r.getId(), false); + EvaluationReason ruleMatchReasonInExperiment = EvaluationReason.ruleMatch(ruleIndex, r.getId(), true); + r.preprocessed = new FlagRulePreprocessed(precomputeMultiVariationResults(f, + ruleMatchReason, ruleMatchReasonInExperiment, r.isTrackEvents())); + + for (Clause c: r.getClauses()) { + preprocessClause(c); + } + } + + static void preprocessSegmentRule(SegmentRule r, int ruleIndex) { + for (Clause c: r.getClauses()) { + preprocessClause(c); + } + } + + static void preprocessClause(Clause c) { + // If the clause values contain a null (which is valid in terms of the JSON schema, even if it + // can't ever produce a true result), Gson will give us an actual null. Change this to + // LDValue.ofNull() to avoid NPEs down the line. It's more efficient to do this just once at + // deserialization time than to do it in every clause match. + List values = c.getValues(); + preprocessValueList(values); + + Operator op = c.getOp(); + if (op == null) { + return; + } + switch (op) { + case in: + // This is a special case where the clause is testing for an exact match against any of the + // clause values. Converting the value list to a Set allows us to do a fast lookup instead of + // a linear search. We do not do this for other operators (or if there are fewer than two + // values) because the slight extra overhead of a Set is not worthwhile in those case. + if (values.size() > 1) { + c.preprocessed = new ClausePreprocessed(ImmutableSet.copyOf(values), null); + } + break; + case matches: + c.preprocessed = preprocessClauseValues(c.getValues(), v -> + new ClausePreprocessed.ValueData(null, EvaluatorTypeConversion.valueToRegex(v), null) + ); + break; + case after: + case before: + c.preprocessed = preprocessClauseValues(c.getValues(), v -> + new ClausePreprocessed.ValueData(EvaluatorTypeConversion.valueToDateTime(v), null, null) + ); + break; + case semVerEqual: + case semVerGreaterThan: + case semVerLessThan: + c.preprocessed = preprocessClauseValues(c.getValues(), v -> + new ClausePreprocessed.ValueData(null, null, EvaluatorTypeConversion.valueToSemVer(v)) + ); + break; + default: + break; + } + } + + static void preprocessValueList(List values) { + // If a list of values contains a null (which is valid in terms of the JSON schema, even if it + // isn't useful because the SDK considers this a non-value), Gson will give us an actual null. + // Change this to LDValue.ofNull() to avoid NPEs down the line. It's more efficient to do this + // just once at deserialization time than to do it in every clause match. + for (int i = 0; i < values.size(); i++) { + if (values.get(i) == null) { + values.set(i, LDValue.ofNull()); + } + } + } + + private static ClausePreprocessed preprocessClauseValues( + List values, + Function f + ) { + List valuesExtra = new ArrayList<>(values.size()); + for (LDValue v: values) { + valuesExtra.add(f.apply(v)); + } + return new ClausePreprocessed(null, valuesExtra); + } + + private static EvalResultFactoryMultiVariations precomputeMultiVariationResults( + FeatureFlag f, + EvaluationReason regularReason, + EvaluationReason inExperimentReason, + boolean alwaysInExperiment + ) { + ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(f.getVariations().size()); + for (int i = 0; i < f.getVariations().size(); i++) { + builder.add(new EvalResultsForSingleVariation(f.getVariations().get(i), i, + regularReason, inExperimentReason, alwaysInExperiment)); + } + return new EvalResultFactoryMultiVariations(builder.build()); + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/EvalResult.java b/src/main/java/com/launchdarkly/sdk/server/EvalResult.java new file mode 100644 index 000000000..cf772042b --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/EvalResult.java @@ -0,0 +1,256 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.EvaluationReason.ErrorKind; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.LDValueType; + +import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; + +/** + * Internal container for the results of an evaluation. This consists of: + *

    + *
  • an {@link EvaluationDetail} in a type-agnostic form using {@link LDValue} + *
  • if appropriate, an additional precomputed {@link EvaluationDetail} for specific Java types + * such as Boolean, so that calling a method like boolVariationDetail won't always have to create + * a new instance + *
  • the boolean forceReasonTracking property (see isForceReasonTracking) + */ +final class EvalResult { + private static final EvaluationDetail WRONG_TYPE_BOOLEAN = wrongTypeWithValue(false); + private static final EvaluationDetail WRONG_TYPE_INTEGER = wrongTypeWithValue((int)0); + private static final EvaluationDetail WRONG_TYPE_DOUBLE = wrongTypeWithValue((double)0); + private static final EvaluationDetail WRONG_TYPE_STRING = wrongTypeWithValue((String)null); + + private final EvaluationDetail anyType; + private final EvaluationDetail asBoolean; + private final EvaluationDetail asInteger; + private final EvaluationDetail asDouble; + private final EvaluationDetail asString; + private final boolean forceReasonTracking; + + /** + * Constructs an instance that wraps the specified EvaluationDetail and also precomputes + * any appropriate type-specific variants (asBoolean, etc.). + * + * @param original the original value + * @return an EvaluatorResult + */ + static EvalResult of(EvaluationDetail original) { + return new EvalResult(original); + } + + /** + * Same as {@link #of(EvaluationDetail)} but specifies the individual properties. + * + * @param value the value + * @param variationIndex the variation index + * @param reason the evaluation reason + * @return an EvaluatorResult + */ + static EvalResult of(LDValue value, int variationIndex, EvaluationReason reason) { + return of(EvaluationDetail.fromValue(value, variationIndex, reason)); + } + + /** + * Constructs an instance for an error result. The value is always null in this case because + * this is a generalized result that wasn't produced by an individual variation() call, so + * we do not know what the application might specify as a default value. + * + * @param errorKind the error kind + * @return an instance + */ + static EvalResult error(ErrorKind errorKind) { + return of(LDValue.ofNull(), EvaluationDetail.NO_VARIATION, EvaluationReason.error(errorKind)); + } + + private EvalResult(EvaluationDetail original) { + this.anyType = original.getValue() == null ? + EvaluationDetail.fromValue(LDValue.ofNull(), original.getVariationIndex(), original.getReason()) : + original; + this.forceReasonTracking = original.getReason().isInExperiment(); + + LDValue value = anyType.getValue(); + int index = anyType.getVariationIndex(); + EvaluationReason reason = anyType.getReason(); + + this.asBoolean = value.getType() == LDValueType.BOOLEAN ? + EvaluationDetail.fromValue(Boolean.valueOf(value.booleanValue()), index, reason) : + WRONG_TYPE_BOOLEAN; + this.asInteger = value.isNumber() ? + EvaluationDetail.fromValue(Integer.valueOf(value.intValue()), index, reason) : + WRONG_TYPE_INTEGER; + this.asDouble = value.isNumber() ? + EvaluationDetail.fromValue(Double.valueOf(value.doubleValue()), index, reason) : + WRONG_TYPE_DOUBLE; + this.asString = value.isString() || value.isNull() ? + EvaluationDetail.fromValue(value.stringValue(), index, reason) : + WRONG_TYPE_STRING; + } + + private EvalResult(EvalResult from, EvaluationReason newReason) { + this.anyType = transformReason(from.anyType, newReason); + this.asBoolean = transformReason(from.asBoolean, newReason); + this.asInteger = transformReason(from.asInteger, newReason); + this.asDouble = transformReason(from.asDouble, newReason); + this.asString = transformReason(from.asString, newReason); + this.forceReasonTracking = from.forceReasonTracking; + } + + private EvalResult(EvalResult from, boolean newForceTracking) { + this.anyType = from.anyType; + this.asBoolean = from.asBoolean; + this.asInteger = from.asInteger; + this.asDouble = from.asDouble; + this.asString = from.asString; + this.forceReasonTracking = newForceTracking; + } + + /** + * Returns the result as an {@code EvaluationDetail} where the value is an {@code LDValue}, + * allowing it to be of any JSON type. + * + * @return the result properties + */ + public EvaluationDetail getAnyType() { + return anyType; + } + + /** + * Returns the result as an {@code EvaluationDetail} where the value is a {@code Boolean}. + * If the result was not a boolean, the returned object has a value of false and a reason + * that is a {@code WRONG_TYPE} error. + *

    + * Note: the "wrong type" logic is just a safety measure to ensure that we never return + * null. Normally, the result will already have been transformed by LDClient.evaluateInternal + * if the wrong type was requested. + * + * @return the result properties + */ + public EvaluationDetail getAsBoolean() { + return asBoolean; + } + + /** + * Returns the result as an {@code EvaluationDetail} where the value is an {@code Integer}. + * If the result was not a number, the returned object has a value of zero and a reason + * that is a {@code WRONG_TYPE} error (see {@link #getAsBoolean()}). + * + * @return the result properties + */ + public EvaluationDetail getAsInteger() { + return asInteger; + } + + /** + * Returns the result as an {@code EvaluationDetail} where the value is a {@code Double}. + * If the result was not a number, the returned object has a value of zero and a reason + * that is a {@code WRONG_TYPE} error (see {@link #getAsBoolean()}). + * + * @return the result properties + */ + public EvaluationDetail getAsDouble() { + return asDouble; + } + + /** + * Returns the result as an {@code EvaluationDetail} where the value is a {@code String}. + * If the result was not a string, the returned object has a value of {@code null} and a + * reason that is a {@code WRONG_TYPE} error (see {@link #getAsBoolean()}). + * + * @return the result properties + */ + public EvaluationDetail getAsString() { + return asString; + } + + /** + * Returns the result value, which may be of any JSON type. + * @return the result value + */ + public LDValue getValue() { return anyType.getValue(); } + + /** + * Returns the variation index, or {@link EvaluationDetail#NO_VARIATION} if evaluation failed + * @return the variation index or {@link EvaluationDetail#NO_VARIATION} + */ + public int getVariationIndex() { return anyType.getVariationIndex(); } + + /** + * Returns the evaluation reason. This is never null, even though we may not always put the + * reason into events. + * @return the evaluation reason + */ + public EvaluationReason getReason() { return anyType.getReason(); } + + /** + * Returns true if the variation index is {@link EvaluationDetail#NO_VARIATION}, indicating + * that evaluation failed or at least that no variation was selected. + * @return true if there is no variation + */ + public boolean isNoVariation() { return anyType.isDefaultValue(); } + + /** + * Returns true if we need to send an evaluation reason in event data whenever we get this + * result. This is true if any of the following are true: 1. the evaluation reason's + * inExperiment property was true, which can happen if the evaluation involved a rollout + * or experiment; 2. the evaluation reason was FALLTHROUGH, and the flag's trackEventsFallthrough + * property was true; 3. the evaluation reason was RULE_MATCH, and the rule-level trackEvents + * property was true. The consequence is that we will tell the event processor "definitely send + * a individual event for this evaluation, even if the flag-level trackEvents was not true", + * and also we will include the evaluation reason in the event even if the application did not + * call a VariationDetail method. + * @return true if reason tracking is required for this result + */ + public boolean isForceReasonTracking() { return forceReasonTracking; } + + /** + * Returns a transformed copy of this EvalResult with a different evaluation reason. + * @param newReason the new evaluation reason + * @return a transformed copy + */ + public EvalResult withReason(EvaluationReason newReason) { + return newReason.equals(this.anyType.getReason()) ? this : new EvalResult(this, newReason); + } + + /** + * Returns a transformed copy of this EvalResult with a different value for {@link #isForceReasonTracking()}. + * @param newValue the new value for the property + * @return a transformed copy + */ + public EvalResult withForceReasonTracking(boolean newValue) { + return this.forceReasonTracking == newValue ? this : new EvalResult(this, newValue); + } + + @Override + public boolean equals(Object other) { + if (other instanceof EvalResult) { + EvalResult o = (EvalResult)other; + return anyType.equals(o.anyType) && forceReasonTracking == o.forceReasonTracking; + } + return false; + } + + @Override + public int hashCode() { + return anyType.hashCode() + (forceReasonTracking ? 1 : 0); + } + + @Override + public String toString() { + if (forceReasonTracking) { + return anyType.toString() + "(forceReasonTracking=true)"; + } + return anyType.toString(); + } + + private static EvaluationDetail transformReason(EvaluationDetail from, EvaluationReason newReason) { + return from == null ? null : + EvaluationDetail.fromValue(from.getValue(), from.getVariationIndex(), newReason); + } + + private static EvaluationDetail wrongTypeWithValue(T value) { + return EvaluationDetail.fromValue(value, NO_VARIATION, EvaluationReason.error(ErrorKind.WRONG_TYPE)); + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/Evaluator.java b/src/main/java/com/launchdarkly/sdk/server/Evaluator.java index d66818f0c..3459ab23b 100644 --- a/src/main/java/com/launchdarkly/sdk/server/Evaluator.java +++ b/src/main/java/com/launchdarkly/sdk/server/Evaluator.java @@ -1,32 +1,62 @@ package com.launchdarkly.sdk.server; -import com.google.common.collect.ImmutableList; -import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.EvaluationReason.Kind; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; -import com.launchdarkly.sdk.EvaluationReason.Kind; +import com.launchdarkly.sdk.server.DataModel.Clause; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Operator; +import com.launchdarkly.sdk.server.DataModel.Prerequisite; +import com.launchdarkly.sdk.server.DataModel.Rollout; +import com.launchdarkly.sdk.server.DataModel.Rule; +import com.launchdarkly.sdk.server.DataModel.Segment; +import com.launchdarkly.sdk.server.DataModel.SegmentRule; +import com.launchdarkly.sdk.server.DataModel.Target; +import com.launchdarkly.sdk.server.DataModel.VariationOrRollout; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; +import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes; -import com.launchdarkly.sdk.server.interfaces.Event; import org.slf4j.Logger; -import java.util.ArrayList; import java.util.List; import java.util.Set; -import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; import static com.launchdarkly.sdk.server.EvaluatorBucketing.bucketUser; /** * Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment; * if it needs to retrieve flags or segments that are referenced by a flag, it does so through a read-only interface - * that is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite - * flags, but does not send them. + * that is provided in the constructor. It also produces evaluation records (to be used in event data) as appropriate + * for any referenced prerequisite flags. */ class Evaluator { + // + // IMPLEMENTATION NOTES ABOUT THIS FILE + // + // Flag evaluation is the hottest code path in the SDK; large applications may evaluate flags at a VERY high + // volume, so every little bit of optimization we can achieve here could add up to quite a bit of overhead we + // are not making the customer incur. Strategies that are used here for that purpose include: + // + // 1. Whenever possible, we are reusing precomputed instances of EvalResult; see DataModelPreprocessing and + // EvaluatorHelpers. + // + // 2. If prerequisite evaluations happen as a side effect of an evaluation, rather than building and returning + // a list of these, we deliver them one at a time via the PrerequisiteEvaluationSink callback mechanism. + // + // 3. If there's a piece of state that needs to be tracked across multiple methods during an evaluation, and + // it's not feasible to just pass it as a method parameter, consider adding it as a field in the mutable + // EvaluatorState object (which we will always have one of) rather than creating a new object to contain it. + // + // 4. Whenever possible, avoid using "for (variable: list)" here because it always creates an iterator object. + // Instead, use the tedious old "get the size, iterate with a counter" approach. + // + // 5. Avoid using lambdas/closures here, because these generally cause a heap object to be allocated for + // variables captured in the closure each time they are used. + // + private final static Logger logger = Loggers.EVALUATION; /** @@ -44,80 +74,31 @@ class Evaluator { * and simplifies testing. */ static interface Getters { - DataModel.FeatureFlag getFlag(String key); - DataModel.Segment getSegment(String key); + FeatureFlag getFlag(String key); + Segment getSegment(String key); BigSegmentStoreWrapper.BigSegmentsQueryResult getBigSegments(String key); } /** - * Internal container for the results of an evaluation. This consists of the same information that is in an - * {@link EvaluationDetail}, plus a list of any feature request events generated by prerequisite flags. - * - * Unlike all the other simple data containers in the SDK, this is mutable. The reason is that flag evaluations - * may be done very frequently and we would like to minimize the amount of heap churn from intermediate objects, - * and Java does not support multiple return values as Go does, or value types as C# does. - * - * We never expose an EvalResult to application code and we never preserve a reference to it outside of a single - * xxxVariation() or xxxVariationDetail() call, so the risks from mutability are minimal. The only setter method - * that is accessible from outside of the Evaluator class is setValue(), which is exposed so that LDClient can - * replace null values with default values, + * An interface for the caller to receive information about prerequisite flags that were evaluated as a side + * effect of evaluating a flag. Evaluator pushes information to this object to avoid the overhead of building + * and returning lists of evaluation events. */ - static class EvalResult { - private LDValue value = LDValue.ofNull(); - private int variationIndex = NO_VARIATION; - private EvaluationReason reason = null; - private List prerequisiteEvents; - - public EvalResult(LDValue value, int variationIndex, EvaluationReason reason) { - this.value = value; - this.variationIndex = variationIndex; - this.reason = reason; - } - - public static EvalResult error(EvaluationReason.ErrorKind errorKind) { - return new EvalResult(LDValue.ofNull(), NO_VARIATION, EvaluationReason.error(errorKind)); - } - - LDValue getValue() { - return LDValue.normalize(value); - } - - void setValue(LDValue value) { - this.value = value; - } - - int getVariationIndex() { - return variationIndex; - } - - boolean isDefault() { - return variationIndex < 0; - } - - EvaluationReason getReason() { - return reason; - } - - EvaluationDetail getDetails() { - return EvaluationDetail.fromValue(LDValue.normalize(value), variationIndex, reason); - } - - Iterable getPrerequisiteEvents() { - return prerequisiteEvents == null ? ImmutableList.of() : prerequisiteEvents; - } - - private void setPrerequisiteEvents(List prerequisiteEvents) { - this.prerequisiteEvents = prerequisiteEvents; - } - - private void setBigSegmentsStatus(EvaluationReason.BigSegmentsStatus bigSegmentsStatus) { - this.reason = this.reason.withBigSegmentsStatus(bigSegmentsStatus); - } + static interface PrerequisiteEvaluationSink { + void recordPrerequisiteEvaluation( + FeatureFlag flag, + FeatureFlag prereqOfFlag, + LDUser user, + EvalResult result + ); } - - static class BigSegmentsState { - private BigSegmentStoreTypes.Membership bigSegmentsMembership; - private EvaluationReason.BigSegmentsStatus bigSegmentsStatus; + + /** + * This object holds mutable state that Evaluator may need during an evaluation. + */ + private static class EvaluatorState { + private BigSegmentStoreTypes.Membership bigSegmentsMembership = null; + private EvaluationReason.BigSegmentsStatus bigSegmentsStatus = null; } Evaluator(Getters getters) { @@ -132,7 +113,7 @@ static class BigSegmentsState { * @param eventFactory produces feature request events * @return an {@link EvalResult} - guaranteed non-null */ - EvalResult evaluate(DataModel.FeatureFlag flag, LDUser user, EventFactory eventFactory) { + EvalResult evaluate(FeatureFlag flag, LDUser user, PrerequisiteEvaluationSink prereqEvals) { if (flag.getKey() == INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION) { throw EXPECTED_EXCEPTION_FROM_INVALID_FLAG; } @@ -140,116 +121,109 @@ EvalResult evaluate(DataModel.FeatureFlag flag, LDUser user, EventFactory eventF if (user == null || user.getKey() == null) { // this should have been prevented by LDClient.evaluateInternal logger.warn("Null user or null user key when evaluating flag \"{}\"; returning null", flag.getKey()); - return new EvalResult(null, NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)); + return EvalResult.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED); } - BigSegmentsState bigSegmentsState = new BigSegmentsState(); - // If the flag doesn't have any prerequisites (which most flags don't) then it cannot generate any feature - // request events for prerequisites and we can skip allocating a List. - List prerequisiteEvents = flag.getPrerequisites().isEmpty() ? - null : new ArrayList(); // note, getPrerequisites() is guaranteed non-null - EvalResult result = evaluateInternal(flag, user, eventFactory, prerequisiteEvents, bigSegmentsState); - if (prerequisiteEvents != null) { - result.setPrerequisiteEvents(prerequisiteEvents); - } - if (bigSegmentsState.bigSegmentsStatus != null) { - result.setBigSegmentsStatus(bigSegmentsState.bigSegmentsStatus); + EvaluatorState state = new EvaluatorState(); + + EvalResult result = evaluateInternal(flag, user, prereqEvals, state); + + if (state.bigSegmentsStatus != null) { + return result.withReason( + result.getReason().withBigSegmentsStatus(state.bigSegmentsStatus) + ); } return result; } - private EvalResult evaluateInternal(DataModel.FeatureFlag flag, LDUser user, EventFactory eventFactory, - List eventsOut, BigSegmentsState bigSegmentsState) { + private EvalResult evaluateInternal(FeatureFlag flag, LDUser user, + PrerequisiteEvaluationSink prereqEvals, EvaluatorState state) { if (!flag.isOn()) { - return getOffValue(flag, EvaluationReason.off()); + return EvaluatorHelpers.offResult(flag); } - EvaluationReason prereqFailureReason = checkPrerequisites(flag, user, eventFactory, eventsOut, bigSegmentsState); - if (prereqFailureReason != null) { - return getOffValue(flag, prereqFailureReason); + EvalResult prereqFailureResult = checkPrerequisites(flag, user, prereqEvals, state); + if (prereqFailureResult != null) { + return prereqFailureResult; } // Check to see if targets match - for (DataModel.Target target: flag.getTargets()) { // getTargets() and getValues() are guaranteed non-null - if (target.getValues().contains(user.getKey())) { - return getVariation(flag, target.getVariation(), EvaluationReason.targetMatch()); + List targets = flag.getTargets(); // guaranteed non-null + int nTargets = targets.size(); + for (int i = 0; i < nTargets; i++) { + Target target = targets.get(i); + if (target.getValues().contains(user.getKey())) { // getValues() is guaranteed non-null + return EvaluatorHelpers.targetMatchResult(flag, target); } } + // Now walk through the rules and see if any match - List rules = flag.getRules(); // guaranteed non-null - for (int i = 0; i < rules.size(); i++) { - DataModel.Rule rule = rules.get(i); - if (ruleMatchesUser(flag, rule, user, bigSegmentsState)) { - EvaluationReason precomputedReason = rule.getRuleMatchReason(); - EvaluationReason reason = precomputedReason != null ? precomputedReason : EvaluationReason.ruleMatch(i, rule.getId()); - return getValueForVariationOrRollout(flag, rule, user, reason); + List rules = flag.getRules(); // guaranteed non-null + int nRules = rules.size(); + for (int i = 0; i < nRules; i++) { + Rule rule = rules.get(i); + if (ruleMatchesUser(flag, rule, user, state)) { + return computeRuleMatch(flag, user, rule, i); } } // Walk through the fallthrough and see if it matches - return getValueForVariationOrRollout(flag, flag.getFallthrough(), user, EvaluationReason.fallthrough()); + return getValueForVariationOrRollout(flag, flag.getFallthrough(), user, + flag.preprocessed == null ? null : flag.preprocessed.fallthroughResults, + EvaluationReason.fallthrough()); } - // Checks prerequisites if any; returns null if successful, or an EvaluationReason if we have to + // Checks prerequisites if any; returns null if successful, or an EvalResult if we have to // short-circuit due to a prerequisite failure. - private EvaluationReason checkPrerequisites(DataModel.FeatureFlag flag, LDUser user, EventFactory eventFactory, - List eventsOut, BigSegmentsState bigSegmentsState) { - for (DataModel.Prerequisite prereq: flag.getPrerequisites()) { // getPrerequisites() is guaranteed non-null + private EvalResult checkPrerequisites(FeatureFlag flag, LDUser user, + PrerequisiteEvaluationSink prereqEvals, EvaluatorState state) { + List prerequisites = flag.getPrerequisites(); // guaranteed non-null + int nPrerequisites = prerequisites.size(); + for (int i = 0; i < nPrerequisites; i++) { + Prerequisite prereq = prerequisites.get(i); boolean prereqOk = true; - DataModel.FeatureFlag prereqFeatureFlag = getters.getFlag(prereq.getKey()); + FeatureFlag prereqFeatureFlag = getters.getFlag(prereq.getKey()); if (prereqFeatureFlag == null) { logger.error("Could not retrieve prerequisite flag \"{}\" when evaluating \"{}\"", prereq.getKey(), flag.getKey()); prereqOk = false; } else { - EvalResult prereqEvalResult = evaluateInternal(prereqFeatureFlag, user, eventFactory, eventsOut, bigSegmentsState); + EvalResult prereqEvalResult = evaluateInternal(prereqFeatureFlag, user, prereqEvals, state); // Note that if the prerequisite flag is off, we don't consider it a match no matter what its // off variation was. But we still need to evaluate it in order to generate an event. if (!prereqFeatureFlag.isOn() || prereqEvalResult.getVariationIndex() != prereq.getVariation()) { prereqOk = false; } - // COVERAGE: currently eventsOut is never null because we preallocate the list in evaluate() if there are any prereqs - if (eventsOut != null) { - eventsOut.add(eventFactory.newPrerequisiteFeatureRequestEvent(prereqFeatureFlag, user, prereqEvalResult, flag)); + if (prereqEvals != null) { + prereqEvals.recordPrerequisiteEvaluation(prereqFeatureFlag, flag, user, prereqEvalResult); } } if (!prereqOk) { - EvaluationReason precomputedReason = prereq.getPrerequisiteFailedReason(); - return precomputedReason != null ? precomputedReason : EvaluationReason.prerequisiteFailed(prereq.getKey()); + return EvaluatorHelpers.prerequisiteFailedResult(flag, prereq); } } return null; } - private EvalResult getVariation(DataModel.FeatureFlag flag, int variation, EvaluationReason reason) { - List variations = flag.getVariations(); - if (variation < 0 || variation >= variations.size()) { - logger.error("Data inconsistency in feature flag \"{}\": invalid variation index", flag.getKey()); - return EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG); - } else { - return new EvalResult(variations.get(variation), variation, reason); - } - } - - private EvalResult getOffValue(DataModel.FeatureFlag flag, EvaluationReason reason) { - Integer offVariation = flag.getOffVariation(); - if (offVariation == null) { // off variation unspecified - return default value - return new EvalResult(null, NO_VARIATION, reason); - } else { - return getVariation(flag, offVariation, reason); - } - } - - private EvalResult getValueForVariationOrRollout(DataModel.FeatureFlag flag, DataModel.VariationOrRollout vr, LDUser user, EvaluationReason reason) { + private static EvalResult getValueForVariationOrRollout( + FeatureFlag flag, + VariationOrRollout vr, + LDUser user, + DataModelPreprocessing.EvalResultFactoryMultiVariations precomputedResults, + EvaluationReason reason + ) { int variation = -1; boolean inExperiment = false; Integer maybeVariation = vr.getVariation(); if (maybeVariation != null) { variation = maybeVariation.intValue(); } else { - DataModel.Rollout rollout = vr.getRollout(); + Rollout rollout = vr.getRollout(); if (rollout != null && !rollout.getVariations().isEmpty()) { float bucket = bucketUser(rollout.getSeed(), user, flag.getKey(), rollout.getBucketBy(), flag.getSalt()); float sum = 0F; - for (DataModel.WeightedVariation wv : rollout.getVariations()) { + List variations = rollout.getVariations(); // guaranteed non-null + int nVariations = variations.size(); + for (int i = 0; i < nVariations; i++) { + WeightedVariation wv = variations.get(i); sum += (float) wv.getWeight() / 100000F; if (bucket < sum) { variation = wv.getVariation(); @@ -273,12 +247,17 @@ private EvalResult getValueForVariationOrRollout(DataModel.FeatureFlag flag, Dat if (variation < 0) { logger.error("Data inconsistency in feature flag \"{}\": variation/rollout object with no variation or rollout", flag.getKey()); return EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG); - } else { - return getVariation(flag, variation, inExperiment ? experimentize(reason) : reason); } + // Normally, we will always have precomputedResults + if (precomputedResults != null) { + return precomputedResults.forVariation(variation, inExperiment); + } + // If for some reason we don't, synthesize an equivalent result + return EvalResult.of(EvaluatorHelpers.evaluationDetailForVariation( + flag, variation, inExperiment ? experimentize(reason) : reason)); } - private EvaluationReason experimentize(EvaluationReason reason) { + private static EvaluationReason experimentize(EvaluationReason reason) { if (reason.getKind() == Kind.FALLTHROUGH) { return EvaluationReason.fallthrough(true); } else if (reason.getKind() == Kind.RULE_MATCH) { @@ -287,24 +266,30 @@ private EvaluationReason experimentize(EvaluationReason reason) { return reason; } - private boolean ruleMatchesUser(DataModel.FeatureFlag flag, DataModel.Rule rule, LDUser user, BigSegmentsState bigSegmentsState) { - for (DataModel.Clause clause: rule.getClauses()) { // getClauses() is guaranteed non-null - if (!clauseMatchesUser(clause, user, bigSegmentsState)) { + private boolean ruleMatchesUser(FeatureFlag flag, Rule rule, LDUser user, EvaluatorState state) { + List clauses = rule.getClauses(); // guaranteed non-null + int nClauses = clauses.size(); + for (int i = 0; i < nClauses; i++) { + Clause clause = clauses.get(i); + if (!clauseMatchesUser(clause, user, state)) { return false; } } return true; } - private boolean clauseMatchesUser(DataModel.Clause clause, LDUser user, BigSegmentsState bigSegmentsState) { + private boolean clauseMatchesUser(Clause clause, LDUser user, EvaluatorState state) { // In the case of a segment match operator, we check if the user is in any of the segments, // and possibly negate - if (clause.getOp() == DataModel.Operator.segmentMatch) { - for (LDValue j: clause.getValues()) { - if (j.isString()) { - DataModel.Segment segment = getters.getSegment(j.stringValue()); + if (clause.getOp() == Operator.segmentMatch) { + List values = clause.getValues(); // guaranteed non-null + int nValues = values.size(); + for (int i = 0; i < nValues; i++) { + LDValue clauseValue = values.get(i); + if (clauseValue.isString()) { + Segment segment = getters.getSegment(clauseValue.stringValue()); if (segment != null) { - if (segmentMatchesUser(segment, user, bigSegmentsState)) { + if (segmentMatchesUser(segment, user, state)) { return maybeNegate(clause, true); } } @@ -316,14 +301,16 @@ private boolean clauseMatchesUser(DataModel.Clause clause, LDUser user, BigSegme return clauseMatchesUserNoSegments(clause, user); } - private boolean clauseMatchesUserNoSegments(DataModel.Clause clause, LDUser user) { + private boolean clauseMatchesUserNoSegments(Clause clause, LDUser user) { LDValue userValue = user.getAttribute(clause.getAttribute()); if (userValue.isNull()) { return false; } if (userValue.getType() == LDValueType.ARRAY) { - for (LDValue value: userValue.values()) { + int nValues = userValue.size(); + for (int i = 0; i < nValues; i++) { + LDValue value = userValue.get(i); if (value.getType() == LDValueType.ARRAY || value.getType() == LDValueType.OBJECT) { logger.error("Invalid custom attribute value in user object for user key \"{}\": {}", user.getKey(), value); return false; @@ -341,11 +328,11 @@ private boolean clauseMatchesUserNoSegments(DataModel.Clause clause, LDUser user return false; } - static boolean clauseMatchAny(DataModel.Clause clause, LDValue userValue) { - DataModel.Operator op = clause.getOp(); + static boolean clauseMatchAny(Clause clause, LDValue userValue) { + Operator op = clause.getOp(); if (op != null) { - EvaluatorPreprocessing.ClauseExtra preprocessed = clause.getPreprocessed(); - if (op == DataModel.Operator.in) { + ClausePreprocessed preprocessed = clause.preprocessed; + if (op == Operator.in) { // see if we have precomputed a Set for fast equality matching Set vs = preprocessed == null ? null : preprocessed.valuesSet; if (vs != null) { @@ -353,12 +340,12 @@ static boolean clauseMatchAny(DataModel.Clause clause, LDValue userValue) { } } List values = clause.getValues(); - List preprocessedValues = + List preprocessedValues = preprocessed == null ? null : preprocessed.valuesExtra; int n = values.size(); for (int i = 0; i < n; i++) { // the preprocessed list, if present, will always have the same size as the values list - EvaluatorPreprocessing.ClauseExtra.ValueExtra p = preprocessedValues == null ? null : preprocessedValues.get(i); + ClausePreprocessed.ValueData p = preprocessedValues == null ? null : preprocessedValues.get(i); LDValue v = values.get(i); if (EvaluatorOperators.apply(op, userValue, v, p)) { return true; @@ -368,11 +355,11 @@ static boolean clauseMatchAny(DataModel.Clause clause, LDValue userValue) { return false; } - private boolean maybeNegate(DataModel.Clause clause, boolean b) { + private boolean maybeNegate(Clause clause, boolean b) { return clause.isNegate() ? !b : b; } - private boolean segmentMatchesUser(DataModel.Segment segment, LDUser user, BigSegmentsState bigSegmentsState) { + private boolean segmentMatchesUser(Segment segment, LDUser user, EvaluatorState state) { String userKey = user.getKey(); // we've already verified that the key is non-null at the top of evaluate() if (segment.isUnbounded()) { if (segment.getGeneration() == null) { @@ -380,24 +367,24 @@ private boolean segmentMatchesUser(DataModel.Segment segment, LDUser user, BigSe // probably means the data store was populated by an older SDK that doesn't know about the // generation property and therefore dropped it from the JSON data. We'll treat that as a // "not configured" condition. - bigSegmentsState.bigSegmentsStatus = EvaluationReason.BigSegmentsStatus.NOT_CONFIGURED; + state.bigSegmentsStatus = EvaluationReason.BigSegmentsStatus.NOT_CONFIGURED; return false; } // Even if multiple Big Segments are referenced within a single flag evaluation, we only need // to do this query once, since it returns *all* of the user's segment memberships. - if (bigSegmentsState.bigSegmentsStatus == null) { + if (state.bigSegmentsStatus == null) { BigSegmentStoreWrapper.BigSegmentsQueryResult queryResult = getters.getBigSegments(user.getKey()); if (queryResult == null) { // The SDK hasn't been configured to be able to use big segments - bigSegmentsState.bigSegmentsStatus = EvaluationReason.BigSegmentsStatus.NOT_CONFIGURED; + state.bigSegmentsStatus = EvaluationReason.BigSegmentsStatus.NOT_CONFIGURED; } else { - bigSegmentsState.bigSegmentsStatus = queryResult.status; - bigSegmentsState.bigSegmentsMembership = queryResult.membership; + state.bigSegmentsStatus = queryResult.status; + state.bigSegmentsMembership = queryResult.membership; } } - Boolean membership = bigSegmentsState.bigSegmentsMembership == null ? - null : bigSegmentsState.bigSegmentsMembership.checkMembership(makeBigSegmentRef(segment)); + Boolean membership = state.bigSegmentsMembership == null ? + null : state.bigSegmentsMembership.checkMembership(makeBigSegmentRef(segment)); if (membership != null) { return membership; } @@ -409,7 +396,10 @@ private boolean segmentMatchesUser(DataModel.Segment segment, LDUser user, BigSe return false; } } - for (DataModel.SegmentRule rule: segment.getRules()) { + List rules = segment.getRules(); // guaranteed non-null + int nRules = rules.size(); + for (int i = 0; i < nRules; i++) { + SegmentRule rule = rules.get(i); if (segmentRuleMatchesUser(rule, user, segment.getKey(), segment.getSalt())) { return true; } @@ -417,8 +407,11 @@ private boolean segmentMatchesUser(DataModel.Segment segment, LDUser user, BigSe return false; } - private boolean segmentRuleMatchesUser(DataModel.SegmentRule segmentRule, LDUser user, String segmentKey, String salt) { - for (DataModel.Clause c: segmentRule.getClauses()) { + private boolean segmentRuleMatchesUser(SegmentRule segmentRule, LDUser user, String segmentKey, String salt) { + List clauses = segmentRule.getClauses(); // guaranteed non-null + int nClauses = clauses.size(); + for (int i = 0; i < nClauses; i++) { + Clause c = clauses.get(i); if (!clauseMatchesUserNoSegments(c, user)) { return false; } @@ -435,7 +428,15 @@ private boolean segmentRuleMatchesUser(DataModel.SegmentRule segmentRule, LDUser return bucket < weight; } - static String makeBigSegmentRef(DataModel.Segment segment) { + private static EvalResult computeRuleMatch(FeatureFlag flag, LDUser user, Rule rule, int ruleIndex) { + if (rule.preprocessed != null) { + return getValueForVariationOrRollout(flag, rule, user, rule.preprocessed.allPossibleResults, null); + } + EvaluationReason reason = EvaluationReason.ruleMatch(ruleIndex, rule.getId()); + return getValueForVariationOrRollout(flag, rule, user, null, reason); + } + + static String makeBigSegmentRef(Segment segment) { return String.format("%s.g%d", segment.getKey(), segment.getGeneration()); } } diff --git a/src/main/java/com/launchdarkly/sdk/server/EvaluatorHelpers.java b/src/main/java/com/launchdarkly/sdk/server/EvaluatorHelpers.java new file mode 100644 index 000000000..68b53de0e --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/EvaluatorHelpers.java @@ -0,0 +1,67 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.EvaluationReason.ErrorKind; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Prerequisite; +import com.launchdarkly.sdk.server.DataModel.Target; + +import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; + +/** + * Low-level helpers for producing various kinds of evaluation results. + *

    + * For all of the methods that return an {@link EvalResult}, the behavior is as follows: + * First we check if the flag data contains a preprocessed value for this kind of result; if + * so, we return that same EvalResult instance, for efficiency. That will normally always be + * the case, because preprocessing happens as part of deserializing a flag. But if somehow + * no preprocessed value is available, we construct one less efficiently on the fly. (The + * reason we can't absolutely guarantee that the preprocessed data is available, by putting + * it in a constructor, is because of how deserialization works: Gson doesn't pass values to + * a constructor, it sets fields directly, so we have to run our preprocessing logic after.) + */ +abstract class EvaluatorHelpers { + static EvalResult offResult(FeatureFlag flag) { + if (flag.preprocessed != null) { + return flag.preprocessed.offResult; + } + return EvalResult.of(evaluationDetailForOffVariation(flag, EvaluationReason.off())); + } + + static EvalResult targetMatchResult(FeatureFlag flag, Target target) { + if (target.preprocessed != null) { + return target.preprocessed.targetMatchResult; + } + return EvalResult.of(evaluationDetailForVariation( + flag, target.getVariation(), EvaluationReason.targetMatch())); + } + + static EvalResult prerequisiteFailedResult(FeatureFlag flag, Prerequisite prereq) { + if (prereq.preprocessed != null) { + return prereq.preprocessed.prerequisiteFailedResult; + } + return EvalResult.of(evaluationDetailForOffVariation( + flag, EvaluationReason.prerequisiteFailed(prereq.getKey()))); + } + + static EvaluationDetail evaluationDetailForOffVariation(FeatureFlag flag, EvaluationReason reason) { + Integer offVariation = flag.getOffVariation(); + if (offVariation == null) { // off variation unspecified - return default value + return EvaluationDetail.fromValue(LDValue.ofNull(), NO_VARIATION, reason); + } + return evaluationDetailForVariation(flag, offVariation, reason); + } + + static EvaluationDetail evaluationDetailForVariation(FeatureFlag flag, int variation, EvaluationReason reason) { + if (variation < 0 || variation >= flag.getVariations().size()) { + return EvaluationDetail.fromValue(LDValue.ofNull(), NO_VARIATION, + EvaluationReason.error(ErrorKind.MALFORMED_FLAG)); + } + return EvaluationDetail.fromValue( + LDValue.normalize(flag.getVariations().get(variation)), + variation, + reason); + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/EvaluatorOperators.java b/src/main/java/com/launchdarkly/sdk/server/EvaluatorOperators.java index 3b383942f..d3043742b 100644 --- a/src/main/java/com/launchdarkly/sdk/server/EvaluatorOperators.java +++ b/src/main/java/com/launchdarkly/sdk/server/EvaluatorOperators.java @@ -1,6 +1,7 @@ package com.launchdarkly.sdk.server; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed; import java.time.Instant; import java.util.regex.Pattern; @@ -44,7 +45,7 @@ static boolean apply( DataModel.Operator op, LDValue userValue, LDValue clauseValue, - EvaluatorPreprocessing.ClauseExtra.ValueExtra preprocessed + ClausePreprocessed.ValueData preprocessed ) { switch (op) { case in: @@ -116,7 +117,7 @@ private static boolean compareDate( ComparisonOp op, LDValue userValue, LDValue clauseValue, - EvaluatorPreprocessing.ClauseExtra.ValueExtra preprocessed + ClausePreprocessed.ValueData preprocessed ) { // If preprocessed is non-null, it means we've already tried to parse the clause value as a date/time, // in which case if preprocessed.parsedDate is null it was not a valid date/time. @@ -135,7 +136,7 @@ private static boolean compareSemVer( ComparisonOp op, LDValue userValue, LDValue clauseValue, - EvaluatorPreprocessing.ClauseExtra.ValueExtra preprocessed + ClausePreprocessed.ValueData preprocessed ) { // If preprocessed is non-null, it means we've already tried to parse the clause value as a version, // in which case if preprocessed.parsedSemVer is null it was not a valid version. diff --git a/src/main/java/com/launchdarkly/sdk/server/EvaluatorPreprocessing.java b/src/main/java/com/launchdarkly/sdk/server/EvaluatorPreprocessing.java deleted file mode 100644 index b31f9a3ff..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/EvaluatorPreprocessing.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableSet; -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.DataModel.Clause; -import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.DataModel.Operator; -import com.launchdarkly.sdk.server.DataModel.Prerequisite; -import com.launchdarkly.sdk.server.DataModel.Rule; -import com.launchdarkly.sdk.server.DataModel.Segment; -import com.launchdarkly.sdk.server.DataModel.SegmentRule; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.regex.Pattern; - -/** - * These methods precompute data that may help to reduce the overhead of feature flag evaluations. They - * are called from the afterDeserialized() methods of FeatureFlag and Segment, after those objects have - * been deserialized from JSON but before they have been made available to any other code (so these - * methods do not need to be thread-safe). - *

    - * If for some reason these methods have not been called before an evaluation happens, the evaluation - * logic must still be able to work without the precomputed data. - */ -abstract class EvaluatorPreprocessing { - private EvaluatorPreprocessing() {} - - static final class ClauseExtra { - final Set valuesSet; - final List valuesExtra; - - ClauseExtra(Set valuesSet, List valuesExtra) { - this.valuesSet = valuesSet; - this.valuesExtra = valuesExtra; - } - - static final class ValueExtra { - final Instant parsedDate; - final Pattern parsedRegex; - final SemanticVersion parsedSemVer; - - ValueExtra(Instant parsedDate, Pattern parsedRegex, SemanticVersion parsedSemVer) { - this.parsedDate = parsedDate; - this.parsedRegex = parsedRegex; - this.parsedSemVer = parsedSemVer; - } - } - } - - static void preprocessFlag(FeatureFlag f) { - for (Prerequisite p: f.getPrerequisites()) { - EvaluatorPreprocessing.preprocessPrerequisite(p); - } - List rules = f.getRules(); - int n = rules.size(); - for (int i = 0; i < n; i++) { - preprocessFlagRule(rules.get(i), i); - } - preprocessValueList(f.getVariations()); - } - - static void preprocessSegment(Segment s) { - List rules = s.getRules(); - int n = rules.size(); - for (int i = 0; i < n; i++) { - preprocessSegmentRule(rules.get(i), i); - } - } - - static void preprocessPrerequisite(Prerequisite p) { - // Precompute an immutable EvaluationReason instance that will be used if the prerequisite fails. - p.setPrerequisiteFailedReason(EvaluationReason.prerequisiteFailed(p.getKey())); - } - - static void preprocessFlagRule(Rule r, int ruleIndex) { - // Precompute an immutable EvaluationReason instance that will be used if a user matches this rule. - r.setRuleMatchReason(EvaluationReason.ruleMatch(ruleIndex, r.getId())); - - for (Clause c: r.getClauses()) { - preprocessClause(c); - } - } - - static void preprocessSegmentRule(SegmentRule r, int ruleIndex) { - for (Clause c: r.getClauses()) { - preprocessClause(c); - } - } - - static void preprocessClause(Clause c) { - // If the clause values contain a null (which is valid in terms of the JSON schema, even if it - // can't ever produce a true result), Gson will give us an actual null. Change this to - // LDValue.ofNull() to avoid NPEs down the line. It's more efficient to do this just once at - // deserialization time than to do it in every clause match. - List values = c.getValues(); - preprocessValueList(values); - - Operator op = c.getOp(); - if (op == null) { - return; - } - switch (op) { - case in: - // This is a special case where the clause is testing for an exact match against any of the - // clause values. Converting the value list to a Set allows us to do a fast lookup instead of - // a linear search. We do not do this for other operators (or if there are fewer than two - // values) because the slight extra overhead of a Set is not worthwhile in those case. - if (values.size() > 1) { - c.setPreprocessed(new ClauseExtra(ImmutableSet.copyOf(values), null)); - } - break; - case matches: - c.setPreprocessed(preprocessClauseValues(c.getValues(), v -> - new ClauseExtra.ValueExtra(null, EvaluatorTypeConversion.valueToRegex(v), null) - )); - break; - case after: - case before: - c.setPreprocessed(preprocessClauseValues(c.getValues(), v -> - new ClauseExtra.ValueExtra(EvaluatorTypeConversion.valueToDateTime(v), null, null) - )); - break; - case semVerEqual: - case semVerGreaterThan: - case semVerLessThan: - c.setPreprocessed(preprocessClauseValues(c.getValues(), v -> - new ClauseExtra.ValueExtra(null, null, EvaluatorTypeConversion.valueToSemVer(v)) - )); - break; - default: - break; - } - } - - static void preprocessValueList(List values) { - // If a list of values contains a null (which is valid in terms of the JSON schema, even if it - // isn't useful because the SDK considers this a non-value), Gson will give us an actual null. - // Change this to LDValue.ofNull() to avoid NPEs down the line. It's more efficient to do this - // just once at deserialization time than to do it in every clause match. - for (int i = 0; i < values.size(); i++) { - if (values.get(i) == null) { - values.set(i, LDValue.ofNull()); - } - } - } - - private static ClauseExtra preprocessClauseValues( - List values, - Function f - ) { - List valuesExtra = new ArrayList<>(values.size()); - for (LDValue v: values) { - valuesExtra.add(f.apply(v)); - } - return new ClauseExtra(null, valuesExtra); - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/EventFactory.java b/src/main/java/com/launchdarkly/sdk/server/EventFactory.java index 73a815e6e..bc06127fd 100644 --- a/src/main/java/com/launchdarkly/sdk/server/EventFactory.java +++ b/src/main/java/com/launchdarkly/sdk/server/EventFactory.java @@ -5,7 +5,6 @@ import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.DataModel.VariationOrRollout; import com.launchdarkly.sdk.server.interfaces.Event; import com.launchdarkly.sdk.server.interfaces.Event.Custom; import com.launchdarkly.sdk.server.interfaces.Event.FeatureRequest; @@ -23,6 +22,7 @@ abstract Event.FeatureRequest newFeatureRequestEvent( LDValue value, int variationIndex, EvaluationReason reason, + boolean forceReasonTracking, LDValue defaultValue, String prereqOf ); @@ -43,15 +43,16 @@ abstract Event.FeatureRequest newUnknownFeatureRequestEvent( final Event.FeatureRequest newFeatureRequestEvent( DataModel.FeatureFlag flag, LDUser user, - Evaluator.EvalResult details, + EvalResult result, LDValue defaultValue ) { return newFeatureRequestEvent( flag, user, - details == null ? null : details.getValue(), - details == null ? -1 : details.getVariationIndex(), - details == null ? null : details.getReason(), + result == null ? null : result.getValue(), + result == null ? -1 : result.getVariationIndex(), + result == null ? null : result.getReason(), + result != null && result.isForceReasonTracking(), defaultValue, null ); @@ -69,6 +70,7 @@ final Event.FeatureRequest newDefaultFeatureRequestEvent( defaultVal, -1, EvaluationReason.error(errorKind), + false, defaultVal, null ); @@ -77,15 +79,16 @@ final Event.FeatureRequest newDefaultFeatureRequestEvent( final Event.FeatureRequest newPrerequisiteFeatureRequestEvent( DataModel.FeatureFlag prereqFlag, LDUser user, - Evaluator.EvalResult details, + EvalResult result, DataModel.FeatureFlag prereqOf ) { return newFeatureRequestEvent( prereqFlag, user, - details == null ? null : details.getValue(), - details == null ? -1 : details.getVariationIndex(), - details == null ? null : details.getReason(), + result == null ? null : result.getValue(), + result == null ? -1 : result.getVariationIndex(), + result == null ? null : result.getReason(), + result != null && result.isForceReasonTracking(), LDValue.ofNull(), prereqOf.getKey() ); @@ -118,9 +121,9 @@ static class Default extends EventFactory { } @Override - final Event.FeatureRequest newFeatureRequestEvent(DataModel.FeatureFlag flag, LDUser user, LDValue value, - int variationIndex, EvaluationReason reason, LDValue defaultValue, String prereqOf){ - boolean requireExperimentData = isExperiment(flag, reason); + final Event.FeatureRequest newFeatureRequestEvent(DataModel.FeatureFlag flag, LDUser user, + LDValue value, int variationIndex, EvaluationReason reason, boolean forceReasonTracking, + LDValue defaultValue, String prereqOf){ return new Event.FeatureRequest( timestampFn.get(), flag.getKey(), @@ -129,9 +132,9 @@ final Event.FeatureRequest newFeatureRequestEvent(DataModel.FeatureFlag flag, LD variationIndex, value, defaultValue, - (requireExperimentData || includeReasons) ? reason : null, + (forceReasonTracking || includeReasons) ? reason : null, prereqOf, - requireExperimentData || flag.isTrackEvents(), + forceReasonTracking || flag.isTrackEvents(), flag.getDebugEventsUntilDate() == null ? 0 : flag.getDebugEventsUntilDate().longValue(), false ); @@ -181,7 +184,7 @@ static final class Disabled extends EventFactory { @Override final FeatureRequest newFeatureRequestEvent(FeatureFlag flag, LDUser user, LDValue value, int variationIndex, - EvaluationReason reason, LDValue defaultValue, String prereqOf) { + EvaluationReason reason, boolean inExperiment, LDValue defaultValue, String prereqOf) { return null; } @@ -205,32 +208,4 @@ Event.AliasEvent newAliasEvent(LDUser user, LDUser previousUser) { return null; } } - - static boolean isExperiment(DataModel.FeatureFlag flag, EvaluationReason reason) { - if (reason == null) { - // doesn't happen in real life, but possible in testing - return false; - } - - // If the reason says we're in an experiment, we are. Otherwise, apply - // the legacy rule exclusion logic. - if (reason.isInExperiment()) return true; - - switch (reason.getKind()) { - case FALLTHROUGH: - return flag.isTrackEventsFallthrough(); - case RULE_MATCH: - int ruleIndex = reason.getRuleIndex(); - // Note, it is OK to rely on the rule index rather than the unique ID in this context, because the - // FeatureFlag that is passed to us here *is* necessarily the same version of the flag that was just - // evaluated, so we cannot be out of sync with its rule list. - if (ruleIndex >= 0 && ruleIndex < flag.getRules().size()) { - DataModel.Rule rule = flag.getRules().get(ruleIndex); - return rule.isTrackEvents(); - } - return false; - default: - return false; - } - } } diff --git a/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java b/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java index cb44a98be..071ee5ee0 100644 --- a/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java +++ b/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java @@ -265,16 +265,15 @@ public Builder add( return this; } - Builder addFlag(DataModel.FeatureFlag flag, Evaluator.EvalResult eval) { - boolean requireExperimentData = EventFactory.isExperiment(flag, eval.getReason()); + Builder addFlag(DataModel.FeatureFlag flag, EvalResult eval) { return add( flag.getKey(), eval.getValue(), - eval.isDefault() ? null : eval.getVariationIndex(), + eval.isNoVariation() ? null : eval.getVariationIndex(), eval.getReason(), flag.getVersion(), - flag.isTrackEvents() || requireExperimentData, - requireExperimentData, + flag.isTrackEvents() || eval.isForceReasonTracking(), + eval.isForceReasonTracking(), flag.getDebugEventsUntilDate() ); } diff --git a/src/main/java/com/launchdarkly/sdk/server/LDClient.java b/src/main/java/com/launchdarkly/sdk/server/LDClient.java index 3e927d401..48a3aa49b 100644 --- a/src/main/java/com/launchdarkly/sdk/server/LDClient.java +++ b/src/main/java/com/launchdarkly/sdk/server/LDClient.java @@ -12,6 +12,7 @@ import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider; import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration; @@ -69,6 +70,8 @@ public final class LDClient implements LDClientInterface { private final ScheduledExecutorService sharedExecutor; private final EventFactory eventFactoryDefault; private final EventFactory eventFactoryWithReasons; + private final Evaluator.PrerequisiteEvaluationSink prereqEvalsDefault; + private final Evaluator.PrerequisiteEvaluationSink prereqEvalsWithReasons; /** * Creates a new client instance that connects to LaunchDarkly with the default configuration. @@ -250,6 +253,11 @@ public BigSegmentStoreWrapper.BigSegmentsQueryResult getBigSegments(String key) this.dataSource = config.dataSourceFactory.createDataSource(context, dataSourceUpdates); this.dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceStatusNotifier, dataSourceUpdates); + this.prereqEvalsDefault = makePrerequisiteEventSender(false); + this.prereqEvalsWithReasons = makePrerequisiteEventSender(true); + // We pre-create those two callback objects, rather than using inline lambdas when we call the Evaluator, + // because using lambdas would cause a new closure object to be allocated every time. + Future startFuture = dataSource.start(); if (!config.startWait.isZero() && !config.startWait.isNegative()) { if (!(dataSource instanceof ComponentsImpl.NullDataSource)) { @@ -353,12 +361,15 @@ public FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) continue; } try { - Evaluator.EvalResult result = evaluator.evaluate(flag, user, eventFactoryDefault); + EvalResult result = evaluator.evaluate(flag, user, null); + // Note: the null parameter to evaluate() is for the PrerequisiteEvaluationSink; allFlagsState should + // not generate evaluation events, so we don't want the evaluator to generate any prerequisite evaluation + // events either. builder.addFlag(flag, result); } catch (Exception e) { Loggers.EVALUATION.error("Exception caught for feature flag \"{}\" when evaluating all flags: {}", entry.getKey(), e.toString()); Loggers.EVALUATION.debug(e.toString(), e); - builder.addFlag(flag, new Evaluator.EvalResult(LDValue.ofNull(), NO_VARIATION, EvaluationReason.exception(e))); + builder.addFlag(flag, EvalResult.of(LDValue.ofNull(), NO_VARIATION, EvaluationReason.exception(e))); } } return builder.build(); @@ -391,41 +402,37 @@ public LDValue jsonValueVariation(String featureKey, LDUser user, LDValue defaul @Override public EvaluationDetail boolVariationDetail(String featureKey, LDUser user, boolean defaultValue) { - Evaluator.EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), - LDValueType.BOOLEAN, eventFactoryWithReasons); - return EvaluationDetail.fromValue(result.getValue().booleanValue(), - result.getVariationIndex(), result.getReason()); + EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), + LDValueType.BOOLEAN, true); + return result.getAsBoolean(); } @Override public EvaluationDetail intVariationDetail(String featureKey, LDUser user, int defaultValue) { - Evaluator.EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), - LDValueType.NUMBER, eventFactoryWithReasons); - return EvaluationDetail.fromValue(result.getValue().intValue(), - result.getVariationIndex(), result.getReason()); + EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), + LDValueType.NUMBER, true); + return result.getAsInteger(); } @Override public EvaluationDetail doubleVariationDetail(String featureKey, LDUser user, double defaultValue) { - Evaluator.EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), - LDValueType.NUMBER, eventFactoryWithReasons); - return EvaluationDetail.fromValue(result.getValue().doubleValue(), - result.getVariationIndex(), result.getReason()); + EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), + LDValueType.NUMBER, true); + return result.getAsDouble(); } @Override public EvaluationDetail stringVariationDetail(String featureKey, LDUser user, String defaultValue) { - Evaluator.EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), - LDValueType.STRING, eventFactoryWithReasons); - return EvaluationDetail.fromValue(result.getValue().stringValue(), - result.getVariationIndex(), result.getReason()); + EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), + LDValueType.STRING, true); + return result.getAsString(); } @Override public EvaluationDetail jsonValueVariationDetail(String featureKey, LDUser user, LDValue defaultValue) { - Evaluator.EvalResult result = evaluateInternal(featureKey, user, LDValue.normalize(defaultValue), - null, eventFactoryWithReasons); - return EvaluationDetail.fromValue(result.getValue(), result.getVariationIndex(), result.getReason()); + EvalResult result = evaluateInternal(featureKey, user, LDValue.normalize(defaultValue), + null, true); + return result.getAnyType(); } @Override @@ -452,15 +459,16 @@ public boolean isFlagKnown(String featureKey) { } private LDValue evaluate(String featureKey, LDUser user, LDValue defaultValue, LDValueType requireType) { - return evaluateInternal(featureKey, user, defaultValue, requireType, eventFactoryDefault).getValue(); + return evaluateInternal(featureKey, user, defaultValue, requireType, false).getValue(); } - private Evaluator.EvalResult errorResult(EvaluationReason.ErrorKind errorKind, final LDValue defaultValue) { - return new Evaluator.EvalResult(defaultValue, NO_VARIATION, EvaluationReason.error(errorKind)); + private EvalResult errorResult(EvaluationReason.ErrorKind errorKind, final LDValue defaultValue) { + return EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.error(errorKind)); } - private Evaluator.EvalResult evaluateInternal(String featureKey, LDUser user, LDValue defaultValue, - LDValueType requireType, EventFactory eventFactory) { + private EvalResult evaluateInternal(String featureKey, LDUser user, LDValue defaultValue, + LDValueType requireType, boolean withDetail) { + EventFactory eventFactory = withDetail ? eventFactoryWithReasons : eventFactoryDefault; if (!isInitialized()) { if (dataStore.isInitialized()) { Loggers.EVALUATION.warn("Evaluation called before client initialized for feature flag \"{}\"; using last known values from data store", featureKey); @@ -490,12 +498,10 @@ private Evaluator.EvalResult evaluateInternal(String featureKey, LDUser user, LD if (user.getKey().isEmpty()) { Loggers.EVALUATION.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly"); } - Evaluator.EvalResult evalResult = evaluator.evaluate(featureFlag, user, eventFactory); - for (Event.FeatureRequest event : evalResult.getPrerequisiteEvents()) { - eventProcessor.sendEvent(event); - } - if (evalResult.isDefault()) { - evalResult.setValue(defaultValue); + EvalResult evalResult = evaluator.evaluate(featureFlag, user, + withDetail ? prereqEvalsWithReasons : prereqEvalsDefault); + if (evalResult.isNoVariation()) { + evalResult = EvalResult.of(defaultValue, evalResult.getVariationIndex(), evalResult.getReason()); } else { LDValue value = evalResult.getValue(); // guaranteed not to be an actual Java null, but can be LDValue.ofNull() if (requireType != null && @@ -519,7 +525,7 @@ private Evaluator.EvalResult evaluateInternal(String featureKey, LDUser user, LD sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue, EvaluationReason.ErrorKind.EXCEPTION)); } - return new Evaluator.EvalResult(defaultValue, NO_VARIATION, EvaluationReason.exception(e)); + return EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.exception(e)); } } @@ -610,4 +616,15 @@ private ScheduledExecutorService createSharedExecutor(LDConfig config) { .build(); return Executors.newSingleThreadScheduledExecutor(threadFactory); } + + private Evaluator.PrerequisiteEvaluationSink makePrerequisiteEventSender(boolean withReasons) { + final EventFactory factory = withReasons ? eventFactoryWithReasons : eventFactoryDefault; + return new Evaluator.PrerequisiteEvaluationSink() { + @Override + public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDUser user, EvalResult result) { + eventProcessor.sendEvent( + factory.newPrerequisiteFeatureRequestEvent(flag, user, result, prereqOfFlag)); + } + }; + } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorPreprocessingTest.java b/src/test/java/com/launchdarkly/sdk/server/DataModelPreprocessingTest.java similarity index 52% rename from src/test/java/com/launchdarkly/sdk/server/EvaluatorPreprocessingTest.java rename to src/test/java/com/launchdarkly/sdk/server/DataModelPreprocessingTest.java index beafff7fa..ea6171e1b 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorPreprocessingTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataModelPreprocessingTest.java @@ -2,14 +2,19 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel.Clause; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Operator; +import com.launchdarkly.sdk.server.DataModel.Prerequisite; import com.launchdarkly.sdk.server.DataModel.Rule; import com.launchdarkly.sdk.server.DataModel.Segment; import com.launchdarkly.sdk.server.DataModel.SegmentRule; +import com.launchdarkly.sdk.server.DataModel.Target; +import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed; import org.junit.Test; @@ -17,15 +22,20 @@ import java.time.ZonedDateTime; import java.util.List; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @SuppressWarnings("javadoc") -public class EvaluatorPreprocessingTest { +public class DataModelPreprocessingTest { // We deliberately use the data model constructors here instead of the more convenient ModelBuilders // equivalents, to make sure we're testing the afterDeserialization() behavior and not just the builder. + private static final LDValue aValue = LDValue.of("a"), bValue = LDValue.of("b"); + private FeatureFlag flagFromClause(Clause c) { return new FeatureFlag("key", 0, false, null, null, null, rulesFromClause(c), null, null, null, false, false, false, null, false); @@ -34,6 +44,121 @@ private FeatureFlag flagFromClause(Clause c) { private List rulesFromClause(Clause c) { return ImmutableList.of(new Rule("", ImmutableList.of(c), null, null, false)); } + + @Test + public void preprocessFlagAddsPrecomputedOffResult() { + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, + ImmutableList.of(), null, + 0, + ImmutableList.of(aValue, bValue), + false, false, false, null, false); + + f.afterDeserialized(); + + assertThat(f.preprocessed, notNullValue()); + assertThat(f.preprocessed.offResult, + equalTo(EvalResult.of(aValue, 0, EvaluationReason.off()))); + } + + @Test + public void preprocessFlagAddsPrecomputedOffResultForNullOffVariation() { + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, + ImmutableList.of(), null, + null, + ImmutableList.of(aValue, bValue), + false, false, false, null, false); + + f.afterDeserialized(); + + assertThat(f.preprocessed, notNullValue()); + assertThat(f.preprocessed.offResult, + equalTo(EvalResult.of(LDValue.ofNull(), EvaluationDetail.NO_VARIATION, EvaluationReason.off()))); + } + + @Test + public void preprocessFlagAddsPrecomputedFallthroughResults() { + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, + ImmutableList.of(), null, 0, + ImmutableList.of(aValue, bValue), + false, false, false, null, false); + + f.afterDeserialized(); + + assertThat(f.preprocessed, notNullValue()); + assertThat(f.preprocessed.fallthroughResults, notNullValue()); + EvaluationReason regularReason = EvaluationReason.fallthrough(false); + EvaluationReason inExperimentReason = EvaluationReason.fallthrough(true); + + assertThat(f.preprocessed.fallthroughResults.forVariation(0, false), + equalTo(EvalResult.of(aValue, 0, regularReason))); + assertThat(f.preprocessed.fallthroughResults.forVariation(0, true), + equalTo(EvalResult.of(aValue, 0, inExperimentReason))); + + assertThat(f.preprocessed.fallthroughResults.forVariation(1, false), + equalTo(EvalResult.of(bValue, 1, regularReason))); + assertThat(f.preprocessed.fallthroughResults.forVariation(1, true), + equalTo(EvalResult.of(bValue, 1, inExperimentReason))); + } + + @Test + public void preprocessFlagAddsPrecomputedTargetMatchResults() { + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, + ImmutableList.of(new Target(ImmutableSet.of(), 1)), + ImmutableList.of(), null, 0, + ImmutableList.of(aValue, bValue), + false, false, false, null, false); + + f.afterDeserialized(); + + Target t = f.getTargets().get(0); + assertThat(t.preprocessed, notNullValue()); + assertThat(t.preprocessed.targetMatchResult, + equalTo(EvalResult.of(bValue, 1, EvaluationReason.targetMatch()))); + } + + @Test + public void preprocessFlagAddsPrecomputedPrerequisiteFailedResults() { + FeatureFlag f = new FeatureFlag("key", 0, false, + ImmutableList.of(new Prerequisite("abc", 1)), + null, null, + ImmutableList.of(), null, 0, + ImmutableList.of(aValue, bValue), + false, false, false, null, false); + + f.afterDeserialized(); + + Prerequisite p = f.getPrerequisites().get(0); + assertThat(p.preprocessed, notNullValue()); + assertThat(p.preprocessed.prerequisiteFailedResult, + equalTo(EvalResult.of(aValue, 0, EvaluationReason.prerequisiteFailed("abc")))); + } + + @Test + public void preprocessFlagAddsPrecomputedResultsToFlagRules() { + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, + ImmutableList.of(new Rule("ruleid0", ImmutableList.of(), null, null, false)), + null, null, + ImmutableList.of(aValue, bValue), + false, false, false, null, false); + + f.afterDeserialized(); + + Rule rule = f.getRules().get(0); + assertThat(rule.preprocessed, notNullValue()); + assertThat(rule.preprocessed.allPossibleResults, notNullValue()); + EvaluationReason regularReason = EvaluationReason.ruleMatch(0, "ruleid0", false); + EvaluationReason inExperimentReason = EvaluationReason.ruleMatch(0, "ruleid0", true); + + assertThat(rule.preprocessed.allPossibleResults.forVariation(0, false), + equalTo(EvalResult.of(aValue, 0, regularReason))); + assertThat(rule.preprocessed.allPossibleResults.forVariation(0, true), + equalTo(EvalResult.of(aValue, 0, inExperimentReason))); + + assertThat(rule.preprocessed.allPossibleResults.forVariation(1, false), + equalTo(EvalResult.of(bValue, 1, regularReason))); + assertThat(rule.preprocessed.allPossibleResults.forVariation(1, true), + equalTo(EvalResult.of(bValue, 1, inExperimentReason))); + } @Test public void preprocessFlagCreatesClauseValuesMapForMultiValueEqualityTest() { @@ -45,11 +170,11 @@ public void preprocessFlagCreatesClauseValuesMapForMultiValueEqualityTest() { ); FeatureFlag f = flagFromClause(c); - assertNull(f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(f.getRules().get(0).getClauses().get(0).preprocessed); f.afterDeserialized(); - EvaluatorPreprocessing.ClauseExtra ce = f.getRules().get(0).getClauses().get(0).getPreprocessed(); + ClausePreprocessed ce = f.getRules().get(0).getClauses().get(0).preprocessed; assertNotNull(ce); assertEquals(ImmutableSet.of(LDValue.of("a"), LDValue.of(0)), ce.valuesSet); } @@ -64,11 +189,11 @@ public void preprocessFlagDoesNotCreateClauseValuesMapForSingleValueEqualityTest ); FeatureFlag f = flagFromClause(c); - assertNull(f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(f.getRules().get(0).getClauses().get(0).preprocessed); f.afterDeserialized(); - assertNull(f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(f.getRules().get(0).getClauses().get(0).preprocessed); } @Test @@ -81,11 +206,11 @@ public void preprocessFlagDoesNotCreateClauseValuesMapForEmptyEqualityTest() { ); FeatureFlag f = flagFromClause(c); - assertNull(f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(f.getRules().get(0).getClauses().get(0).preprocessed); f.afterDeserialized(); - assertNull(f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(f.getRules().get(0).getClauses().get(0).preprocessed); } @Test @@ -104,11 +229,11 @@ public void preprocessFlagDoesNotCreateClauseValuesMapForNonEqualityOperators() // matters is that there's more than one of them, so that it *would* build a map if the operator were "in" FeatureFlag f = flagFromClause(c); - assertNull(op.name(), f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(op.name(), f.getRules().get(0).getClauses().get(0).preprocessed); f.afterDeserialized(); - EvaluatorPreprocessing.ClauseExtra ce = f.getRules().get(0).getClauses().get(0).getPreprocessed(); + ClausePreprocessed ce = f.getRules().get(0).getClauses().get(0).preprocessed; // this might be non-null if we preprocessed the values list, but there should still not be a valuesSet if (ce != null) { assertNull(ce.valuesSet); @@ -132,11 +257,11 @@ public void preprocessFlagParsesClauseDate() { ); FeatureFlag f = flagFromClause(c); - assertNull(f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(f.getRules().get(0).getClauses().get(0).preprocessed); f.afterDeserialized(); - EvaluatorPreprocessing.ClauseExtra ce = f.getRules().get(0).getClauses().get(0).getPreprocessed(); + ClausePreprocessed ce = f.getRules().get(0).getClauses().get(0).preprocessed; assertNotNull(op.name(), ce); assertNotNull(op.name(), ce.valuesExtra); assertEquals(op.name(), 4, ce.valuesExtra.size()); @@ -157,11 +282,11 @@ public void preprocessFlagParsesClauseRegex() { ); FeatureFlag f = flagFromClause(c); - assertNull(f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(f.getRules().get(0).getClauses().get(0).preprocessed); f.afterDeserialized(); - EvaluatorPreprocessing.ClauseExtra ce = f.getRules().get(0).getClauses().get(0).getPreprocessed(); + ClausePreprocessed ce = f.getRules().get(0).getClauses().get(0).preprocessed; assertNotNull(ce); assertNotNull(ce.valuesExtra); assertEquals(3, ce.valuesExtra.size()); @@ -186,11 +311,11 @@ public void preprocessFlagParsesClauseSemVer() { ); FeatureFlag f = flagFromClause(c); - assertNull(f.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(f.getRules().get(0).getClauses().get(0).preprocessed); f.afterDeserialized(); - EvaluatorPreprocessing.ClauseExtra ce = f.getRules().get(0).getClauses().get(0).getPreprocessed(); + ClausePreprocessed ce = f.getRules().get(0).getClauses().get(0).preprocessed; assertNotNull(op.name(), ce); assertNotNull(op.name(), ce.valuesExtra); assertEquals(op.name(), 3, ce.valuesExtra.size()); @@ -213,11 +338,11 @@ public void preprocessSegmentPreprocessesClausesInRules() { SegmentRule rule = new SegmentRule(ImmutableList.of(c), null, null); Segment s = new Segment("key", null, null, null, ImmutableList.of(rule), 0, false, false, null); - assertNull(s.getRules().get(0).getClauses().get(0).getPreprocessed()); + assertNull(s.getRules().get(0).getClauses().get(0).preprocessed); s.afterDeserialized(); - EvaluatorPreprocessing.ClauseExtra ce = s.getRules().get(0).getClauses().get(0).getPreprocessed(); + ClausePreprocessed ce = s.getRules().get(0).getClauses().get(0).preprocessed; assertNotNull(ce.valuesExtra); assertEquals(1, ce.valuesExtra.size()); assertNotNull(ce.valuesExtra.get(0).parsedRegex); diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorOutputTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorOutputTest.java index 90b1cb602..4088bc6fa 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorOutputTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorOutputTest.java @@ -197,7 +197,7 @@ public void featureEventCanContainReason() throws Exception { DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); EvaluationReason reason = EvaluationReason.ruleMatch(1, null); Event.FeatureRequest fe = EventFactory.DEFAULT_WITH_REASONS.newFeatureRequestEvent(flag, user, - new Evaluator.EvalResult(LDValue.of("value"), 1, reason), LDValue.ofNull()); + EvalResult.of(LDValue.of("value"), 1, reason), LDValue.ofNull()); try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { ep.sendEvent(fe); diff --git a/src/test/java/com/launchdarkly/sdk/server/EvalResultTest.java b/src/test/java/com/launchdarkly/sdk/server/EvalResultTest.java new file mode 100644 index 000000000..a3987486f --- /dev/null +++ b/src/test/java/com/launchdarkly/sdk/server/EvalResultTest.java @@ -0,0 +1,157 @@ +package com.launchdarkly.sdk.server; + +import java.util.function.Function; + +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDValue; + +import org.junit.Test; + +import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; +import static com.launchdarkly.sdk.EvaluationReason.ErrorKind.WRONG_TYPE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + +public class EvalResultTest { + private static final LDValue SOME_VALUE = LDValue.of("value"); + private static final LDValue ARRAY_VALUE = LDValue.arrayOf(); + private static final LDValue OBJECT_VALUE = LDValue.buildObject().build(); + private static final int SOME_VARIATION = 11; + private static final EvaluationReason SOME_REASON = EvaluationReason.fallthrough(); + + @Test + public void getValue() { + assertThat(EvalResult.of(EvaluationDetail.fromValue(SOME_VALUE, SOME_VARIATION, SOME_REASON)).getValue(), + equalTo(SOME_VALUE)); + assertThat(EvalResult.of(SOME_VALUE, SOME_VARIATION, SOME_REASON).getValue(), + equalTo(SOME_VALUE)); + } + + @Test + public void getVariationIndex() { + assertThat(EvalResult.of(EvaluationDetail.fromValue(SOME_VALUE, SOME_VARIATION, SOME_REASON)).getVariationIndex(), + equalTo(SOME_VARIATION)); + assertThat(EvalResult.of(SOME_VALUE, SOME_VARIATION, SOME_REASON).getVariationIndex(), + equalTo(SOME_VARIATION)); + } + + @Test + public void getReason() { + assertThat(EvalResult.of(EvaluationDetail.fromValue(SOME_VALUE, SOME_VARIATION, SOME_REASON)).getReason(), + equalTo(SOME_REASON)); + assertThat(EvalResult.of(SOME_VALUE, SOME_VARIATION, SOME_REASON).getReason(), + equalTo(SOME_REASON)); + } + + @Test + public void isNoVariation() { + assertThat(EvalResult.of(EvaluationDetail.fromValue(SOME_VALUE, SOME_VARIATION, SOME_REASON)).isNoVariation(), + is(false)); + assertThat(EvalResult.of(SOME_VALUE, SOME_VARIATION, SOME_REASON).isNoVariation(), + is(false)); + + assertThat(EvalResult.of(EvaluationDetail.fromValue(SOME_VALUE, NO_VARIATION, SOME_REASON)).isNoVariation(), + is(true)); + assertThat(EvalResult.of(SOME_VALUE, NO_VARIATION, SOME_REASON).isNoVariation(), + is(true)); + } + + @Test + public void getAnyType() { + testForType(SOME_VALUE, SOME_VALUE, r -> r.getAnyType()); + } + + @Test + public void getAsBoolean() { + testForType(true, LDValue.of(true), r -> r.getAsBoolean()); + + testWrongType(false, LDValue.ofNull(), r -> r.getAsBoolean()); + testWrongType(false, LDValue.of(1), r -> r.getAsBoolean()); + testWrongType(false, LDValue.of("a"), r -> r.getAsBoolean()); + testWrongType(false, ARRAY_VALUE, r -> r.getAsBoolean()); + testWrongType(false, OBJECT_VALUE, r -> r.getAsBoolean()); + } + + @Test + public void getAsInteger() { + testForType(99, LDValue.of(99), r -> r.getAsInteger()); + testForType(99, LDValue.of(99.25), r -> r.getAsInteger()); + + testWrongType(0, LDValue.ofNull(), r -> r.getAsInteger()); + testWrongType(0, LDValue.of(true), r -> r.getAsInteger()); + testWrongType(0, LDValue.of("a"), r -> r.getAsInteger()); + testWrongType(0, ARRAY_VALUE, r -> r.getAsInteger()); + testWrongType(0, OBJECT_VALUE, r -> r.getAsInteger()); + } + + @Test + public void getAsDouble() { + testForType((double)99, LDValue.of(99), r -> r.getAsDouble()); + testForType((double)99.25, LDValue.of(99.25), r -> r.getAsDouble()); + + testWrongType((double)0, LDValue.ofNull(), r -> r.getAsDouble()); + testWrongType((double)0, LDValue.of(true), r -> r.getAsDouble()); + testWrongType((double)0, LDValue.of("a"), r -> r.getAsDouble()); + testWrongType((double)0, ARRAY_VALUE, r -> r.getAsDouble()); + testWrongType((double)0, OBJECT_VALUE, r -> r.getAsDouble()); + } + + @Test + public void getAsString() { + testForType("a", LDValue.of("a"), r -> r.getAsString()); + testForType((String)null, LDValue.ofNull(), r -> r.getAsString()); + + testWrongType((String)null, LDValue.of(true), r -> r.getAsString()); + testWrongType((String)null, LDValue.of(1), r -> r.getAsString()); + testWrongType((String)null, ARRAY_VALUE, r -> r.getAsString()); + testWrongType((String)null, OBJECT_VALUE, r -> r.getAsString()); + } + + @Test + public void withReason() { + EvalResult r = EvalResult.of(LDValue.of(true), SOME_VARIATION, EvaluationReason.fallthrough()); + + EvalResult r1 = r.withReason(EvaluationReason.off()); + assertThat(r1.getReason(), equalTo(EvaluationReason.off())); + assertThat(r1.getValue(), equalTo(r.getValue())); + assertThat(r1.getVariationIndex(), equalTo(r.getVariationIndex())); + } + + @Test + public void withForceReasonTracking() { + EvalResult r = EvalResult.of(SOME_VALUE, SOME_VARIATION, SOME_REASON); + assertThat(r.isForceReasonTracking(), is(false)); + + EvalResult r0 = r.withForceReasonTracking(false); + assertThat(r0, sameInstance(r)); + + EvalResult r1 = r.withForceReasonTracking(true); + assertThat(r1.isForceReasonTracking(), is(true)); + assertThat(r1.getAnyType(), sameInstance(r.getAnyType())); + } + + private void testForType(T value, LDValue ldValue, Function getter) { + assertThat( + getter.apply(EvalResult.of(EvaluationDetail.fromValue(ldValue, SOME_VARIATION, SOME_REASON))), + equalTo(EvaluationDetail.fromValue(value, SOME_VARIATION, SOME_REASON)) + ); + assertThat( + getter.apply(EvalResult.of(EvaluationDetail.fromValue(ldValue, SOME_VARIATION, SOME_REASON))), + equalTo(EvaluationDetail.fromValue(value, SOME_VARIATION, SOME_REASON)) + ); + } + + private void testWrongType(T value, LDValue ldValue, Function getter) { + assertThat( + getter.apply(EvalResult.of(EvaluationDetail.fromValue(ldValue, SOME_VARIATION, SOME_REASON))), + equalTo(EvaluationDetail.fromValue(value, EvaluationDetail.NO_VARIATION, EvaluationReason.error(WRONG_TYPE))) + ); + assertThat( + getter.apply(EvalResult.of(EvaluationDetail.fromValue(ldValue, SOME_VARIATION, SOME_REASON))), + equalTo(EvaluationDetail.fromValue(value, EvaluationDetail.NO_VARIATION, EvaluationReason.error(WRONG_TYPE))) + ); + } +} diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorBigSegmentTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorBigSegmentTest.java index 0ce40e8f8..88a587c0e 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorBigSegmentTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorBigSegmentTest.java @@ -1,7 +1,16 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDValue; + +import org.junit.Test; + +import java.util.Collections; + import static com.launchdarkly.sdk.server.Evaluator.makeBigSegmentRef; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.evaluatorBuilder; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.booleanFlagWithClauses; import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingSegment; import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingUser; @@ -15,15 +24,6 @@ import static org.easymock.EasyMock.strictMock; import static org.junit.Assert.assertEquals; -import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.Evaluator.EvalResult; - -import org.junit.Test; - -import java.util.Collections; - @SuppressWarnings("javadoc") public class EvaluatorBigSegmentTest { private static final LDUser testUser = new LDUser("userkey"); @@ -36,7 +36,7 @@ public void bigSegmentWithNoProviderIsNotMatched() { DataModel.FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), null).build(); - EvalResult result = evaluator.evaluate(flag, testUser, EventFactory.DEFAULT); + EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(false), result.getValue()); assertEquals(BigSegmentsStatus.NOT_CONFIGURED, result.getReason().getBigSegmentsStatus()); } @@ -48,7 +48,7 @@ public void bigSegmentWithNoGenerationIsNotMatched() { DataModel.FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).build(); - EvalResult result = evaluator.evaluate(flag, testUser, EventFactory.DEFAULT); + EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(false), result.getValue()); assertEquals(BigSegmentsStatus.NOT_CONFIGURED, result.getReason().getBigSegmentsStatus()); } @@ -62,7 +62,7 @@ public void matchedWithInclude() { queryResult.membership = createMembershipFromSegmentRefs(Collections.singleton(makeBigSegmentRef(segment)), null); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), queryResult).build(); - EvalResult result = evaluator.evaluate(flag, testUser, EventFactory.DEFAULT); + EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(true), result.getValue()); assertEquals(BigSegmentsStatus.HEALTHY, result.getReason().getBigSegmentsStatus()); } @@ -80,7 +80,7 @@ public void matchedWithRule() { queryResult.membership = createMembershipFromSegmentRefs(null, null); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), queryResult).build(); - EvalResult result = evaluator.evaluate(flag, testUser, EventFactory.DEFAULT); + EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(true), result.getValue()); assertEquals(BigSegmentsStatus.HEALTHY, result.getReason().getBigSegmentsStatus()); } @@ -98,7 +98,7 @@ public void unmatchedByExcludeRegardlessOfRule() { queryResult.membership = createMembershipFromSegmentRefs(null, Collections.singleton(makeBigSegmentRef(segment))); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), queryResult).build(); - EvalResult result = evaluator.evaluate(flag, testUser, EventFactory.DEFAULT); + EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(false), result.getValue()); assertEquals(BigSegmentsStatus.HEALTHY, result.getReason().getBigSegmentsStatus()); } @@ -112,7 +112,7 @@ public void bigSegmentStatusIsReturnedFromProvider() { queryResult.membership = createMembershipFromSegmentRefs(Collections.singleton(makeBigSegmentRef(segment)), null); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), queryResult).build(); - EvalResult result = evaluator.evaluate(flag, testUser, EventFactory.DEFAULT); + EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(true), result.getValue()); assertEquals(BigSegmentsStatus.STALE, result.getReason().getBigSegmentsStatus()); } @@ -142,7 +142,7 @@ public void bigSegmentStateIsQueriedOnlyOncePerUserEvenIfFlagReferencesMultipleS replay(mockGetters); Evaluator evaluator = new Evaluator(mockGetters); - EvalResult result = evaluator.evaluate(flag, testUser, EventFactory.DEFAULT); + EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(true), result.getValue()); assertEquals(BigSegmentsStatus.HEALTHY, result.getReason().getBigSegmentsStatus()); } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java index f2ca34451..fcadc8e0c 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java @@ -9,7 +9,6 @@ import com.launchdarkly.sdk.server.DataModel.Rollout; import com.launchdarkly.sdk.server.DataModel.RolloutKind; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; -import com.launchdarkly.sdk.server.Evaluator.EvalResult; import org.junit.Test; @@ -17,6 +16,7 @@ import java.util.List; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -155,7 +155,7 @@ private static void assertVariationIndexFromRollout( .fallthrough(rollout) .salt(salt) .build(); - EvalResult result1 = BASE_EVALUATOR.evaluate(flag1, user, EventFactory.DEFAULT); + EvalResult result1 = BASE_EVALUATOR.evaluate(flag1, user, expectNoPrerequisiteEvals()); assertThat(result1.getReason(), equalTo(EvaluationReason.fallthrough())); assertThat(result1.getVariationIndex(), equalTo(expectedVariation)); @@ -169,7 +169,7 @@ private static void assertVariationIndexFromRollout( .build()) .salt(salt) .build(); - EvalResult result2 = BASE_EVALUATOR.evaluate(flag2, user, EventFactory.DEFAULT); + EvalResult result2 = BASE_EVALUATOR.evaluate(flag2, user, expectNoPrerequisiteEvals()); assertThat(result2.getReason().getKind(), equalTo(EvaluationReason.Kind.RULE_MATCH)); assertThat(result2.getVariationIndex(), equalTo(expectedVariation)); } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorClauseTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorClauseTest.java index 01e24eb22..880f821d3 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorClauseTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorClauseTest.java @@ -11,6 +11,7 @@ import static com.launchdarkly.sdk.EvaluationDetail.fromValue; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.evaluatorBuilder; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.booleanFlagWithClauses; import static com.launchdarkly.sdk.server.ModelBuilders.clause; import static com.launchdarkly.sdk.server.ModelBuilders.fallthroughVariation; @@ -25,7 +26,7 @@ @SuppressWarnings("javadoc") public class EvaluatorClauseTest { private static void assertMatch(Evaluator eval, DataModel.FeatureFlag flag, LDUser user, boolean expectMatch) { - assertEquals(LDValue.of(expectMatch), eval.evaluate(flag, user, EventFactory.DEFAULT).getDetails().getValue()); + assertEquals(LDValue.of(expectMatch), eval.evaluate(flag, user, expectNoPrerequisiteEvals()).getValue()); } private static DataModel.Segment makeSegmentThatMatchesUser(String segmentKey, String userKey) { @@ -195,7 +196,7 @@ public void clauseWithNullOperatorDoesNotStopSubsequentRuleFromMatching() throws .build(); LDUser user = new LDUser.Builder("key").name("Bob").build(); - EvaluationDetail details = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT).getDetails(); + EvaluationDetail details = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()).getAnyType(); assertEquals(fromValue(LDValue.of(true), 1, EvaluationReason.ruleMatch(1, "rule2")), details); } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorOperatorsParameterizedTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorOperatorsParameterizedTest.java index 9dec6a0ba..1445cce23 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorOperatorsParameterizedTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorOperatorsParameterizedTest.java @@ -180,7 +180,7 @@ public void parameterizedTestComparison() { assertEquals("without preprocessing", shouldBe, Evaluator.clauseMatchAny(clause1, userValue)); Clause clause2 = new Clause(userAttr, op, values, false); - EvaluatorPreprocessing.preprocessClause(clause2); + DataModelPreprocessing.preprocessClause(clause2); assertEquals("without preprocessing", shouldBe, Evaluator.clauseMatchAny(clause2, userValue)); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java index f3a658956..10825353d 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java @@ -1,6 +1,5 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; @@ -11,12 +10,11 @@ import org.junit.Test; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.clause; import static com.launchdarkly.sdk.server.ModelBuilders.emptyRollout; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyIterable; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; @@ -41,7 +39,7 @@ private RuleBuilder buildTestRule(String id, DataModel.Clause... clauses) { } @Test - public void ruleMatchReasonInstanceIsReusedForSameRule() { + public void ruleMatchResultInstanceIsReusedForSameRule() { DataModel.Clause clause0 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("wrongkey")); DataModel.Clause clause1 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); DataModel.Rule rule0 = buildTestRule("ruleid0", clause0).build(); @@ -51,19 +49,19 @@ public void ruleMatchReasonInstanceIsReusedForSameRule() { LDUser user = new LDUser.Builder("userkey").build(); LDUser otherUser = new LDUser.Builder("wrongkey").build(); - Evaluator.EvalResult sameResult0 = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); - Evaluator.EvalResult sameResult1 = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); - Evaluator.EvalResult otherResult = BASE_EVALUATOR.evaluate(f, otherUser, EventFactory.DEFAULT); + EvalResult sameResult0 = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); + EvalResult sameResult1 = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); + EvalResult otherResult = BASE_EVALUATOR.evaluate(f, otherUser, expectNoPrerequisiteEvals()); - assertEquals(EvaluationReason.ruleMatch(1, "ruleid1"), sameResult0.getDetails().getReason()); - assertSame(sameResult0.getDetails().getReason(), sameResult1.getDetails().getReason()); + assertEquals(EvaluationReason.ruleMatch(1, "ruleid1"), sameResult0.getReason()); + assertSame(sameResult0, sameResult1); - assertEquals(EvaluationReason.ruleMatch(0, "ruleid0"), otherResult.getDetails().getReason()); + assertEquals(EvaluationReason.ruleMatch(0, "ruleid0"), otherResult.getReason()); } @Test - public void ruleMatchReasonInstanceCanBeCreatedFromScratch() { - // Normally we will always do the preprocessing step that creates the reason instances ahead of time, + public void ruleMatchResultInstanceCanBeCreatedFromScratch() { + // Normally we will always do the preprocessing step that creates the result instances ahead of time, // but if somehow we didn't, it should create them as needed DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); DataModel.Rule rule = buildTestRule("ruleid", clause).build(); @@ -72,14 +70,14 @@ public void ruleMatchReasonInstanceCanBeCreatedFromScratch() { DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule) .disablePreprocessing(true) .build(); - assertNull(f.getRules().get(0).getRuleMatchReason()); + assertNull(f.getRules().get(0).preprocessed); - Evaluator.EvalResult result1 = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); - Evaluator.EvalResult result2 = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); + EvalResult result1 = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); + EvalResult result2 = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - assertEquals(EvaluationReason.ruleMatch(0, "ruleid"), result1.getDetails().getReason()); - assertNotSame(result1.getDetails().getReason(), result2.getDetails().getReason()); // they were created individually - assertEquals(result1.getDetails().getReason(), result2.getDetails().getReason()); // but they're equal + assertEquals(EvaluationReason.ruleMatch(0, "ruleid"), result1.getReason()); + assertNotSame(result1, result2); // they were created individually + assertEquals(result1, result2); // but they're equal } @Test @@ -88,10 +86,9 @@ public void ruleWithTooHighVariationReturnsMalformedFlagError() { DataModel.Rule rule = buildTestRule("ruleid", clause).variation(999).build(); DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); LDUser user = new LDUser.Builder("userkey").build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -100,10 +97,9 @@ public void ruleWithNegativeVariationReturnsMalformedFlagError() { DataModel.Rule rule = buildTestRule("ruleid", clause).variation(-1).build(); DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); LDUser user = new LDUser.Builder("userkey").build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -112,10 +108,9 @@ public void ruleWithNoVariationOrRolloutReturnsMalformedFlagError() { DataModel.Rule rule = buildTestRule("ruleid", clause).variation(null).build(); DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); LDUser user = new LDUser.Builder("userkey").build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -124,9 +119,8 @@ public void ruleWithRolloutWithEmptyVariationsListReturnsMalformedFlagError() { DataModel.Rule rule = buildTestRule("ruleid", clause).variation(null).rollout(emptyRollout()).build(); DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); LDUser user = new LDUser.Builder("userkey").build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java index 90b02a9bc..e5f730f22 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.evaluatorBuilder; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.booleanFlagWithClauses; import static com.launchdarkly.sdk.server.ModelBuilders.clause; import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; @@ -114,6 +115,6 @@ private static boolean segmentMatchesUser(DataModel.Segment segment, LDUser user DataModel.Clause clause = clause(null, DataModel.Operator.segmentMatch, LDValue.of(segment.getKey())); DataModel.FeatureFlag flag = booleanFlagWithClauses("flag", clause); Evaluator e = evaluatorBuilder().withStoredSegments(segment).build(); - return e.evaluate(flag, user, EventFactory.DEFAULT).getValue().booleanValue(); + return e.evaluate(flag, user, expectNoPrerequisiteEvals()).getValue().booleanValue(); } } \ No newline at end of file diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java index 6864c9bf3..9ddd3411d 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java @@ -1,7 +1,6 @@ package com.launchdarkly.sdk.server; import com.google.common.collect.Iterables; -import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; @@ -10,24 +9,24 @@ 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 com.launchdarkly.sdk.server.ModelBuilders.FlagBuilder; -import com.launchdarkly.sdk.server.interfaces.Event; + +import org.junit.Test; import java.util.ArrayList; import java.util.List; -import org.junit.Test; import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; -import static com.launchdarkly.sdk.EvaluationDetail.fromValue; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.evaluatorBuilder; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.clause; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.prerequisite; import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.target; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyIterable; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; @@ -87,19 +86,17 @@ private static int versionFromKey(String flagKey) { @Test public void evaluationReturnsErrorIfUserIsNull() throws Exception { DataModel.FeatureFlag f = flagBuilder("feature").build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, null, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, null, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED), result); } @Test public void evaluationReturnsErrorIfUserKeyIsNull() throws Exception { DataModel.FeatureFlag f = flagBuilder("feature").build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, new LDUser(null), EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, new LDUser(null), expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED), result); } @Test @@ -107,10 +104,9 @@ public void flagReturnsOffVariationIfFlagIsOff() throws Exception { DataModel.FeatureFlag f = buildThreeWayFlag("feature") .on(false) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(fromValue(OFF_VALUE, OFF_VARIATION, EvaluationReason.off()), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, EvaluationReason.off()), result); } @Test @@ -119,10 +115,9 @@ public void flagReturnsNullIfFlagIsOffAndOffVariationIsUnspecified() throws Exce .on(false) .offVariation(null) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(fromValue(LDValue.ofNull(), NO_VARIATION, EvaluationReason.off()), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.of(LDValue.ofNull(), NO_VARIATION, EvaluationReason.off()), result); } @Test @@ -131,10 +126,9 @@ public void flagReturnsErrorIfFlagIsOffAndOffVariationIsTooHigh() throws Excepti .on(false) .offVariation(999) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -143,10 +137,9 @@ public void flagReturnsErrorIfFlagIsOffAndOffVariationIsNegative() throws Except .on(false) .offVariation(-1) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -158,7 +151,7 @@ public void flagReturnsInExperimentForFallthroughWhenInExperimentVariation() thr .on(true) .fallthrough(vr) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assert(result.getReason().isInExperiment()); } @@ -172,7 +165,7 @@ public void flagReturnsNotInExperimentForFallthroughWhenNotInExperimentVariation .on(true) .fallthrough(vr) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assert(!result.getReason().isInExperiment()); } @@ -186,7 +179,7 @@ public void flagReturnsNotInExperimentForFallthrougWhenInExperimentVariationButN .on(true) .fallthrough(vr) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assert(!result.getReason().isInExperiment()); } @@ -202,7 +195,7 @@ public void flagReturnsInExperimentForRuleMatchWhenInExperimentVariation() throw .on(true) .rules(rule) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assert(result.getReason().isInExperiment()); } @@ -218,7 +211,7 @@ public void flagReturnsNotInExperimentForRuleMatchWhenNotInExperimentVariation() .on(true) .rules(rule) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assert(!result.getReason().isInExperiment()); } @@ -228,10 +221,23 @@ public void flagReturnsFallthroughIfFlagIsOnAndThereAreNoRules() throws Exceptio DataModel.FeatureFlag f = buildThreeWayFlag("feature") .on(true) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); + + assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); + } + + @Test + public void fallthroughResultHasForceReasonTrackingTrueIfTrackEventsFallthroughIstrue() throws Exception { + DataModel.FeatureFlag f = buildThreeWayFlag("feature") + .on(true) + .trackEventsFallthrough(true) + .build(); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(fromValue(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals( + EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()) + .withForceReasonTracking(true), + result); } @Test @@ -240,10 +246,9 @@ public void flagReturnsErrorIfFallthroughHasTooHighVariation() throws Exception .on(true) .fallthroughVariation(999) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -252,10 +257,9 @@ public void flagReturnsErrorIfFallthroughHasNegativeVariation() throws Exception .on(true) .fallthroughVariation(-1) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -264,10 +268,9 @@ public void flagReturnsErrorIfFallthroughHasNeitherVariationNorRollout() throws .on(true) .fallthrough(new DataModel.VariationOrRollout(null, null)) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -276,10 +279,9 @@ public void flagReturnsErrorIfFallthroughHasEmptyRolloutVariationList() throws E .on(true) .fallthrough(new DataModel.VariationOrRollout(null, ModelBuilders.emptyRollout())) .build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); - assertEquals(EvaluationDetail.error(EvaluationReason.ErrorKind.MALFORMED_FLAG, null), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test @@ -289,11 +291,10 @@ public void flagReturnsOffVariationIfPrerequisiteIsNotFound() throws Exception { .prerequisites(prerequisite("feature1", 1)) .build(); Evaluator e = evaluatorBuilder().withNonexistentFlag("feature1").build(); - Evaluator.EvalResult result = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); + EvalResult result = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); - assertEquals(fromValue(OFF_VALUE, OFF_VARIATION, expectedReason), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); } @Test @@ -308,18 +309,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(); - Evaluator.EvalResult result = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); + PrereqRecorder recordPrereqs = new PrereqRecorder(); + EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); - assertEquals(fromValue(OFF_VALUE, OFF_VARIATION, expectedReason), result.getDetails()); - - assertEquals(1, Iterables.size(result.getPrerequisiteEvents())); - Event.FeatureRequest event = Iterables.get(result.getPrerequisiteEvents(), 0); - assertEquals(f1.getKey(), event.getKey()); - assertEquals(GREEN_VARIATION, event.getVariation()); - assertEquals(GREEN_VALUE, event.getValue()); - assertEquals(f1.getVersion(), event.getVersion()); - assertEquals(f0.getKey(), event.getPrereqOf()); + assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); + + assertEquals(1, Iterables.size(recordPrereqs.evals)); + PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(f1, eval.flag); + assertEquals(f0, eval.prereqOfFlag); + assertEquals(GREEN_VARIATION, eval.result.getVariationIndex()); + assertEquals(GREEN_VALUE, eval.result.getValue()); } @Test @@ -333,33 +334,33 @@ public void flagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() throws Excep .fallthroughVariation(RED_VARIATION) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); - Evaluator.EvalResult result = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); + PrereqRecorder recordPrereqs = new PrereqRecorder(); + EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); - assertEquals(fromValue(OFF_VALUE, OFF_VARIATION, expectedReason), result.getDetails()); - - assertEquals(1, Iterables.size(result.getPrerequisiteEvents())); - Event.FeatureRequest event = Iterables.get(result.getPrerequisiteEvents(), 0); - assertEquals(f1.getKey(), event.getKey()); - assertEquals(RED_VARIATION, event.getVariation()); - assertEquals(RED_VALUE, event.getValue()); - assertEquals(f1.getVersion(), event.getVersion()); - assertEquals(f0.getKey(), event.getPrereqOf()); + assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); + + assertEquals(1, Iterables.size(recordPrereqs.evals)); + PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(f1, eval.flag); + assertEquals(f0, eval.prereqOfFlag); + assertEquals(RED_VARIATION, eval.result.getVariationIndex()); + assertEquals(RED_VALUE, eval.result.getValue()); } @Test - public void prerequisiteFailedReasonInstanceIsReusedForSamePrerequisite() throws Exception { + public void prerequisiteFailedResultInstanceIsReusedForSamePrerequisite() throws Exception { DataModel.FeatureFlag f0 = buildThreeWayFlag("feature") .on(true) .prerequisites(prerequisite("feature1", GREEN_VARIATION)) .build(); Evaluator e = evaluatorBuilder().withNonexistentFlag("feature1").build(); - Evaluator.EvalResult result0 = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); - Evaluator.EvalResult result1 = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); + EvalResult result0 = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); + EvalResult result1 = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); - assertEquals(expectedReason, result0.getDetails().getReason()); - assertSame(result0.getDetails().getReason(), result1.getDetails().getReason()); + assertEquals(expectedReason, result0.getReason()); + assertSame(result0, result1); } @Test @@ -371,16 +372,16 @@ public void prerequisiteFailedReasonInstanceCanBeCreatedFromScratch() throws Exc .prerequisites(prerequisite("feature1", GREEN_VARIATION)) .disablePreprocessing(true) .build(); - assertNull(f0.getPrerequisites().get(0).getPrerequisiteFailedReason()); + assertNull(f0.getPrerequisites().get(0).preprocessed); Evaluator e = evaluatorBuilder().withNonexistentFlag("feature1").build(); - Evaluator.EvalResult result0 = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); - Evaluator.EvalResult result1 = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); + EvalResult result0 = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); + EvalResult result1 = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); - assertEquals(expectedReason, result0.getDetails().getReason()); - assertNotSame(result0.getDetails().getReason(), result1.getDetails().getReason()); // they were created individually - assertEquals(result0.getDetails().getReason(), result1.getDetails().getReason()); // but they're equal + assertEquals(expectedReason, result0.getReason()); + assertNotSame(result0.getReason(), result1.getReason()); // they were created individually + assertEquals(result0.getReason(), result1.getReason()); // but they're equal } @Test @@ -395,17 +396,17 @@ public void flagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAr .version(2) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); - Evaluator.EvalResult result = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); + PrereqRecorder recordPrereqs = new PrereqRecorder(); + EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); - assertEquals(fromValue(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result.getDetails()); + assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); - assertEquals(1, Iterables.size(result.getPrerequisiteEvents())); - Event.FeatureRequest event = Iterables.get(result.getPrerequisiteEvents(), 0); - assertEquals(f1.getKey(), event.getKey()); - assertEquals(GREEN_VARIATION, event.getVariation()); - assertEquals(GREEN_VALUE, event.getValue()); - assertEquals(f1.getVersion(), event.getVersion()); - assertEquals(f0.getKey(), event.getPrereqOf()); + assertEquals(1, Iterables.size(recordPrereqs.evals)); + PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(f1, eval.flag); + assertEquals(f0, eval.prereqOfFlag); + assertEquals(GREEN_VARIATION, eval.result.getVariationIndex()); + assertEquals(GREEN_VALUE, eval.result.getValue()); } @Test @@ -424,24 +425,24 @@ public void multipleLevelsOfPrerequisitesProduceMultipleEvents() throws Exceptio .fallthroughVariation(GREEN_VARIATION) .build(); Evaluator e = evaluatorBuilder().withStoredFlags(f1, f2).build(); - Evaluator.EvalResult result = e.evaluate(f0, BASE_USER, EventFactory.DEFAULT); + PrereqRecorder recordPrereqs = new PrereqRecorder(); + EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); - assertEquals(fromValue(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result.getDetails()); - assertEquals(2, Iterables.size(result.getPrerequisiteEvents())); + assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); + + assertEquals(2, Iterables.size(recordPrereqs.evals)); - Event.FeatureRequest event0 = Iterables.get(result.getPrerequisiteEvents(), 0); - assertEquals(f2.getKey(), event0.getKey()); - assertEquals(GREEN_VARIATION, event0.getVariation()); - assertEquals(GREEN_VALUE, event0.getValue()); - assertEquals(f2.getVersion(), event0.getVersion()); - assertEquals(f1.getKey(), event0.getPrereqOf()); + PrereqEval eval0 = recordPrereqs.evals.get(0); + assertEquals(f2, eval0.flag); + assertEquals(f1, eval0.prereqOfFlag); + assertEquals(GREEN_VARIATION, eval0.result.getVariationIndex()); + assertEquals(GREEN_VALUE, eval0.result.getValue()); - Event.FeatureRequest event1 = Iterables.get(result.getPrerequisiteEvents(), 1); - assertEquals(f1.getKey(), event1.getKey()); - assertEquals(GREEN_VARIATION, event1.getVariation()); - assertEquals(GREEN_VALUE, event1.getValue()); - assertEquals(f1.getVersion(), event1.getVersion()); - assertEquals(f0.getKey(), event1.getPrereqOf()); + PrereqEval eval1 = recordPrereqs.evals.get(1); + assertEquals(f1, eval1.flag); + assertEquals(f0, eval1.prereqOfFlag); + assertEquals(GREEN_VARIATION, eval1.result.getVariationIndex()); + assertEquals(GREEN_VALUE, eval1.result.getValue()); } @Test @@ -451,10 +452,9 @@ public void flagMatchesUserFromTargets() throws Exception { .targets(target(2, "whoever", "userkey")) .build(); LDUser user = new LDUser.Builder("userkey").build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - assertEquals(fromValue(MATCH_VALUE, MATCH_VARIATION, EvaluationReason.targetMatch()), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals(EvalResult.of(MATCH_VALUE, MATCH_VARIATION, EvaluationReason.targetMatch()), result); } @Test @@ -470,16 +470,37 @@ public void flagMatchesUserFromRules() { .build(); LDUser user = new LDUser.Builder("userkey").build(); - Evaluator.EvalResult result = BASE_EVALUATOR.evaluate(f, user, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); + + assertEquals(EvalResult.of(MATCH_VALUE, MATCH_VARIATION, EvaluationReason.ruleMatch(1, "ruleid1")), result); + } + + @Test + public void ruleMatchReasonHasTrackReasonTrueIfRuleLevelTrackEventsIsTrue() { + DataModel.Clause clause0 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("wrongkey")); + DataModel.Clause clause1 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); + DataModel.Rule rule0 = ruleBuilder().id("ruleid0").clauses(clause0).variation(2).build(); + DataModel.Rule rule1 = ruleBuilder().id("ruleid1").clauses(clause1).variation(2) + .trackEvents(true).build(); + + DataModel.FeatureFlag f = buildThreeWayFlag("feature") + .on(true) + .rules(rule0, rule1) + .build(); + + LDUser user = new LDUser.Builder("userkey").build(); + EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - assertEquals(fromValue(MATCH_VALUE, MATCH_VARIATION, EvaluationReason.ruleMatch(1, "ruleid1")), result.getDetails()); - assertThat(result.getPrerequisiteEvents(), emptyIterable()); + assertEquals( + EvalResult.of(MATCH_VALUE, MATCH_VARIATION, EvaluationReason.ruleMatch(1, "ruleid1")) + .withForceReasonTracking(true), + result); } @Test(expected=RuntimeException.class) public void canSimulateErrorUsingTestInstrumentationFlagKey() { // Other tests rely on the ability to simulate an exception in this way DataModel.FeatureFlag badFlag = flagBuilder(Evaluator.INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION).build(); - BASE_EVALUATOR.evaluate(badFlag, BASE_USER, EventFactory.DEFAULT); + BASE_EVALUATOR.evaluate(badFlag, BASE_USER, expectNoPrerequisiteEvals()); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java index af105256a..4a71fcd71 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java @@ -1,8 +1,12 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.server.BigSegmentStoreWrapper.BigSegmentsQueryResult; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; @SuppressWarnings("javadoc") public abstract class EvaluatorTestUtil { @@ -71,4 +75,34 @@ public EvaluatorBuilder withBigSegmentQueryResult(final String userKey, BigSegme return this; } } + + public static Evaluator.PrerequisiteEvaluationSink expectNoPrerequisiteEvals() { + return (f1, f2, u, r) -> { + throw new AssertionError("did not expect any prerequisite evaluations, but got one"); + }; + } + + public static final class PrereqEval { + public final FeatureFlag flag; + public final FeatureFlag prereqOfFlag; + public final LDUser user; + public final EvalResult result; + + public PrereqEval(FeatureFlag flag, FeatureFlag prereqOfFlag, LDUser user, EvalResult result) { + this.flag = flag; + this.prereqOfFlag = prereqOfFlag; + this.user = user; + this.result = result; + } + } + + public static final class PrereqRecorder implements Evaluator.PrerequisiteEvaluationSink { + public final List evals = new ArrayList(); + + @Override + public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDUser user, + EvalResult result) { + evals.add(new PrereqEval(flag, prereqOfFlag, user, result)); + } + } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EventFactoryTest.java b/src/test/java/com/launchdarkly/sdk/server/EventFactoryTest.java index ca50c76ca..213d710d0 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EventFactoryTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EventFactoryTest.java @@ -1,80 +1,113 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.server.DataModel.Rollout; -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.EvaluationReason; +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.interfaces.Event.FeatureRequest; import org.junit.Test; -import static com.launchdarkly.sdk.server.ModelBuilders.*; - -import java.util.ArrayList; -import java.util.List; - -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; +import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public class EventFactoryTest { private static final LDUser BASE_USER = new LDUser.Builder("x").build(); - private static Rollout buildRollout(boolean isExperiment, boolean untrackedVariations) { - List variations = new ArrayList<>(); - variations.add(new WeightedVariation(1, 50000, untrackedVariations)); - variations.add(new WeightedVariation(2, 50000, untrackedVariations)); - UserAttribute bucketBy = UserAttribute.KEY; - RolloutKind kind = isExperiment ? RolloutKind.experiment : RolloutKind.rollout; - Integer seed = 123; - Rollout rollout = new Rollout(variations, bucketBy, kind, seed); - return rollout; + private static final LDValue SOME_VALUE = LDValue.of("value"); + private static final int SOME_VARIATION = 11; + private static final EvaluationReason SOME_REASON = EvaluationReason.fallthrough(); + private static final EvalResult SOME_RESULT = EvalResult.of(SOME_VALUE, SOME_VARIATION, SOME_REASON); + private static final LDValue DEFAULT_VALUE = LDValue.of("default"); + + @Test + public void flagKeyIsSetInFeatureEvent() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); + + assertEquals(flag.getKey(), fr.getKey()); } @Test - public void trackEventFalseTest() { - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(false).build(); - LDUser user = new LDUser("moniker"); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, null, null); + public void flagVersionIsSetInFeatureEvent() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - assert(!fr.isTrackEvents()); + assertEquals(flag.getVersion(), fr.getVersion()); } + + @Test + public void userIsSetInFeatureEvent() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); + assertEquals(BASE_USER, fr.getUser()); + } + @Test - public void trackEventTrueTest() { - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); - LDUser user = new LDUser("moniker"); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, null, null); + public void valueIsSetInFeatureEvent() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - assert(fr.isTrackEvents()); + assertEquals(SOME_VALUE, fr.getValue()); + } + + @Test + public void variationIsSetInFeatureEvent() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); + + assertEquals(SOME_VARIATION, fr.getVariation()); + } + + @Test + public void reasonIsNormallyNotIncludedWithDefaultEventFactory() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); + + assertNull(fr.getReason()); + } + + @Test + public void reasonIsIncludedWithEventFactoryThatIsConfiguredToIncludedReasons() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT_WITH_REASONS.newFeatureRequestEvent( + flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); + + assertEquals(SOME_REASON, fr.getReason()); } @Test - public void trackEventTrueWhenTrackEventsFalseButExperimentFallthroughReasonTest() { - Rollout rollout = buildRollout(true, false); - VariationOrRollout vr = new VariationOrRollout(null, rollout); + public void reasonIsIncludedIfForceReasonTrackingIsTrue() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, + SOME_RESULT.withForceReasonTracking(true), DEFAULT_VALUE); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(false) - .fallthrough(vr).build(); - LDUser user = new LDUser("moniker"); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, null, 0, - EvaluationReason.fallthrough(true), null, null); + assertEquals(SOME_REASON, fr.getReason()); + } + @Test + public void trackEventsIsNormallyFalse() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); + + assert(!fr.isTrackEvents()); + } + + @Test + public void trackEventsIsTrueIfItIsTrueInFlag() { + FeatureFlag flag = flagBuilder("flagkey") + .trackEvents(true) + .build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); assert(fr.isTrackEvents()); } @Test - public void trackEventTrueWhenTrackEventsFalseButExperimentRuleMatchReasonTest() { - Rollout rollout = buildRollout(true, false); - - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of(BASE_USER.getKey())); - DataModel.Rule rule = ruleBuilder().id("ruleid0").clauses(clause).rollout(rollout).build(); - - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(false) - .rules(rule).build(); - LDUser user = new LDUser("moniker"); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, null, 0, - EvaluationReason.ruleMatch(0, "something", true), null, null); + public void trackEventsIsTrueIfForceReasonTrackingIsTrue() { + FeatureFlag flag = flagBuilder("flagkey").build(); + FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, + SOME_RESULT.withForceReasonTracking(true), DEFAULT_VALUE); assert(fr.isTrackEvents()); } diff --git a/src/test/java/com/launchdarkly/sdk/server/EventOutputTest.java b/src/test/java/com/launchdarkly/sdk/server/EventOutputTest.java index 15165abac..3d0240595 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EventOutputTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EventOutputTest.java @@ -82,7 +82,7 @@ public void userKeyIsSetInsteadOfUserWhenNotInlined() throws Exception { Event.FeatureRequest featureEvent = EventFactory.DEFAULT.newFeatureRequestEvent( flagBuilder("flag").build(), user, - new Evaluator.EvalResult(LDValue.ofNull(), NO_VARIATION, EvaluationReason.off()), + EvalResult.of(LDValue.ofNull(), NO_VARIATION, EvaluationReason.off()), LDValue.ofNull()); LDValue outputEvent = getSingleOutputEvent(f, featureEvent); assertEquals(LDValue.ofNull(), outputEvent.get("user")); @@ -189,7 +189,7 @@ public void featureEventIsSerialized() throws Exception { EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); FeatureRequest feWithVariation = factory.newFeatureRequestEvent(flag, user, - new Evaluator.EvalResult(LDValue.of("flagvalue"), 1, EvaluationReason.off()), + EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.off()), LDValue.of("defaultvalue")); LDValue feJson1 = buildFeatureEventProps("flag") .put("version", 11) @@ -200,7 +200,7 @@ public void featureEventIsSerialized() throws Exception { assertEquals(feJson1, getSingleOutputEvent(f, feWithVariation)); FeatureRequest feWithoutVariationOrDefault = factory.newFeatureRequestEvent(flag, user, - new Evaluator.EvalResult(LDValue.of("flagvalue"), NO_VARIATION, EvaluationReason.off()), + EvalResult.of(LDValue.of("flagvalue"), NO_VARIATION, EvaluationReason.off()), LDValue.ofNull()); LDValue feJson2 = buildFeatureEventProps("flag") .put("version", 11) @@ -209,7 +209,7 @@ public void featureEventIsSerialized() throws Exception { assertEquals(feJson2, getSingleOutputEvent(f, feWithoutVariationOrDefault)); FeatureRequest feWithReason = factoryWithReason.newFeatureRequestEvent(flag, user, - new Evaluator.EvalResult(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), + EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), LDValue.of("defaultvalue")); LDValue feJson3 = buildFeatureEventProps("flag") .put("version", 11) @@ -245,7 +245,7 @@ public void featureEventIsSerialized() throws Exception { DataModel.FeatureFlag parentFlag = flagBuilder("parent").build(); Event.FeatureRequest prereqEvent = factory.newPrerequisiteFeatureRequestEvent(flag, user, - new Evaluator.EvalResult(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), parentFlag); + EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), parentFlag); LDValue feJson6 = buildFeatureEventProps("flag") .put("version", 11) .put("variation", 1) @@ -255,7 +255,7 @@ public void featureEventIsSerialized() throws Exception { assertEquals(feJson6, getSingleOutputEvent(f, prereqEvent)); Event.FeatureRequest prereqWithReason = factoryWithReason.newPrerequisiteFeatureRequestEvent(flag, user, - new Evaluator.EvalResult(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), parentFlag); + EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), parentFlag); LDValue feJson7 = buildFeatureEventProps("flag") .put("version", 11) .put("variation", 1) @@ -274,7 +274,7 @@ public void featureEventIsSerialized() throws Exception { assertEquals(feJson8, getSingleOutputEvent(f, prereqWithoutResult)); FeatureRequest anonFeWithVariation = factory.newFeatureRequestEvent(flag, anon, - new Evaluator.EvalResult(LDValue.of("flagvalue"), 1, EvaluationReason.off()), + EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.off()), LDValue.of("defaultvalue")); LDValue anonFeJson1 = buildFeatureEventProps("flag", "anonymouskey") .put("version", 11) @@ -519,7 +519,7 @@ private void testInlineUserSerialization(LDUser user, LDValue expectedJsonValue, Event.FeatureRequest featureEvent = EventFactory.DEFAULT.newFeatureRequestEvent( flagBuilder("flag").build(), user, - new Evaluator.EvalResult(LDValue.ofNull(), NO_VARIATION, EvaluationReason.off()), + EvalResult.of(LDValue.ofNull(), NO_VARIATION, EvaluationReason.off()), LDValue.ofNull()); LDValue outputEvent = getSingleOutputEvent(f, featureEvent); assertEquals(LDValue.ofNull(), outputEvent.get("userKey")); diff --git a/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java b/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java index 23f660668..dc67b3242 100644 --- a/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/FeatureFlagsStateTest.java @@ -179,11 +179,11 @@ public void canConvertFromJson() throws SerializationException { } private static FeatureFlagsState makeInstanceForSerialization() { - Evaluator.EvalResult eval1 = new Evaluator.EvalResult(LDValue.of("value1"), 0, EvaluationReason.off()); + EvalResult eval1 = EvalResult.of(LDValue.of("value1"), 0, EvaluationReason.off()); DataModel.FeatureFlag flag1 = flagBuilder("key1").version(100).trackEvents(false).build(); - Evaluator.EvalResult eval2 = new Evaluator.EvalResult(LDValue.of("value2"), 1, EvaluationReason.fallthrough()); + EvalResult eval2 = EvalResult.of(LDValue.of("value2"), 1, EvaluationReason.fallthrough()); DataModel.FeatureFlag flag2 = flagBuilder("key2").version(200).trackEvents(true).debugEventsUntilDate(1000L).build(); - Evaluator.EvalResult eval3 = new Evaluator.EvalResult(LDValue.ofNull(), NO_VARIATION, EvaluationReason.error(MALFORMED_FLAG)); + EvalResult eval3 = EvalResult.of(LDValue.ofNull(), NO_VARIATION, EvaluationReason.error(MALFORMED_FLAG)); DataModel.FeatureFlag flag3 = flagBuilder("key3").version(300).build(); return FeatureFlagsState.builder(FlagsStateOption.WITH_REASONS) .addFlag(flag1, eval1).addFlag(flag2, eval2).addFlag(flag3, eval3).build(); diff --git a/src/test/java/com/launchdarkly/sdk/server/FlagModelDeserializationTest.java b/src/test/java/com/launchdarkly/sdk/server/FlagModelDeserializationTest.java index 83d0dea83..24e437483 100644 --- a/src/test/java/com/launchdarkly/sdk/server/FlagModelDeserializationTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/FlagModelDeserializationTest.java @@ -1,35 +1,56 @@ package com.launchdarkly.sdk.server; import com.google.gson.Gson; -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.server.DataModel; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.server.DataModel.Clause; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Operator; +import com.launchdarkly.sdk.server.DataModel.Prerequisite; +import com.launchdarkly.sdk.server.DataModel.Rule; +import com.launchdarkly.sdk.server.DataModel.Target; import org.junit.Test; -import static org.junit.Assert.assertEquals; +import static com.launchdarkly.sdk.server.ModelBuilders.clause; +import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; +import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; +import static com.launchdarkly.sdk.server.ModelBuilders.target; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertNotNull; @SuppressWarnings("javadoc") public class FlagModelDeserializationTest { private static final Gson gson = new Gson(); + // The details of the preprocessed data are verified by DataModelPreprocessingTest; here we're + // just verifying that the preprocessing is actually being done whenever we deserialize a flag. @Test - public void precomputedReasonsAreAddedToPrerequisites() { - String flagJson = "{\"key\":\"flagkey\",\"prerequisites\":[{\"key\":\"prereq0\"},{\"key\":\"prereq1\"}]}"; - DataModel.FeatureFlag flag = gson.fromJson(flagJson, DataModel.FeatureFlag.class); - assertNotNull(flag.getPrerequisites()); - assertEquals(2, flag.getPrerequisites().size()); - assertEquals(EvaluationReason.prerequisiteFailed("prereq0"), flag.getPrerequisites().get(0).getPrerequisiteFailedReason()); - assertEquals(EvaluationReason.prerequisiteFailed("prereq1"), flag.getPrerequisites().get(1).getPrerequisiteFailedReason()); - } - - @Test - public void precomputedReasonsAreAddedToRules() { - String flagJson = "{\"key\":\"flagkey\",\"rules\":[{\"id\":\"ruleid0\"},{\"id\":\"ruleid1\"}]}"; - DataModel.FeatureFlag flag = gson.fromJson(flagJson, DataModel.FeatureFlag.class); - assertNotNull(flag.getRules()); - assertEquals(2, flag.getRules().size()); - assertEquals(EvaluationReason.ruleMatch(0, "ruleid0"), flag.getRules().get(0).getRuleMatchReason()); - assertEquals(EvaluationReason.ruleMatch(1, "ruleid1"), flag.getRules().get(1).getRuleMatchReason()); + public void preprocessingIsDoneOnDeserialization() { + FeatureFlag originalFlag = flagBuilder("flagkey") + .variations("a", "b") + .prerequisites(new Prerequisite("abc", 0)) + .targets(target(0, "x")) + .rules(ruleBuilder().clauses( + clause(UserAttribute.KEY, Operator.in, LDValue.of("x"), LDValue.of("y")) + ).build()) + .build(); + String flagJson = JsonHelpers.serialize(originalFlag); + + FeatureFlag flag = gson.fromJson(flagJson, FeatureFlag.class); + assertNotNull(flag.preprocessed); + for (Prerequisite p: flag.getPrerequisites()) { + assertNotNull(p.preprocessed); + } + for (Target t: flag.getTargets()) { + assertNotNull(t.preprocessed); + } + for (Rule r: flag.getRules()) { + assertThat(r.preprocessed, notNullValue()); + for (Clause c: r.getClauses()) { + assertThat(c.preprocessed, notNullValue()); + } + } } } diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientEventTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientEventTest.java index 4408af8b1..6f992ff59 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientEventTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientEventTest.java @@ -4,6 +4,7 @@ import com.launchdarkly.sdk.EvaluationReason.ErrorKind; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.Prerequisite; import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.Event; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; @@ -21,6 +22,10 @@ import static com.launchdarkly.sdk.server.TestComponents.specificDataStore; import static com.launchdarkly.sdk.server.TestComponents.specificEventProcessor; import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasKey; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -561,6 +566,49 @@ public void aliasEventIsCorrectlyGenerated() { assertEquals("anonymousUser", evt.getPreviousContextKind()); } + @Test + public void allFlagsStateGeneratesNoEvaluationEvents() { + DataModel.FeatureFlag flag = flagBuilder("flag") + .on(true) + .fallthrough(fallthroughVariation(0)) + .offVariation(1) + .variations(LDValue.of(true), LDValue.of(false)) + .version(1) + .build(); + upsertFlag(dataStore, flag); + + FeatureFlagsState state = client.allFlagsState(user); + assertThat(state.toValuesMap(), hasKey(flag.getKey())); + + assertThat(eventSink.events, empty()); + } + + @Test + public void allFlagsStateGeneratesNoPrerequisiteEvaluationEvents() { + DataModel.FeatureFlag flag1 = flagBuilder("flag1") + .on(true) + .fallthrough(fallthroughVariation(0)) + .offVariation(1) + .variations(LDValue.of(true), LDValue.of(false)) + .version(1) + .build(); + DataModel.FeatureFlag flag0 = flagBuilder("flag0") + .on(true) + .fallthrough(fallthroughVariation(0)) + .offVariation(1) + .variations(LDValue.of(true), LDValue.of(false)) + .prerequisites(new Prerequisite(flag1.getKey(), 0)) + .version(1) + .build(); + upsertFlag(dataStore, flag1); + upsertFlag(dataStore, flag0); + + FeatureFlagsState state = client.allFlagsState(user); + assertThat(state.toValuesMap(), allOf(hasKey(flag0.getKey()), hasKey(flag1.getKey()))); + + assertThat(eventSink.events, empty()); + } + private void checkFeatureEvent(Event e, DataModel.FeatureFlag flag, LDValue value, LDValue defaultVal, String prereqOf, EvaluationReason reason) { assertEquals(Event.FeatureRequest.class, e.getClass()); diff --git a/src/test/java/com/launchdarkly/sdk/server/ModelBuilders.java b/src/test/java/com/launchdarkly/sdk/server/ModelBuilders.java index ef38b83c9..ad4b1c1f0 100644 --- a/src/test/java/com/launchdarkly/sdk/server/ModelBuilders.java +++ b/src/test/java/com/launchdarkly/sdk/server/ModelBuilders.java @@ -197,6 +197,15 @@ FlagBuilder variations(boolean... variations) { this.variations = values; return this; } + + FlagBuilder variations(String... variations) { + List values = new ArrayList<>(); + for (String v: variations) { + values.add(LDValue.of(v)); + } + this.variations = values; + return this; + } FlagBuilder generatedVariations(int numVariations) { variations.clear(); diff --git a/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java b/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java index 6614c5108..687aeacef 100644 --- a/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java @@ -7,7 +7,6 @@ import com.launchdarkly.sdk.server.DataModel.Rollout; import com.launchdarkly.sdk.server.DataModel.RolloutKind; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; -import com.launchdarkly.sdk.server.Evaluator.EvalResult; import org.junit.Test; @@ -15,6 +14,7 @@ import java.util.List; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; @@ -75,7 +75,7 @@ private static void assertVariationIndexAndExperimentStateForRollout( .fallthrough(rollout) .salt(salt) .build(); - EvalResult result = BASE_EVALUATOR.evaluate(flag, user, EventFactory.DEFAULT); + EvalResult result = BASE_EVALUATOR.evaluate(flag, user, expectNoPrerequisiteEvals()); assertThat(result.getVariationIndex(), equalTo(expectedVariation)); assertThat(result.getReason().getKind(), equalTo(EvaluationReason.Kind.FALLTHROUGH)); assertThat(result.getReason().isInExperiment(), equalTo(expectedInExperiment)); diff --git a/src/test/java/com/launchdarkly/sdk/server/TestUtil.java b/src/test/java/com/launchdarkly/sdk/server/TestUtil.java index 37eaea5ef..5f47ffce5 100644 --- a/src/test/java/com/launchdarkly/sdk/server/TestUtil.java +++ b/src/test/java/com/launchdarkly/sdk/server/TestUtil.java @@ -25,13 +25,13 @@ import java.net.Socket; import java.net.SocketAddress; import java.time.Duration; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.function.Supplier; import java.util.HashSet; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; import javax.net.SocketFactory; @@ -152,8 +152,8 @@ public static void expectEvents(BlockingQueue eve assertNoMoreValues(events, 100, TimeUnit.MILLISECONDS); } - public static Evaluator.EvalResult simpleEvaluation(int variation, LDValue value) { - return new Evaluator.EvalResult(value, variation, EvaluationReason.fallthrough()); + public static EvalResult simpleEvaluation(int variation, LDValue value) { + return EvalResult.of(value, variation, EvaluationReason.fallthrough()); } // returns a socket factory that creates sockets that only connect to host and port