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