Skip to content

Feature/prerequisites #569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.4.3</version>
<version>5.4.4</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void updateCache(Map<SplitAndKey, LocalhostSplit> map) {
String treatment = conditions.size() > 0 ? Treatments.CONTROL : localhostSplit.treatment;
configurations.put(localhostSplit.treatment, localhostSplit.config);

split = new ParsedSplit(splitName, 0, false, treatment,conditions, LOCALHOST, 0, 100, 0, 0, configurations, new HashSet<>(), true);
split = new ParsedSplit(splitName, 0, false, treatment,conditions, LOCALHOST, 0, 100, 0, 0, configurations, new HashSet<>(), true, null);
parsedSplits.removeIf(parsedSplit -> parsedSplit.feature().equals(splitName));
parsedSplits.add(split);
}
Expand Down
4 changes: 4 additions & 0 deletions client/src/main/java/io/split/client/api/SplitView.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.split.client.api;

import io.split.client.dtos.Partition;
import io.split.client.dtos.Prerequisites;
import io.split.engine.experiments.ParsedCondition;
import io.split.engine.experiments.ParsedSplit;

Expand All @@ -27,6 +28,7 @@ public class SplitView {
public List<String> sets;
public String defaultTreatment;
public boolean impressionsDisabled;
public List<Prerequisites> prerequisites;

public static SplitView fromParsedSplit(ParsedSplit parsedSplit) {
SplitView splitView = new SplitView();
Expand All @@ -48,6 +50,8 @@ public static SplitView fromParsedSplit(ParsedSplit parsedSplit) {
splitView.treatments = new ArrayList<String>(treatments);
splitView.configs = parsedSplit.configurations() == null? Collections.<String, String>emptyMap() : parsedSplit.configurations() ;
splitView.impressionsDisabled = parsedSplit.impressionsDisabled();
splitView.prerequisites = parsedSplit.prerequisitesMatcher() != null ?
parsedSplit.prerequisitesMatcher().getPrerequisites(): new ArrayList<>();

return splitView;
}
Expand Down
12 changes: 12 additions & 0 deletions client/src/main/java/io/split/client/dtos/Prerequisites.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.split.client.dtos;

import com.google.gson.annotations.SerializedName;

import java.util.List;

public class Prerequisites {
@SerializedName("n")
public String featureFlagName;
@SerializedName("ts")
public List<String> treatments;
}
1 change: 1 addition & 0 deletions client/src/main/java/io/split/client/dtos/Split.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class Split {
public Map<String, String> configurations;
public HashSet<String> sets;
public Boolean impressionsDisabled = null;
public List<Prerequisites> prerequisites;

@Override
public String toString() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ private List<String> getFeatureFlagNamesByFlagSets(List<String> flagSets) {
private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bucketingKey, ParsedSplit parsedSplit, Map<String,
Object> attributes) throws ChangeNumberExceptionWrapper {
try {
String config = getConfig(parsedSplit, parsedSplit.defaultTreatment());
if (parsedSplit.killed()) {
String config = parsedSplit.configurations() != null ? parsedSplit.configurations().get(parsedSplit.defaultTreatment()) : null;
return new TreatmentLabelAndChangeNumber(
parsedSplit.defaultTreatment(),
Labels.KILLED,
Expand All @@ -96,6 +96,17 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu
parsedSplit.impressionsDisabled());
}

String bk = getBucketingKey(bucketingKey, matchingKey);

if (!parsedSplit.prerequisitesMatcher().match(matchingKey, bk, attributes, _evaluationContext)) {
return new TreatmentLabelAndChangeNumber(
parsedSplit.defaultTreatment(),
Labels.PREREQUISITES_NOT_MET,
parsedSplit.changeNumber(),
config,
parsedSplit.impressionsDisabled());
}

/*
* There are three parts to a single Feature flag: 1) Whitelists 2) Traffic Allocation
* 3) Rollout. The flag inRollout is there to understand when we move into the Rollout
Expand All @@ -104,20 +115,17 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu
*/
boolean inRollout = false;

String bk = (bucketingKey == null) ? matchingKey : bucketingKey;

for (ParsedCondition parsedCondition : parsedSplit.parsedConditions()) {

if (!inRollout && parsedCondition.conditionType() == ConditionType.ROLLOUT) {
if (checkRollout(inRollout, parsedCondition)) {

if (parsedSplit.trafficAllocation() < 100) {
// if the traffic allocation is 100%, no need to do anything special.
int bucket = Splitter.getBucket(bk, parsedSplit.trafficAllocationSeed(), parsedSplit.algo());

if (bucket > parsedSplit.trafficAllocation()) {
// out of split
String config = parsedSplit.configurations() != null ?
parsedSplit.configurations().get(parsedSplit.defaultTreatment()) : null;
config = getConfig(parsedSplit, parsedSplit.defaultTreatment());
return new TreatmentLabelAndChangeNumber(parsedSplit.defaultTreatment(), Labels.NOT_IN_SPLIT,
parsedSplit.changeNumber(), config, parsedSplit.impressionsDisabled());
}
Expand All @@ -128,7 +136,7 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu

if (parsedCondition.matcher().match(matchingKey, bucketingKey, attributes, _evaluationContext)) {
String treatment = Splitter.getTreatment(bk, parsedSplit.seed(), parsedCondition.partitions(), parsedSplit.algo());
String config = parsedSplit.configurations() != null ? parsedSplit.configurations().get(treatment) : null;
config = getConfig(parsedSplit, treatment);
return new TreatmentLabelAndChangeNumber(
treatment,
parsedCondition.label(),
Expand All @@ -138,7 +146,8 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu
}
}

String config = parsedSplit.configurations() != null ? parsedSplit.configurations().get(parsedSplit.defaultTreatment()) : null;
config = getConfig(parsedSplit, parsedSplit.defaultTreatment());

return new TreatmentLabelAndChangeNumber(
parsedSplit.defaultTreatment(),
Labels.DEFAULT_RULE,
Expand All @@ -150,13 +159,24 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu
}
}

private boolean checkRollout(boolean inRollout, ParsedCondition parsedCondition) {
return (!inRollout && parsedCondition.conditionType() == ConditionType.ROLLOUT);
}

private String getBucketingKey(String bucketingKey, String matchingKey) {
return (bucketingKey == null) ? matchingKey : bucketingKey;
}

private String getConfig(ParsedSplit parsedSplit, String returnedTreatment) {
return parsedSplit.configurations() != null ? parsedSplit.configurations().get(returnedTreatment) : null;
}

private TreatmentLabelAndChangeNumber evaluateParsedSplit(String matchingKey, String bucketingKey, Map<String, Object> attributes,
ParsedSplit parsedSplit) {
try {
if (parsedSplit == null) {
return new TreatmentLabelAndChangeNumber(Treatments.CONTROL, Labels.DEFINITION_NOT_FOUND);
}

return getTreatment(matchingKey, bucketingKey, parsedSplit, attributes);
} catch (ChangeNumberExceptionWrapper e) {
_log.error("Evaluator Exception", e.wrappedException());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public class Labels {
public static final String DEFINITION_NOT_FOUND = "definition not found";
public static final String EXCEPTION = "exception";
public static final String UNSUPPORTED_MATCHER = "targeting rule type unsupported by sdk";
public static final String PREREQUISITES_NOT_MET = "prerequisites not met";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.common.collect.ImmutableList;
import io.split.engine.matchers.AttributeMatcher;
import io.split.engine.matchers.PrerequisitesMatcher;
import io.split.engine.matchers.RuleBasedSegmentMatcher;
import io.split.engine.matchers.UserDefinedSegmentMatcher;

Expand Down Expand Up @@ -34,6 +35,7 @@ public class ParsedSplit {
private final Map<String, String> _configurations;
private final HashSet<String> _flagSets;
private final boolean _impressionsDisabled;
private PrerequisitesMatcher _prerequisitesMatcher;

public static ParsedSplit createParsedSplitForTests(
String feature,
Expand All @@ -45,7 +47,8 @@ public static ParsedSplit createParsedSplitForTests(
long changeNumber,
int algo,
HashSet<String> flagSets,
boolean impressionsDisabled
boolean impressionsDisabled,
PrerequisitesMatcher prerequisitesMatcher
) {
return new ParsedSplit(
feature,
Expand All @@ -60,7 +63,8 @@ public static ParsedSplit createParsedSplitForTests(
algo,
null,
flagSets,
impressionsDisabled
impressionsDisabled,
prerequisitesMatcher
);
}

Expand All @@ -75,7 +79,8 @@ public static ParsedSplit createParsedSplitForTests(
int algo,
Map<String, String> configurations,
HashSet<String> flagSets,
boolean impressionsDisabled
boolean impressionsDisabled,
PrerequisitesMatcher prerequisitesMatcher
) {
return new ParsedSplit(
feature,
Expand All @@ -90,7 +95,8 @@ public static ParsedSplit createParsedSplitForTests(
algo,
configurations,
flagSets,
impressionsDisabled
impressionsDisabled,
prerequisitesMatcher
);
}

Expand All @@ -107,7 +113,8 @@ public ParsedSplit(
int algo,
Map<String, String> configurations,
HashSet<String> flagSets,
boolean impressionsDisabled
boolean impressionsDisabled,
PrerequisitesMatcher prerequisitesMatcher
) {
_split = feature;
_seed = seed;
Expand All @@ -125,6 +132,7 @@ public ParsedSplit(
_configurations = configurations;
_flagSets = flagSets;
_impressionsDisabled = impressionsDisabled;
_prerequisitesMatcher = prerequisitesMatcher;
}

public String feature() {
Expand Down Expand Up @@ -171,6 +179,7 @@ public Map<String, String> configurations() {
public boolean impressionsDisabled() {
return _impressionsDisabled;
}
public PrerequisitesMatcher prerequisitesMatcher() { return _prerequisitesMatcher; }

@Override
public int hashCode() {
Expand All @@ -195,17 +204,20 @@ public boolean equals(Object obj) {
if (!(obj instanceof ParsedSplit)) return false;

ParsedSplit other = (ParsedSplit) obj;
boolean trafficTypeCond = _trafficTypeName == null ? other._trafficTypeName == null : _trafficTypeName.equals(other._trafficTypeName);
boolean configCond = _configurations == null ? other._configurations == null : _configurations.equals(other._configurations);

return _split.equals(other._split)
&& _seed == other._seed
&& _killed == other._killed
&& _defaultTreatment.equals(other._defaultTreatment)
&& _parsedCondition.equals(other._parsedCondition)
&& _trafficTypeName == null ? other._trafficTypeName == null : _trafficTypeName.equals(other._trafficTypeName)
&& trafficTypeCond
&& _changeNumber == other._changeNumber
&& _algo == other._algo
&& _configurations == null ? other._configurations == null : _configurations.equals(other._configurations)
&& _impressionsDisabled == other._impressionsDisabled;
&& configCond
&& _impressionsDisabled == other._impressionsDisabled
&& _prerequisitesMatcher == other._prerequisitesMatcher;
}

@Override
Expand All @@ -231,6 +243,9 @@ public String toString() {
bldr.append(_configurations);
bldr.append(", impressionsDisabled:");
bldr.append(_impressionsDisabled);
bldr.append(", prerequisites:");
bldr.append(_prerequisitesMatcher);

return bldr.toString();

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.split.client.dtos.Partition;
import io.split.client.dtos.Split;
import io.split.engine.matchers.CombiningMatcher;
import io.split.engine.matchers.PrerequisitesMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -68,6 +69,7 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) {
split.algo,
split.configurations,
split.sets,
split.impressionsDisabled);
split.impressionsDisabled,
new PrerequisitesMatcher(split.prerequisites));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.split.engine.matchers;

import io.split.client.dtos.Prerequisites;
import io.split.engine.evaluator.EvaluationContext;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class PrerequisitesMatcher implements Matcher {
private List<Prerequisites> _prerequisites;

public PrerequisitesMatcher(List<Prerequisites> prerequisites) {
_prerequisites = prerequisites;
}

public List<Prerequisites> getPrerequisites() { return _prerequisites; }

@Override
public boolean match(Object matchValue, String bucketingKey, Map<String, Object> attributes, EvaluationContext evaluationContext) {
if (matchValue == null) {
return false;
}

if (!(matchValue instanceof String)) {
return false;
}

if (_prerequisites == null) {
return true;
}

for (Prerequisites prerequisites : _prerequisites) {
String treatment = evaluationContext.getEvaluator().evaluateFeature((String) matchValue, bucketingKey,
prerequisites.featureFlagName, attributes). treatment;
if (!prerequisites.treatments.contains(treatment)) {
return false;
}
}
return true;
}

@Override
public String toString() {
StringBuilder bldr = new StringBuilder();
bldr.append("prerequisites: ");
if (this._prerequisites != null) {
bldr.append(this._prerequisites.stream().map(pr -> pr.featureFlagName + " " +
pr.treatments.toString()).map(Object::toString).collect(Collectors.joining(", ")));
}
return bldr.toString();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

PrerequisitesMatcher that = (PrerequisitesMatcher) o;

return Objects.equals(_prerequisites, that._prerequisites);
}

@Override
public int hashCode() {
int result = _prerequisites != null ? _prerequisites.hashCode() : 0;
result = 31 * result + (_prerequisites != null ? _prerequisites.hashCode() : 0);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ public void kill(String splitName, String defaultTreatment, long changeNumber) {
parsedSplit.algo(),
parsedSplit.configurations(),
parsedSplit.flagSets(),
parsedSplit.impressionsDisabled()
parsedSplit.impressionsDisabled(),
parsedSplit.prerequisitesMatcher()
);

_concurrentMap.put(splitName, updatedSplit);
Expand Down
Loading