Skip to content

Commit b5f0838

Browse files
authored
Merge pull request #569 from splitio/feature/prerequisites
Feature/prerequisites
2 parents 285d29f + ba2df25 commit b5f0838

22 files changed

+783
-136
lines changed

client/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
<dependency>
169169
<groupId>org.apache.httpcomponents.client5</groupId>
170170
<artifactId>httpclient5</artifactId>
171-
<version>5.4.3</version>
171+
<version>5.4.4</version>
172172
</dependency>
173173
<dependency>
174174
<groupId>com.google.code.gson</groupId>

client/src/main/java/io/split/client/CacheUpdaterService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void updateCache(Map<SplitAndKey, LocalhostSplit> map) {
5151
String treatment = conditions.size() > 0 ? Treatments.CONTROL : localhostSplit.treatment;
5252
configurations.put(localhostSplit.treatment, localhostSplit.config);
5353

54-
split = new ParsedSplit(splitName, 0, false, treatment,conditions, LOCALHOST, 0, 100, 0, 0, configurations, new HashSet<>(), true);
54+
split = new ParsedSplit(splitName, 0, false, treatment,conditions, LOCALHOST, 0, 100, 0, 0, configurations, new HashSet<>(), true, null);
5555
parsedSplits.removeIf(parsedSplit -> parsedSplit.feature().equals(splitName));
5656
parsedSplits.add(split);
5757
}

client/src/main/java/io/split/client/api/SplitView.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.split.client.api;
22

33
import io.split.client.dtos.Partition;
4+
import io.split.client.dtos.Prerequisites;
45
import io.split.engine.experiments.ParsedCondition;
56
import io.split.engine.experiments.ParsedSplit;
67

@@ -27,6 +28,7 @@ public class SplitView {
2728
public List<String> sets;
2829
public String defaultTreatment;
2930
public boolean impressionsDisabled;
31+
public List<Prerequisites> prerequisites;
3032

3133
public static SplitView fromParsedSplit(ParsedSplit parsedSplit) {
3234
SplitView splitView = new SplitView();
@@ -48,6 +50,8 @@ public static SplitView fromParsedSplit(ParsedSplit parsedSplit) {
4850
splitView.treatments = new ArrayList<String>(treatments);
4951
splitView.configs = parsedSplit.configurations() == null? Collections.<String, String>emptyMap() : parsedSplit.configurations() ;
5052
splitView.impressionsDisabled = parsedSplit.impressionsDisabled();
53+
splitView.prerequisites = parsedSplit.prerequisitesMatcher() != null ?
54+
parsedSplit.prerequisitesMatcher().getPrerequisites(): new ArrayList<>();
5155

5256
return splitView;
5357
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.split.client.dtos;
2+
3+
import com.google.gson.annotations.SerializedName;
4+
5+
import java.util.List;
6+
7+
public class Prerequisites {
8+
@SerializedName("n")
9+
public String featureFlagName;
10+
@SerializedName("ts")
11+
public List<String> treatments;
12+
}

client/src/main/java/io/split/client/dtos/Split.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class Split {
1919
public Map<String, String> configurations;
2020
public HashSet<String> sets;
2121
public Boolean impressionsDisabled = null;
22+
public List<Prerequisites> prerequisites;
2223

2324
@Override
2425
public String toString() {

client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ private List<String> getFeatureFlagNamesByFlagSets(List<String> flagSets) {
8686
private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bucketingKey, ParsedSplit parsedSplit, Map<String,
8787
Object> attributes) throws ChangeNumberExceptionWrapper {
8888
try {
89+
String config = getConfig(parsedSplit, parsedSplit.defaultTreatment());
8990
if (parsedSplit.killed()) {
90-
String config = parsedSplit.configurations() != null ? parsedSplit.configurations().get(parsedSplit.defaultTreatment()) : null;
9191
return new TreatmentLabelAndChangeNumber(
9292
parsedSplit.defaultTreatment(),
9393
Labels.KILLED,
@@ -96,6 +96,17 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu
9696
parsedSplit.impressionsDisabled());
9797
}
9898

99+
String bk = getBucketingKey(bucketingKey, matchingKey);
100+
101+
if (!parsedSplit.prerequisitesMatcher().match(matchingKey, bk, attributes, _evaluationContext)) {
102+
return new TreatmentLabelAndChangeNumber(
103+
parsedSplit.defaultTreatment(),
104+
Labels.PREREQUISITES_NOT_MET,
105+
parsedSplit.changeNumber(),
106+
config,
107+
parsedSplit.impressionsDisabled());
108+
}
109+
99110
/*
100111
* There are three parts to a single Feature flag: 1) Whitelists 2) Traffic Allocation
101112
* 3) Rollout. The flag inRollout is there to understand when we move into the Rollout
@@ -104,20 +115,17 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu
104115
*/
105116
boolean inRollout = false;
106117

107-
String bk = (bucketingKey == null) ? matchingKey : bucketingKey;
108-
109118
for (ParsedCondition parsedCondition : parsedSplit.parsedConditions()) {
110119

111-
if (!inRollout && parsedCondition.conditionType() == ConditionType.ROLLOUT) {
120+
if (checkRollout(inRollout, parsedCondition)) {
112121

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

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

129137
if (parsedCondition.matcher().match(matchingKey, bucketingKey, attributes, _evaluationContext)) {
130138
String treatment = Splitter.getTreatment(bk, parsedSplit.seed(), parsedCondition.partitions(), parsedSplit.algo());
131-
String config = parsedSplit.configurations() != null ? parsedSplit.configurations().get(treatment) : null;
139+
config = getConfig(parsedSplit, treatment);
132140
return new TreatmentLabelAndChangeNumber(
133141
treatment,
134142
parsedCondition.label(),
@@ -138,7 +146,8 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu
138146
}
139147
}
140148

141-
String config = parsedSplit.configurations() != null ? parsedSplit.configurations().get(parsedSplit.defaultTreatment()) : null;
149+
config = getConfig(parsedSplit, parsedSplit.defaultTreatment());
150+
142151
return new TreatmentLabelAndChangeNumber(
143152
parsedSplit.defaultTreatment(),
144153
Labels.DEFAULT_RULE,
@@ -150,13 +159,24 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu
150159
}
151160
}
152161

162+
private boolean checkRollout(boolean inRollout, ParsedCondition parsedCondition) {
163+
return (!inRollout && parsedCondition.conditionType() == ConditionType.ROLLOUT);
164+
}
165+
166+
private String getBucketingKey(String bucketingKey, String matchingKey) {
167+
return (bucketingKey == null) ? matchingKey : bucketingKey;
168+
}
169+
170+
private String getConfig(ParsedSplit parsedSplit, String returnedTreatment) {
171+
return parsedSplit.configurations() != null ? parsedSplit.configurations().get(returnedTreatment) : null;
172+
}
173+
153174
private TreatmentLabelAndChangeNumber evaluateParsedSplit(String matchingKey, String bucketingKey, Map<String, Object> attributes,
154175
ParsedSplit parsedSplit) {
155176
try {
156177
if (parsedSplit == null) {
157178
return new TreatmentLabelAndChangeNumber(Treatments.CONTROL, Labels.DEFINITION_NOT_FOUND);
158179
}
159-
160180
return getTreatment(matchingKey, bucketingKey, parsedSplit, attributes);
161181
} catch (ChangeNumberExceptionWrapper e) {
162182
_log.error("Evaluator Exception", e.wrappedException());

client/src/main/java/io/split/engine/evaluator/Labels.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ public class Labels {
77
public static final String DEFINITION_NOT_FOUND = "definition not found";
88
public static final String EXCEPTION = "exception";
99
public static final String UNSUPPORTED_MATCHER = "targeting rule type unsupported by sdk";
10+
public static final String PREREQUISITES_NOT_MET = "prerequisites not met";
1011
}

client/src/main/java/io/split/engine/experiments/ParsedSplit.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.google.common.collect.ImmutableList;
44
import io.split.engine.matchers.AttributeMatcher;
5+
import io.split.engine.matchers.PrerequisitesMatcher;
56
import io.split.engine.matchers.RuleBasedSegmentMatcher;
67
import io.split.engine.matchers.UserDefinedSegmentMatcher;
78

@@ -34,6 +35,7 @@ public class ParsedSplit {
3435
private final Map<String, String> _configurations;
3536
private final HashSet<String> _flagSets;
3637
private final boolean _impressionsDisabled;
38+
private PrerequisitesMatcher _prerequisitesMatcher;
3739

3840
public static ParsedSplit createParsedSplitForTests(
3941
String feature,
@@ -45,7 +47,8 @@ public static ParsedSplit createParsedSplitForTests(
4547
long changeNumber,
4648
int algo,
4749
HashSet<String> flagSets,
48-
boolean impressionsDisabled
50+
boolean impressionsDisabled,
51+
PrerequisitesMatcher prerequisitesMatcher
4952
) {
5053
return new ParsedSplit(
5154
feature,
@@ -60,7 +63,8 @@ public static ParsedSplit createParsedSplitForTests(
6063
algo,
6164
null,
6265
flagSets,
63-
impressionsDisabled
66+
impressionsDisabled,
67+
prerequisitesMatcher
6468
);
6569
}
6670

@@ -75,7 +79,8 @@ public static ParsedSplit createParsedSplitForTests(
7579
int algo,
7680
Map<String, String> configurations,
7781
HashSet<String> flagSets,
78-
boolean impressionsDisabled
82+
boolean impressionsDisabled,
83+
PrerequisitesMatcher prerequisitesMatcher
7984
) {
8085
return new ParsedSplit(
8186
feature,
@@ -90,7 +95,8 @@ public static ParsedSplit createParsedSplitForTests(
9095
algo,
9196
configurations,
9297
flagSets,
93-
impressionsDisabled
98+
impressionsDisabled,
99+
prerequisitesMatcher
94100
);
95101
}
96102

@@ -107,7 +113,8 @@ public ParsedSplit(
107113
int algo,
108114
Map<String, String> configurations,
109115
HashSet<String> flagSets,
110-
boolean impressionsDisabled
116+
boolean impressionsDisabled,
117+
PrerequisitesMatcher prerequisitesMatcher
111118
) {
112119
_split = feature;
113120
_seed = seed;
@@ -125,6 +132,7 @@ public ParsedSplit(
125132
_configurations = configurations;
126133
_flagSets = flagSets;
127134
_impressionsDisabled = impressionsDisabled;
135+
_prerequisitesMatcher = prerequisitesMatcher;
128136
}
129137

130138
public String feature() {
@@ -171,6 +179,7 @@ public Map<String, String> configurations() {
171179
public boolean impressionsDisabled() {
172180
return _impressionsDisabled;
173181
}
182+
public PrerequisitesMatcher prerequisitesMatcher() { return _prerequisitesMatcher; }
174183

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

197206
ParsedSplit other = (ParsedSplit) obj;
207+
boolean trafficTypeCond = _trafficTypeName == null ? other._trafficTypeName == null : _trafficTypeName.equals(other._trafficTypeName);
208+
boolean configCond = _configurations == null ? other._configurations == null : _configurations.equals(other._configurations);
198209

199210
return _split.equals(other._split)
200211
&& _seed == other._seed
201212
&& _killed == other._killed
202213
&& _defaultTreatment.equals(other._defaultTreatment)
203214
&& _parsedCondition.equals(other._parsedCondition)
204-
&& _trafficTypeName == null ? other._trafficTypeName == null : _trafficTypeName.equals(other._trafficTypeName)
215+
&& trafficTypeCond
205216
&& _changeNumber == other._changeNumber
206217
&& _algo == other._algo
207-
&& _configurations == null ? other._configurations == null : _configurations.equals(other._configurations)
208-
&& _impressionsDisabled == other._impressionsDisabled;
218+
&& configCond
219+
&& _impressionsDisabled == other._impressionsDisabled
220+
&& _prerequisitesMatcher == other._prerequisitesMatcher;
209221
}
210222

211223
@Override
@@ -231,6 +243,9 @@ public String toString() {
231243
bldr.append(_configurations);
232244
bldr.append(", impressionsDisabled:");
233245
bldr.append(_impressionsDisabled);
246+
bldr.append(", prerequisites:");
247+
bldr.append(_prerequisitesMatcher);
248+
234249
return bldr.toString();
235250

236251
}

client/src/main/java/io/split/engine/experiments/SplitParser.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.split.client.dtos.Partition;
77
import io.split.client.dtos.Split;
88
import io.split.engine.matchers.CombiningMatcher;
9+
import io.split.engine.matchers.PrerequisitesMatcher;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
1112

@@ -68,6 +69,7 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) {
6869
split.algo,
6970
split.configurations,
7071
split.sets,
71-
split.impressionsDisabled);
72+
split.impressionsDisabled,
73+
new PrerequisitesMatcher(split.prerequisites));
7274
}
7375
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.split.engine.matchers;
2+
3+
import io.split.client.dtos.Prerequisites;
4+
import io.split.engine.evaluator.EvaluationContext;
5+
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Objects;
9+
import java.util.stream.Collectors;
10+
11+
public class PrerequisitesMatcher implements Matcher {
12+
private List<Prerequisites> _prerequisites;
13+
14+
public PrerequisitesMatcher(List<Prerequisites> prerequisites) {
15+
_prerequisites = prerequisites;
16+
}
17+
18+
public List<Prerequisites> getPrerequisites() { return _prerequisites; }
19+
20+
@Override
21+
public boolean match(Object matchValue, String bucketingKey, Map<String, Object> attributes, EvaluationContext evaluationContext) {
22+
if (matchValue == null) {
23+
return false;
24+
}
25+
26+
if (!(matchValue instanceof String)) {
27+
return false;
28+
}
29+
30+
if (_prerequisites == null) {
31+
return true;
32+
}
33+
34+
for (Prerequisites prerequisites : _prerequisites) {
35+
String treatment = evaluationContext.getEvaluator().evaluateFeature((String) matchValue, bucketingKey,
36+
prerequisites.featureFlagName, attributes). treatment;
37+
if (!prerequisites.treatments.contains(treatment)) {
38+
return false;
39+
}
40+
}
41+
return true;
42+
}
43+
44+
@Override
45+
public String toString() {
46+
StringBuilder bldr = new StringBuilder();
47+
bldr.append("prerequisites: ");
48+
if (this._prerequisites != null) {
49+
bldr.append(this._prerequisites.stream().map(pr -> pr.featureFlagName + " " +
50+
pr.treatments.toString()).map(Object::toString).collect(Collectors.joining(", ")));
51+
}
52+
return bldr.toString();
53+
}
54+
55+
@Override
56+
public boolean equals(Object o) {
57+
if (this == o) return true;
58+
if (o == null || getClass() != o.getClass()) return false;
59+
60+
PrerequisitesMatcher that = (PrerequisitesMatcher) o;
61+
62+
return Objects.equals(_prerequisites, that._prerequisites);
63+
}
64+
65+
@Override
66+
public int hashCode() {
67+
int result = _prerequisites != null ? _prerequisites.hashCode() : 0;
68+
result = 31 * result + (_prerequisites != null ? _prerequisites.hashCode() : 0);
69+
return result;
70+
}
71+
}

client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ public void kill(String splitName, String defaultTreatment, long changeNumber) {
131131
parsedSplit.algo(),
132132
parsedSplit.configurations(),
133133
parsedSplit.flagSets(),
134-
parsedSplit.impressionsDisabled()
134+
parsedSplit.impressionsDisabled(),
135+
parsedSplit.prerequisitesMatcher()
135136
);
136137

137138
_concurrentMap.put(splitName, updatedSplit);

0 commit comments

Comments
 (0)