Skip to content

Commit 285d29f

Browse files
authored
Rule-based Segments Implementation
2 parents 3d9d592 + 08ce1f5 commit 285d29f

File tree

116 files changed

+5671
-1019
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+5671
-1019
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.1</version>
171+
<version>5.4.3</version>
172172
</dependency>
173173
<dependency>
174174
<groupId>com.google.code.gson</groupId>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ private Spec() {
66
// restrict instantiation
77
}
88

9-
public static final String SPEC_VERSION = "1.1";
9+
public static final String SPEC_1_3 = "1.3";
10+
public static final String SPEC_1_1 = "1.1";
1011
}
1112

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import io.split.engine.matchers.CombiningMatcher;
1212
import io.split.engine.matchers.strings.WhitelistMatcher;
1313
import io.split.grammar.Treatments;
14-
import io.split.storages.SplitCacheConsumer;
1514
import io.split.storages.SplitCacheProducer;
1615

1716
import java.util.ArrayList;

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

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import com.google.common.annotations.VisibleForTesting;
44

5+
import io.split.Spec;
56
import io.split.client.dtos.SplitChange;
67
import io.split.client.dtos.SplitHttpResponse;
8+
import io.split.client.dtos.SplitChangesOldPayloadDto;
79
import io.split.client.exceptions.UriTooLongException;
810
import io.split.client.utils.Json;
911
import io.split.client.utils.Utils;
@@ -22,7 +24,8 @@
2224
import java.net.URISyntaxException;
2325

2426
import static com.google.common.base.Preconditions.checkNotNull;
25-
import static io.split.Spec.SPEC_VERSION;
27+
import static io.split.Spec.SPEC_1_3;
28+
import static io.split.Spec.SPEC_1_1;
2629

2730
/**
2831
* Created by adilaijaz on 5/30/15.
@@ -31,23 +34,30 @@ public final class HttpSplitChangeFetcher implements SplitChangeFetcher {
3134
private static final Logger _log = LoggerFactory.getLogger(HttpSplitChangeFetcher.class);
3235

3336
private static final String SINCE = "since";
37+
private static final String RB_SINCE = "rbSince";
3438
private static final String TILL = "till";
3539
private static final String SETS = "sets";
3640
private static final String SPEC = "s";
41+
private String specVersion = SPEC_1_3;
42+
private int PROXY_CHECK_INTERVAL_MILLISECONDS_SS = 24 * 60 * 60 * 1000;
43+
private Long _lastProxyCheckTimestamp = 0L;
3744
private final SplitHttpClient _client;
3845
private final URI _target;
3946
private final TelemetryRuntimeProducer _telemetryRuntimeProducer;
47+
private final boolean _rootURIOverriden;
4048

41-
public static HttpSplitChangeFetcher create(SplitHttpClient client, URI root, TelemetryRuntimeProducer telemetryRuntimeProducer)
49+
public static HttpSplitChangeFetcher create(SplitHttpClient client, URI root, TelemetryRuntimeProducer telemetryRuntimeProducer,
50+
boolean rootURIOverriden)
4251
throws URISyntaxException {
43-
return new HttpSplitChangeFetcher(client, Utils.appendPath(root, "api/splitChanges"), telemetryRuntimeProducer);
52+
return new HttpSplitChangeFetcher(client, Utils.appendPath(root, "api/splitChanges"), telemetryRuntimeProducer, rootURIOverriden);
4453
}
4554

46-
private HttpSplitChangeFetcher(SplitHttpClient client, URI uri, TelemetryRuntimeProducer telemetryRuntimeProducer) {
55+
private HttpSplitChangeFetcher(SplitHttpClient client, URI uri, TelemetryRuntimeProducer telemetryRuntimeProducer, boolean rootURIOverriden) {
4756
_client = client;
4857
_target = uri;
4958
checkNotNull(_target);
5059
_telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer);
60+
_rootURIOverriden = rootURIOverriden;
5161
}
5262

5363
long makeRandomTill() {
@@ -56,38 +66,66 @@ long makeRandomTill() {
5666
}
5767

5868
@Override
59-
public SplitChange fetch(long since, FetchOptions options) {
60-
69+
public SplitChange fetch(long since, long sinceRBS, FetchOptions options) {
6170
long start = System.currentTimeMillis();
62-
6371
try {
64-
URIBuilder uriBuilder = new URIBuilder(_target).addParameter(SPEC, "" + SPEC_VERSION);
65-
uriBuilder.addParameter(SINCE, "" + since);
66-
if (!options.flagSetsFilter().isEmpty()) {
67-
uriBuilder.addParameter(SETS, "" + options.flagSetsFilter());
68-
}
69-
if (options.hasCustomCN()) {
70-
uriBuilder.addParameter(TILL, "" + options.targetCN());
72+
URI uri = buildURL(options, since, sinceRBS);
73+
if (specVersion.equals(SPEC_1_1) && (System.currentTimeMillis() - _lastProxyCheckTimestamp >= PROXY_CHECK_INTERVAL_MILLISECONDS_SS)) {
74+
_log.info("Switching to new Feature flag spec ({}) and fetching.", SPEC_1_3);
75+
specVersion = SPEC_1_3;
76+
uri = buildURL(options, -1,-1);
7177
}
72-
URI uri = uriBuilder.build();
73-
SplitHttpResponse response = _client.get(uri, options, null);
7478

79+
SplitHttpResponse response = _client.get(uri, options, null);
7580
if (response.statusCode() < HttpStatus.SC_OK || response.statusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) {
7681
if (response.statusCode() == HttpStatus.SC_REQUEST_URI_TOO_LONG) {
7782
_log.error("The amount of flag sets provided are big causing uri length error.");
7883
throw new UriTooLongException(String.format("Status code: %s. Message: %s", response.statusCode(), response.statusMessage()));
7984
}
85+
86+
if (response.statusCode() == HttpStatus.SC_BAD_REQUEST && specVersion.equals(Spec.SPEC_1_3) && _rootURIOverriden) {
87+
specVersion = Spec.SPEC_1_1;
88+
_log.warn("Detected proxy without support for Feature flags spec {} version, will switch to spec version {}",
89+
SPEC_1_3, SPEC_1_1);
90+
_lastProxyCheckTimestamp = System.currentTimeMillis();
91+
return fetch(since, sinceRBS, options);
92+
}
93+
8094
_telemetryRuntimeProducer.recordSyncError(ResourceEnum.SPLIT_SYNC, response.statusCode());
8195
throw new IllegalStateException(
8296
String.format("Could not retrieve splitChanges since %s; http return code %s", since, response.statusCode())
8397
);
8498
}
85-
return Json.fromJson(response.body(), SplitChange.class);
99+
100+
if (specVersion.equals(Spec.SPEC_1_1)) {
101+
return Json.fromJson(response.body(), SplitChangesOldPayloadDto.class).toSplitChange();
102+
}
103+
104+
SplitChange splitChange = Json.fromJson(response.body(), SplitChange.class);
105+
splitChange.clearCache = _lastProxyCheckTimestamp != 0;
106+
_lastProxyCheckTimestamp = 0L;
107+
return splitChange;
86108
} catch (Exception e) {
87109
throw new IllegalStateException(String.format("Problem fetching splitChanges since %s: %s", since, e), e);
88110
} finally {
89-
_telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.SPLITS, System.currentTimeMillis()-start);
111+
_telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.SPLITS, System.currentTimeMillis() - start);
112+
}
113+
}
114+
115+
116+
private URI buildURL(FetchOptions options, long since, long sinceRBS) throws URISyntaxException {
117+
URIBuilder uriBuilder = new URIBuilder(_target).addParameter(SPEC, "" + specVersion);
118+
uriBuilder.addParameter(SINCE, "" + since);
119+
if (specVersion.equals(SPEC_1_3)) {
120+
uriBuilder.addParameter(RB_SINCE, "" + sinceRBS);
121+
}
122+
if (!options.flagSetsFilter().isEmpty()) {
123+
uriBuilder.addParameter(SETS, "" + options.flagSetsFilter());
124+
}
125+
if (options.hasCustomCN()) {
126+
uriBuilder.addParameter(TILL, "" + options.targetCN());
90127
}
128+
return uriBuilder.build();
91129
}
92130

93131
@VisibleForTesting
Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package io.split.client;
22

3+
import com.google.gson.JsonObject;
34
import com.google.gson.stream.JsonReader;
45
import io.split.client.dtos.SplitChange;
6+
import io.split.client.dtos.SplitChangesOldPayloadDto;
57
import io.split.client.utils.InputStreamProvider;
68
import io.split.client.utils.Json;
79
import io.split.client.utils.LocalhostSanitizer;
810
import io.split.engine.common.FetchOptions;
911
import io.split.engine.experiments.SplitChangeFetcher;
12+
1013
import org.slf4j.Logger;
1114
import org.slf4j.LoggerFactory;
1215

@@ -17,47 +20,71 @@
1720
import java.security.NoSuchAlgorithmException;
1821
import java.util.Arrays;
1922

23+
import static io.split.client.utils.Utils.checkExitConditions;
24+
2025
public class JsonLocalhostSplitChangeFetcher implements SplitChangeFetcher {
2126

2227
private static final Logger _log = LoggerFactory.getLogger(JsonLocalhostSplitChangeFetcher.class);
2328
private final InputStreamProvider _inputStreamProvider;
24-
private byte [] lastHash;
29+
private byte [] lastHashFeatureFlags;
30+
private byte [] lastHashRuleBasedSegments;
2531

2632
public JsonLocalhostSplitChangeFetcher(InputStreamProvider inputStreamProvider) {
2733
_inputStreamProvider = inputStreamProvider;
28-
lastHash = new byte[0];
34+
lastHashFeatureFlags = new byte[0];
35+
lastHashRuleBasedSegments = new byte[0];
2936
}
3037

3138
@Override
32-
public SplitChange fetch(long since, FetchOptions options) {
39+
public SplitChange fetch(long since, long sinceRBS, FetchOptions options) {
3340
try {
3441
JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(_inputStreamProvider.get(), StandardCharsets.UTF_8)));
42+
if (checkOldSpec(new JsonReader(new BufferedReader(new InputStreamReader(_inputStreamProvider.get(), StandardCharsets.UTF_8))))) {
43+
return Json.fromJson(jsonReader, SplitChangesOldPayloadDto.class).toSplitChange();
44+
}
3545
SplitChange splitChange = Json.fromJson(jsonReader, SplitChange.class);
36-
return processSplitChange(splitChange, since);
46+
return processSplitChange(splitChange, since, sinceRBS);
3747
} catch (Exception e) {
3848
throw new IllegalStateException("Problem fetching splitChanges: " + e.getMessage(), e);
3949
}
4050
}
4151

42-
private SplitChange processSplitChange(SplitChange splitChange, long changeNumber) throws NoSuchAlgorithmException {
52+
private boolean checkOldSpec(JsonReader jsonReader) {
53+
return Json.fromJson(jsonReader, JsonObject.class).has("splits");
54+
}
55+
56+
private SplitChange processSplitChange(SplitChange splitChange, long changeNumber, long changeNumberRBS) throws NoSuchAlgorithmException {
4357
SplitChange splitChangeToProcess = LocalhostSanitizer.sanitization(splitChange);
4458
// if the till is less than storage CN and different from the default till ignore the change
45-
if (splitChangeToProcess.till < changeNumber && splitChangeToProcess.till != -1) {
59+
if (checkExitConditions(splitChangeToProcess.featureFlags, changeNumber) ||
60+
checkExitConditions(splitChangeToProcess.ruleBasedSegments, changeNumberRBS)) {
4661
_log.warn("The till is lower than the change number or different to -1");
4762
return null;
4863
}
49-
String splitJson = splitChange.splits.toString();
50-
MessageDigest digest = MessageDigest.getInstance("SHA-256");
51-
digest.reset();
52-
digest.update(splitJson.getBytes());
53-
// calculate the json sha
54-
byte [] currHash = digest.digest();
64+
65+
byte [] currHashFeatureFlags = getStringDigest(splitChange.featureFlags.d.toString());
66+
byte [] currHashRuleBasedSegments = getStringDigest(splitChange.ruleBasedSegments.d.toString());
5567
//if sha exist and is equal to before sha, or if till is equal to default till returns the same segmentChange with till equals to storage CN
56-
if (java.security.MessageDigest.isEqual(lastHash, currHash) || splitChangeToProcess.till == -1) {
57-
splitChangeToProcess.till = changeNumber;
68+
if (Arrays.equals(lastHashFeatureFlags, currHashFeatureFlags) || splitChangeToProcess.featureFlags.t == -1) {
69+
splitChangeToProcess.featureFlags.t = changeNumber;
70+
}
71+
if (Arrays.equals(lastHashRuleBasedSegments, currHashRuleBasedSegments) || splitChangeToProcess.ruleBasedSegments.t == -1) {
72+
splitChangeToProcess.ruleBasedSegments.t = changeNumberRBS;
5873
}
59-
lastHash = currHash;
60-
splitChangeToProcess.since = changeNumber;
74+
75+
lastHashFeatureFlags = currHashFeatureFlags;
76+
lastHashRuleBasedSegments = currHashRuleBasedSegments;
77+
splitChangeToProcess.featureFlags.s = changeNumber;
78+
splitChangeToProcess.ruleBasedSegments.s = changeNumberRBS;
79+
6180
return splitChangeToProcess;
6281
}
82+
83+
private byte[] getStringDigest(String json) throws NoSuchAlgorithmException {
84+
MessageDigest digest = MessageDigest.getInstance("SHA-1");
85+
digest.reset();
86+
digest.update(json.getBytes());
87+
// calculate the json sha
88+
return digest.digest();
89+
}
6390
}

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

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.split.client.dtos.Split;
66
import io.split.client.dtos.SplitChange;
77
import io.split.client.dtos.Status;
8+
import io.split.client.dtos.ChangeDto;
89
import io.split.client.utils.LocalhostConstants;
910
import io.split.client.utils.LocalhostSanitizer;
1011
import io.split.engine.common.FetchOptions;
@@ -34,11 +35,12 @@ public LegacyLocalhostSplitChangeFetcher(String directory) {
3435
}
3536

3637
@Override
37-
public SplitChange fetch(long since, FetchOptions options) {
38+
public SplitChange fetch(long since, long sinceRBS, FetchOptions options) {
3839

3940
try (BufferedReader reader = new BufferedReader(new FileReader(_splitFile))) {
4041
SplitChange splitChange = new SplitChange();
41-
splitChange.splits = new ArrayList<>();
42+
splitChange.featureFlags = new ChangeDto<>();
43+
splitChange.featureFlags.d = new ArrayList<>();
4244
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
4345
String lineTrim = line.trim();
4446
if (lineTrim.isEmpty() || lineTrim.startsWith("#")) {
@@ -51,37 +53,37 @@ public SplitChange fetch(long since, FetchOptions options) {
5153
_log.info("Ignoring line since it does not have 2 or 3 columns: " + lineTrim);
5254
continue;
5355
}
54-
Optional<Split> splitOptional = splitChange.splits.stream().filter(split -> split.name.equals(featureTreatment[0])).findFirst();
56+
Optional<Split> splitOptional = splitChange.featureFlags.d.stream().
57+
filter(split -> split.name.equals(featureTreatment[0])).findFirst();
5558
Split split = splitOptional.orElse(null);
5659
if(split == null) {
5760
split = new Split();
5861
split.name = featureTreatment[0];
5962
split.configurations = new HashMap<>();
6063
split.conditions = new ArrayList<>();
6164
} else {
62-
splitChange.splits.remove(split);
65+
splitChange.featureFlags.d.remove(split);
6366
}
6467
split.status = Status.ACTIVE;
6568
split.defaultTreatment = featureTreatment[1];
6669
split.trafficTypeName = LocalhostConstants.USER;
6770
split.trafficAllocation = LocalhostConstants.SIZE_100;
6871
split.trafficAllocationSeed = LocalhostConstants.SIZE_1;
6972

70-
Condition condition;
71-
if (featureTreatment.length == 2) {
72-
condition = LocalhostSanitizer.createCondition(null, featureTreatment[1]);
73-
} else {
74-
condition = LocalhostSanitizer.createCondition(featureTreatment[2], featureTreatment[1]);
75-
}
73+
Condition condition = checkCondition(featureTreatment);
7674
if(condition.conditionType != ConditionType.ROLLOUT){
7775
split.conditions.add(0, condition);
7876
} else {
7977
split.conditions.add(condition);
8078
}
81-
splitChange.splits.add(split);
79+
splitChange.featureFlags.d.add(split);
8280
}
83-
splitChange.till = since;
84-
splitChange.since = since;
81+
splitChange.featureFlags.t = since;
82+
splitChange.featureFlags.s = since;
83+
splitChange.ruleBasedSegments = new ChangeDto<>();
84+
splitChange.ruleBasedSegments.s = -1;
85+
splitChange.ruleBasedSegments.t = -1;
86+
splitChange.ruleBasedSegments.d = new ArrayList<>();
8587
return splitChange;
8688
} catch (FileNotFoundException f) {
8789
_log.warn("There was no file named " + _splitFile.getPath() + " found. " +
@@ -96,4 +98,14 @@ public SplitChange fetch(long since, FetchOptions options) {
9698
throw new IllegalStateException("Problem fetching splitChanges: " + e.getMessage(), e);
9799
}
98100
}
101+
102+
private Condition checkCondition(String[] featureTreatment) {
103+
Condition condition;
104+
if (featureTreatment.length == 2) {
105+
condition = LocalhostSanitizer.createCondition(null, featureTreatment[1]);
106+
} else {
107+
condition = LocalhostSanitizer.createCondition(featureTreatment[2], featureTreatment[1]);
108+
}
109+
return condition;
110+
}
99111
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ public CustomHeaderDecorator customHeaderDecorator() {
412412
return _customHeaderDecorator;
413413
}
414414

415+
public boolean isSdkEndpointOverridden() {
416+
return !_endpoint.equals(SDK_ENDPOINT);
417+
}
418+
415419
public CustomHttpModule alternativeHTTPModule() { return _alternativeHTTPModule; }
416420
public static final class Builder {
417421

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import io.split.inputValidation.ApiKeyValidator;
44
import io.split.grammar.Treatments;
5-
import io.split.service.SplitHttpClient;
65
import io.split.storages.enums.StorageMode;
76
import org.slf4j.Logger;
87
import org.slf4j.LoggerFactory;

0 commit comments

Comments
 (0)