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

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

There is a newer version: 7.5.0
Show newest version
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()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy