All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.launchdarkly.sdk.server.Evaluator Maven / Gradle / Ivy

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.LDUser;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.LDValueType;
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;

/**
 * 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.
 */
class Evaluator {
  private final static Logger logger = Loggers.EVALUATION;

  /**
   * This key cannot exist in LaunchDarkly because it contains invalid characters. We use it in tests as a way to
   * simulate an unexpected RuntimeException during flag evaluations. We check for it by reference equality, so
   * the tests must use this exact constant.
   */
  static final String INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION = "$ test error flag $";
  static final RuntimeException EXPECTED_EXCEPTION_FROM_INVALID_FLAG = new RuntimeException("deliberate test error");
  
  private final Getters getters;
  
  /**
   * An abstraction of getting flags or segments by key. This ensures that Evaluator cannot modify the data store,
   * and simplifies testing.
   */
  static interface Getters {
    DataModel.FeatureFlag getFlag(String key);
    DataModel.Segment getSegment(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,
   */
  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;
    }
  }
  
  Evaluator(Getters getters) {
    this.getters = getters;
  }

  /**
   * The client's entry point for evaluating a flag. No other Evaluator methods should be exposed.
   * 
   * @param flag an existing feature flag; any other referenced flags or segments will be queried via {@link Getters}
   * @param user the user to evaluate against
   * @param eventFactory produces feature request events
   * @return an {@link EvalResult} - guaranteed non-null
   */
  EvalResult evaluate(DataModel.FeatureFlag flag, LDUser user, EventFactory eventFactory) {
    if (flag.getKey() == INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION) {
      throw EXPECTED_EXCEPTION_FROM_INVALID_FLAG;
    }
    
    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));
    }

    // 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);
    if (prerequisiteEvents != null) {
      result.setPrerequisiteEvents(prerequisiteEvents);
    }
    return result;
  }

  private EvalResult evaluateInternal(DataModel.FeatureFlag flag, LDUser user, EventFactory eventFactory,
      List eventsOut) {
    if (!flag.isOn()) {
      return getOffValue(flag, EvaluationReason.off());
    }
    
    EvaluationReason prereqFailureReason = checkPrerequisites(flag, user, eventFactory, eventsOut);
    if (prereqFailureReason != null) {
      return getOffValue(flag, prereqFailureReason);
    }
    
    // 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());
      }
    }
    // 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)) {
        EvaluationReason precomputedReason = rule.getRuleMatchReason();
        EvaluationReason reason = precomputedReason != null ? precomputedReason : EvaluationReason.ruleMatch(i, rule.getId());
        return getValueForVariationOrRollout(flag, rule, user, reason);
      }
    }
    // Walk through the fallthrough and see if it matches
    return getValueForVariationOrRollout(flag, flag.getFallthrough(), user, EvaluationReason.fallthrough());
  }

  // Checks prerequisites if any; returns null if successful, or an EvaluationReason if we have to
  // short-circuit due to a prerequisite failure.
  private EvaluationReason checkPrerequisites(DataModel.FeatureFlag flag, LDUser user, EventFactory eventFactory,
      List eventsOut) {
    for (DataModel.Prerequisite prereq: flag.getPrerequisites()) { // getPrerequisites() is guaranteed non-null
      boolean prereqOk = true;
      DataModel.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);
        // 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 (!prereqOk) {
        EvaluationReason precomputedReason = prereq.getPrerequisiteFailedReason();
        return precomputedReason != null ? precomputedReason : EvaluationReason.prerequisiteFailed(prereq.getKey());
      }
    }
    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) {
    Integer index = EvaluatorBucketing.variationIndexForUser(vr, user, flag.getKey(), flag.getSalt());
    if (index == null) {
      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, index, reason);
    }
  }

  private boolean ruleMatchesUser(DataModel.FeatureFlag flag, DataModel.Rule rule, LDUser user) {
    for (DataModel.Clause clause: rule.getClauses()) { // getClauses() is guaranteed non-null
      if (!clauseMatchesUser(clause, user)) {
        return false;
      }
    }
    return true;
  }

  private boolean clauseMatchesUser(DataModel.Clause clause, LDUser user) {
    // 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 (segment != null) {
            if (segmentMatchesUser(segment, user)) {
              return maybeNegate(clause, true);
            }
          }
        }
      }
      return maybeNegate(clause, false);
    }
    
    return clauseMatchesUserNoSegments(clause, user);
  }
  
  private boolean clauseMatchesUserNoSegments(DataModel.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()) {
        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;
        }
        if (clauseMatchAny(clause, value)) {
          return maybeNegate(clause, true);
        }
      }
      return maybeNegate(clause, false);
    } else if (userValue.getType() != LDValueType.OBJECT) {
      return maybeNegate(clause, clauseMatchAny(clause, userValue));
    }
    logger.warn("Got unexpected user attribute type \"{}\" for user key \"{}\" and attribute \"{}\"",
        userValue.getType(), user.getKey(), clause.getAttribute());
    return false;
  }
  
  static boolean clauseMatchAny(DataModel.Clause clause, LDValue userValue) {
    DataModel.Operator op = clause.getOp();
    if (op != null) {
      EvaluatorPreprocessing.ClauseExtra preprocessed = clause.getPreprocessed();
      if (op == DataModel.Operator.in) {
        // see if we have precomputed a Set for fast equality matching
        Set vs = preprocessed == null ? null : preprocessed.valuesSet;
        if (vs != null) {
          return vs.contains(userValue);
        }
      }
      List values = clause.getValues();
      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);
        LDValue v = values.get(i);
        if (EvaluatorOperators.apply(op, userValue, v, p)) {
          return true;
        }
      }
    }
    return false;
  }

  private boolean maybeNegate(DataModel.Clause clause, boolean b) {
    return clause.isNegate() ? !b : b;
  }
  
  private boolean segmentMatchesUser(DataModel.Segment segment, LDUser user) {
    String userKey = user.getKey(); // we've already verified that the key is non-null at the top of evaluate()
    if (segment.getIncluded().contains(userKey)) { // getIncluded(), getExcluded(), and getRules() are guaranteed non-null
      return true;
    }
    if (segment.getExcluded().contains(userKey)) {
      return false;
    }
    for (DataModel.SegmentRule rule: segment.getRules()) {
      if (segmentRuleMatchesUser(rule, user, segment.getKey(), segment.getSalt())) {
        return true;
      }
    }
    return false;
  }

  private boolean segmentRuleMatchesUser(DataModel.SegmentRule segmentRule, LDUser user, String segmentKey, String salt) {
    for (DataModel.Clause c: segmentRule.getClauses()) {
      if (!clauseMatchesUserNoSegments(c, user)) {
        return false;
      }
    }
    
    // If the Weight is absent, this rule matches
    if (segmentRule.getWeight() == null) {
      return true;
    }
    
    // All of the clauses are met. See if the user buckets in
    double bucket = EvaluatorBucketing.bucketUser(user, segmentKey, segmentRule.getBucketBy(), salt);
    double weight = (double)segmentRule.getWeight() / 100000.0;
    return bucket < weight;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy