diff --git a/CHANGES.txt b/CHANGES.txt index e3be07e36..a99b79df4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +4.16.0 (May 28, 2025) +- Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. +- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules. + 4.15.0 (Apr 18, 2025) - Prevent polling threads from starting when the SDK calls destroy method. - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. diff --git a/client/pom.xml b/client/pom.xml index eea9088e7..d0ae4a789 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,9 +5,9 @@ io.split.client java-client-parent - 4.15.0 + 4.16.0 - 4.15.0 + 4.16.0 java-client jar Java Client @@ -168,7 +168,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.4.1 + 5.4.4 com.google.code.gson diff --git a/client/src/main/java/io/split/Spec.java b/client/src/main/java/io/split/Spec.java index 9e03a59ab..b2c7de4b3 100644 --- a/client/src/main/java/io/split/Spec.java +++ b/client/src/main/java/io/split/Spec.java @@ -6,6 +6,7 @@ private Spec() { // restrict instantiation } - public static final String SPEC_VERSION = "1.1"; + public static final String SPEC_1_3 = "1.3"; + public static final String SPEC_1_1 = "1.1"; } diff --git a/client/src/main/java/io/split/client/CacheUpdaterService.java b/client/src/main/java/io/split/client/CacheUpdaterService.java index d69c66d58..63b426634 100644 --- a/client/src/main/java/io/split/client/CacheUpdaterService.java +++ b/client/src/main/java/io/split/client/CacheUpdaterService.java @@ -11,7 +11,6 @@ import io.split.engine.matchers.CombiningMatcher; import io.split.engine.matchers.strings.WhitelistMatcher; import io.split.grammar.Treatments; -import io.split.storages.SplitCacheConsumer; import io.split.storages.SplitCacheProducer; import java.util.ArrayList; @@ -52,7 +51,7 @@ public void updateCache(Map 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); } diff --git a/client/src/main/java/io/split/client/HttpSplitChangeFetcher.java b/client/src/main/java/io/split/client/HttpSplitChangeFetcher.java index a3e234a3e..49eb66a99 100644 --- a/client/src/main/java/io/split/client/HttpSplitChangeFetcher.java +++ b/client/src/main/java/io/split/client/HttpSplitChangeFetcher.java @@ -2,8 +2,10 @@ import com.google.common.annotations.VisibleForTesting; +import io.split.Spec; import io.split.client.dtos.SplitChange; import io.split.client.dtos.SplitHttpResponse; +import io.split.client.dtos.SplitChangesOldPayloadDto; import io.split.client.exceptions.UriTooLongException; import io.split.client.utils.Json; import io.split.client.utils.Utils; @@ -22,7 +24,8 @@ import java.net.URISyntaxException; import static com.google.common.base.Preconditions.checkNotNull; -import static io.split.Spec.SPEC_VERSION; +import static io.split.Spec.SPEC_1_3; +import static io.split.Spec.SPEC_1_1; /** * Created by adilaijaz on 5/30/15. @@ -31,23 +34,30 @@ public final class HttpSplitChangeFetcher implements SplitChangeFetcher { private static final Logger _log = LoggerFactory.getLogger(HttpSplitChangeFetcher.class); private static final String SINCE = "since"; + private static final String RB_SINCE = "rbSince"; private static final String TILL = "till"; private static final String SETS = "sets"; private static final String SPEC = "s"; + private String specVersion = SPEC_1_3; + private int PROXY_CHECK_INTERVAL_MILLISECONDS_SS = 24 * 60 * 60 * 1000; + private Long _lastProxyCheckTimestamp = 0L; private final SplitHttpClient _client; private final URI _target; private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + private final boolean _rootURIOverriden; - public static HttpSplitChangeFetcher create(SplitHttpClient client, URI root, TelemetryRuntimeProducer telemetryRuntimeProducer) + public static HttpSplitChangeFetcher create(SplitHttpClient client, URI root, TelemetryRuntimeProducer telemetryRuntimeProducer, + boolean rootURIOverriden) throws URISyntaxException { - return new HttpSplitChangeFetcher(client, Utils.appendPath(root, "api/splitChanges"), telemetryRuntimeProducer); + return new HttpSplitChangeFetcher(client, Utils.appendPath(root, "api/splitChanges"), telemetryRuntimeProducer, rootURIOverriden); } - private HttpSplitChangeFetcher(SplitHttpClient client, URI uri, TelemetryRuntimeProducer telemetryRuntimeProducer) { + private HttpSplitChangeFetcher(SplitHttpClient client, URI uri, TelemetryRuntimeProducer telemetryRuntimeProducer, boolean rootURIOverriden) { _client = client; _target = uri; checkNotNull(_target); _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + _rootURIOverriden = rootURIOverriden; } long makeRandomTill() { @@ -56,38 +66,66 @@ long makeRandomTill() { } @Override - public SplitChange fetch(long since, FetchOptions options) { - + public SplitChange fetch(long since, long sinceRBS, FetchOptions options) { long start = System.currentTimeMillis(); - try { - URIBuilder uriBuilder = new URIBuilder(_target).addParameter(SPEC, "" + SPEC_VERSION); - uriBuilder.addParameter(SINCE, "" + since); - if (!options.flagSetsFilter().isEmpty()) { - uriBuilder.addParameter(SETS, "" + options.flagSetsFilter()); - } - if (options.hasCustomCN()) { - uriBuilder.addParameter(TILL, "" + options.targetCN()); + URI uri = buildURL(options, since, sinceRBS); + if (specVersion.equals(SPEC_1_1) && (System.currentTimeMillis() - _lastProxyCheckTimestamp >= PROXY_CHECK_INTERVAL_MILLISECONDS_SS)) { + _log.info("Switching to new Feature flag spec ({}) and fetching.", SPEC_1_3); + specVersion = SPEC_1_3; + uri = buildURL(options, -1,-1); } - URI uri = uriBuilder.build(); - SplitHttpResponse response = _client.get(uri, options, null); + SplitHttpResponse response = _client.get(uri, options, null); if (response.statusCode() < HttpStatus.SC_OK || response.statusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) { if (response.statusCode() == HttpStatus.SC_REQUEST_URI_TOO_LONG) { _log.error("The amount of flag sets provided are big causing uri length error."); throw new UriTooLongException(String.format("Status code: %s. Message: %s", response.statusCode(), response.statusMessage())); } + + if (response.statusCode() == HttpStatus.SC_BAD_REQUEST && specVersion.equals(Spec.SPEC_1_3) && _rootURIOverriden) { + specVersion = Spec.SPEC_1_1; + _log.warn("Detected proxy without support for Feature flags spec {} version, will switch to spec version {}", + SPEC_1_3, SPEC_1_1); + _lastProxyCheckTimestamp = System.currentTimeMillis(); + return fetch(since, sinceRBS, options); + } + _telemetryRuntimeProducer.recordSyncError(ResourceEnum.SPLIT_SYNC, response.statusCode()); throw new IllegalStateException( String.format("Could not retrieve splitChanges since %s; http return code %s", since, response.statusCode()) ); } - return Json.fromJson(response.body(), SplitChange.class); + + if (specVersion.equals(Spec.SPEC_1_1)) { + return Json.fromJson(response.body(), SplitChangesOldPayloadDto.class).toSplitChange(); + } + + SplitChange splitChange = Json.fromJson(response.body(), SplitChange.class); + splitChange.clearCache = _lastProxyCheckTimestamp != 0; + _lastProxyCheckTimestamp = 0L; + return splitChange; } catch (Exception e) { throw new IllegalStateException(String.format("Problem fetching splitChanges since %s: %s", since, e), e); } finally { - _telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.SPLITS, System.currentTimeMillis()-start); + _telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.SPLITS, System.currentTimeMillis() - start); + } + } + + + private URI buildURL(FetchOptions options, long since, long sinceRBS) throws URISyntaxException { + URIBuilder uriBuilder = new URIBuilder(_target).addParameter(SPEC, "" + specVersion); + uriBuilder.addParameter(SINCE, "" + since); + if (specVersion.equals(SPEC_1_3)) { + uriBuilder.addParameter(RB_SINCE, "" + sinceRBS); + } + if (!options.flagSetsFilter().isEmpty()) { + uriBuilder.addParameter(SETS, "" + options.flagSetsFilter()); + } + if (options.hasCustomCN()) { + uriBuilder.addParameter(TILL, "" + options.targetCN()); } + return uriBuilder.build(); } @VisibleForTesting diff --git a/client/src/main/java/io/split/client/JsonLocalhostSplitChangeFetcher.java b/client/src/main/java/io/split/client/JsonLocalhostSplitChangeFetcher.java index 6eb092464..03530d099 100644 --- a/client/src/main/java/io/split/client/JsonLocalhostSplitChangeFetcher.java +++ b/client/src/main/java/io/split/client/JsonLocalhostSplitChangeFetcher.java @@ -1,12 +1,15 @@ package io.split.client; +import com.google.gson.JsonObject; import com.google.gson.stream.JsonReader; import io.split.client.dtos.SplitChange; +import io.split.client.dtos.SplitChangesOldPayloadDto; import io.split.client.utils.InputStreamProvider; import io.split.client.utils.Json; import io.split.client.utils.LocalhostSanitizer; import io.split.engine.common.FetchOptions; import io.split.engine.experiments.SplitChangeFetcher; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,47 +20,71 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import static io.split.client.utils.Utils.checkExitConditions; + public class JsonLocalhostSplitChangeFetcher implements SplitChangeFetcher { private static final Logger _log = LoggerFactory.getLogger(JsonLocalhostSplitChangeFetcher.class); private final InputStreamProvider _inputStreamProvider; - private byte [] lastHash; + private byte [] lastHashFeatureFlags; + private byte [] lastHashRuleBasedSegments; public JsonLocalhostSplitChangeFetcher(InputStreamProvider inputStreamProvider) { _inputStreamProvider = inputStreamProvider; - lastHash = new byte[0]; + lastHashFeatureFlags = new byte[0]; + lastHashRuleBasedSegments = new byte[0]; } @Override - public SplitChange fetch(long since, FetchOptions options) { + public SplitChange fetch(long since, long sinceRBS, FetchOptions options) { try { JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(_inputStreamProvider.get(), StandardCharsets.UTF_8))); + if (checkOldSpec(new JsonReader(new BufferedReader(new InputStreamReader(_inputStreamProvider.get(), StandardCharsets.UTF_8))))) { + return Json.fromJson(jsonReader, SplitChangesOldPayloadDto.class).toSplitChange(); + } SplitChange splitChange = Json.fromJson(jsonReader, SplitChange.class); - return processSplitChange(splitChange, since); + return processSplitChange(splitChange, since, sinceRBS); } catch (Exception e) { throw new IllegalStateException("Problem fetching splitChanges: " + e.getMessage(), e); } } - private SplitChange processSplitChange(SplitChange splitChange, long changeNumber) throws NoSuchAlgorithmException { + private boolean checkOldSpec(JsonReader jsonReader) { + return Json.fromJson(jsonReader, JsonObject.class).has("splits"); + } + + private SplitChange processSplitChange(SplitChange splitChange, long changeNumber, long changeNumberRBS) throws NoSuchAlgorithmException { SplitChange splitChangeToProcess = LocalhostSanitizer.sanitization(splitChange); // if the till is less than storage CN and different from the default till ignore the change - if (splitChangeToProcess.till < changeNumber && splitChangeToProcess.till != -1) { + if (checkExitConditions(splitChangeToProcess.featureFlags, changeNumber) || + checkExitConditions(splitChangeToProcess.ruleBasedSegments, changeNumberRBS)) { _log.warn("The till is lower than the change number or different to -1"); return null; } - String splitJson = splitChange.splits.toString(); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - digest.reset(); - digest.update(splitJson.getBytes()); - // calculate the json sha - byte [] currHash = digest.digest(); + + byte [] currHashFeatureFlags = getStringDigest(splitChange.featureFlags.d.toString()); + byte [] currHashRuleBasedSegments = getStringDigest(splitChange.ruleBasedSegments.d.toString()); //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 - if (java.security.MessageDigest.isEqual(lastHash, currHash) || splitChangeToProcess.till == -1) { - splitChangeToProcess.till = changeNumber; + if (Arrays.equals(lastHashFeatureFlags, currHashFeatureFlags) || splitChangeToProcess.featureFlags.t == -1) { + splitChangeToProcess.featureFlags.t = changeNumber; + } + if (Arrays.equals(lastHashRuleBasedSegments, currHashRuleBasedSegments) || splitChangeToProcess.ruleBasedSegments.t == -1) { + splitChangeToProcess.ruleBasedSegments.t = changeNumberRBS; } - lastHash = currHash; - splitChangeToProcess.since = changeNumber; + + lastHashFeatureFlags = currHashFeatureFlags; + lastHashRuleBasedSegments = currHashRuleBasedSegments; + splitChangeToProcess.featureFlags.s = changeNumber; + splitChangeToProcess.ruleBasedSegments.s = changeNumberRBS; + return splitChangeToProcess; } + + private byte[] getStringDigest(String json) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + digest.update(json.getBytes()); + // calculate the json sha + return digest.digest(); + } } \ No newline at end of file diff --git a/client/src/main/java/io/split/client/LegacyLocalhostSplitChangeFetcher.java b/client/src/main/java/io/split/client/LegacyLocalhostSplitChangeFetcher.java index a35c92cfe..c67055ec8 100644 --- a/client/src/main/java/io/split/client/LegacyLocalhostSplitChangeFetcher.java +++ b/client/src/main/java/io/split/client/LegacyLocalhostSplitChangeFetcher.java @@ -5,6 +5,7 @@ import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; import io.split.client.dtos.Status; +import io.split.client.dtos.ChangeDto; import io.split.client.utils.LocalhostConstants; import io.split.client.utils.LocalhostSanitizer; import io.split.engine.common.FetchOptions; @@ -34,11 +35,12 @@ public LegacyLocalhostSplitChangeFetcher(String directory) { } @Override - public SplitChange fetch(long since, FetchOptions options) { + public SplitChange fetch(long since, long sinceRBS, FetchOptions options) { try (BufferedReader reader = new BufferedReader(new FileReader(_splitFile))) { SplitChange splitChange = new SplitChange(); - splitChange.splits = new ArrayList<>(); + splitChange.featureFlags = new ChangeDto<>(); + splitChange.featureFlags.d = new ArrayList<>(); for (String line = reader.readLine(); line != null; line = reader.readLine()) { String lineTrim = line.trim(); if (lineTrim.isEmpty() || lineTrim.startsWith("#")) { @@ -51,7 +53,8 @@ public SplitChange fetch(long since, FetchOptions options) { _log.info("Ignoring line since it does not have 2 or 3 columns: " + lineTrim); continue; } - Optional splitOptional = splitChange.splits.stream().filter(split -> split.name.equals(featureTreatment[0])).findFirst(); + Optional splitOptional = splitChange.featureFlags.d.stream(). + filter(split -> split.name.equals(featureTreatment[0])).findFirst(); Split split = splitOptional.orElse(null); if(split == null) { split = new Split(); @@ -59,7 +62,7 @@ public SplitChange fetch(long since, FetchOptions options) { split.configurations = new HashMap<>(); split.conditions = new ArrayList<>(); } else { - splitChange.splits.remove(split); + splitChange.featureFlags.d.remove(split); } split.status = Status.ACTIVE; split.defaultTreatment = featureTreatment[1]; @@ -67,21 +70,20 @@ public SplitChange fetch(long since, FetchOptions options) { split.trafficAllocation = LocalhostConstants.SIZE_100; split.trafficAllocationSeed = LocalhostConstants.SIZE_1; - Condition condition; - if (featureTreatment.length == 2) { - condition = LocalhostSanitizer.createCondition(null, featureTreatment[1]); - } else { - condition = LocalhostSanitizer.createCondition(featureTreatment[2], featureTreatment[1]); - } + Condition condition = checkCondition(featureTreatment); if(condition.conditionType != ConditionType.ROLLOUT){ split.conditions.add(0, condition); } else { split.conditions.add(condition); } - splitChange.splits.add(split); + splitChange.featureFlags.d.add(split); } - splitChange.till = since; - splitChange.since = since; + splitChange.featureFlags.t = since; + splitChange.featureFlags.s = since; + splitChange.ruleBasedSegments = new ChangeDto<>(); + splitChange.ruleBasedSegments.s = -1; + splitChange.ruleBasedSegments.t = -1; + splitChange.ruleBasedSegments.d = new ArrayList<>(); return splitChange; } catch (FileNotFoundException f) { _log.warn("There was no file named " + _splitFile.getPath() + " found. " + @@ -96,4 +98,14 @@ public SplitChange fetch(long since, FetchOptions options) { throw new IllegalStateException("Problem fetching splitChanges: " + e.getMessage(), e); } } + + private Condition checkCondition(String[] featureTreatment) { + Condition condition; + if (featureTreatment.length == 2) { + condition = LocalhostSanitizer.createCondition(null, featureTreatment[1]); + } else { + condition = LocalhostSanitizer.createCondition(featureTreatment[2], featureTreatment[1]); + } + return condition; + } } \ No newline at end of file diff --git a/client/src/main/java/io/split/client/SplitClientConfig.java b/client/src/main/java/io/split/client/SplitClientConfig.java index 8787c1069..fd312c3b2 100644 --- a/client/src/main/java/io/split/client/SplitClientConfig.java +++ b/client/src/main/java/io/split/client/SplitClientConfig.java @@ -412,6 +412,10 @@ public CustomHeaderDecorator customHeaderDecorator() { return _customHeaderDecorator; } + public boolean isSdkEndpointOverridden() { + return !_endpoint.equals(SDK_ENDPOINT); + } + public CustomHttpModule alternativeHTTPModule() { return _alternativeHTTPModule; } public static final class Builder { diff --git a/client/src/main/java/io/split/client/SplitFactoryBuilder.java b/client/src/main/java/io/split/client/SplitFactoryBuilder.java index 2b48fb0d3..c2271ec4f 100644 --- a/client/src/main/java/io/split/client/SplitFactoryBuilder.java +++ b/client/src/main/java/io/split/client/SplitFactoryBuilder.java @@ -2,7 +2,6 @@ import io.split.inputValidation.ApiKeyValidator; import io.split.grammar.Treatments; -import io.split.service.SplitHttpClient; import io.split.storages.enums.StorageMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index 86b77b8d7..9932cbf8c 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -54,6 +54,7 @@ import io.split.engine.experiments.SplitFetcherImp; import io.split.engine.experiments.SplitParser; import io.split.engine.experiments.SplitSynchronizationTask; +import io.split.engine.experiments.RuleBasedSegmentParser; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTaskImp; import io.split.integrations.IntegrationsConfig; @@ -66,15 +67,20 @@ import io.split.storages.SplitCache; import io.split.storages.SplitCacheConsumer; import io.split.storages.SplitCacheProducer; +import io.split.storages.RuleBasedSegmentCache; +import io.split.storages.RuleBasedSegmentCacheProducer; +import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.enums.OperationMode; import io.split.storages.memory.InMemoryCacheImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.storages.pluggable.adapters.UserCustomEventAdapterProducer; import io.split.storages.pluggable.adapters.UserCustomImpressionAdapterConsumer; import io.split.storages.pluggable.adapters.UserCustomImpressionAdapterProducer; import io.split.storages.pluggable.adapters.UserCustomSegmentAdapterConsumer; import io.split.storages.pluggable.adapters.UserCustomSplitAdapterConsumer; import io.split.storages.pluggable.adapters.UserCustomTelemetryAdapterProducer; +import io.split.storages.pluggable.adapters.UserCustomRuleBasedSegmentAdapterConsumer; import io.split.storages.pluggable.domain.UserStorageWrapper; import io.split.storages.pluggable.synchronizer.TelemetryConsumerSubmitter; import io.split.telemetry.storage.InMemoryTelemetryStorage; @@ -202,6 +208,7 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn // Cache Initialisations SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); FlagSetsFilter flagSetsFilter = new FlagSetsFilterImpl(config.getSetsFilter()); SplitCache splitCache = new InMemoryCacheImp(flagSetsFilter); ImpressionsStorage impressionsStorage = new InMemoryImpressionsStorage(config.impressionsQueueSize()); @@ -212,11 +219,13 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn splitCache, _segmentCache, telemetryStorage, _startTime); // Segments - _segmentSynchronizationTaskImp = buildSegments(config, segmentCache, splitCache); + _segmentSynchronizationTaskImp = buildSegments(config, segmentCache, splitCache, ruleBasedSegmentCache); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); // SplitFetcher - _splitFetcher = buildSplitFetcher(splitCache, splitParser, flagSetsFilter); + _splitFetcher = buildSplitFetcher(splitCache, splitParser, flagSetsFilter, + ruleBasedSegmentParser, ruleBasedSegmentCache, config.isSdkEndpointOverridden()); // SplitSynchronizationTask _splitSynchronizationTask = new SplitSynchronizationTask(_splitFetcher, @@ -244,7 +253,7 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn config.getThreadFactory()); // Evaluator - _evaluator = new EvaluatorImp(splitCache, segmentCache); + _evaluator = new EvaluatorImp(splitCache, segmentCache, ruleBasedSegmentCache); // SplitClient _client = new SplitClientImpl(this, @@ -269,7 +278,7 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn _syncManager = SyncManagerImp.build(splitTasks, _splitFetcher, splitCache, splitAPI, segmentCache, _gates, _telemetryStorageProducer, _telemetrySynchronizer, config, splitParser, - flagSetsFilter); + ruleBasedSegmentParser, flagSetsFilter, ruleBasedSegmentCache); _syncManager.start(); // DestroyOnShutDown @@ -333,7 +342,9 @@ protected SplitFactoryImpl(String apiToken, SplitClientConfig config, CustomStor _gates = new SDKReadinessGates(); _telemetrySynchronizer = new TelemetryConsumerSubmitter(customStorageWrapper, _sdkMetadata); - _evaluator = new EvaluatorImp(userCustomSplitAdapterConsumer, userCustomSegmentAdapterConsumer); + UserCustomRuleBasedSegmentAdapterConsumer userCustomRuleBasedSegmentAdapterConsumer = + new UserCustomRuleBasedSegmentAdapterConsumer(customStorageWrapper); + _evaluator = new EvaluatorImp(userCustomSplitAdapterConsumer, userCustomSegmentAdapterConsumer, userCustomRuleBasedSegmentAdapterConsumer); _impressionsSender = PluggableImpressionSender.create(customStorageWrapper); _uniqueKeysTracker = createUniqueKeysTracker(config); _impressionsManager = buildImpressionsManager(config, userCustomImpressionAdapterConsumer, @@ -392,6 +403,7 @@ protected SplitFactoryImpl(SplitClientConfig config) { SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); FlagSetsFilter flagSetsFilter = new FlagSetsFilterImpl(config.getSetsFilter()); SplitCache splitCache = new InMemoryCacheImp(flagSetsFilter); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); _splitCache = splitCache; _gates = new SDKReadinessGates(); _segmentCache = segmentCache; @@ -409,14 +421,16 @@ protected SplitFactoryImpl(SplitClientConfig config) { segmentCache, _telemetryStorageProducer, _splitCache, - config.getThreadFactory()); + config.getThreadFactory(), + ruleBasedSegmentCache); // SplitFetcher SplitChangeFetcher splitChangeFetcher = createSplitChangeFetcher(config); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); _splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCache, _telemetryStorageProducer, - flagSetsFilter); + flagSetsFilter, ruleBasedSegmentParser, ruleBasedSegmentCache); // SplitSynchronizationTask _splitSynchronizationTask = new SplitSynchronizationTask(_splitFetcher, splitCache, @@ -428,7 +442,7 @@ protected SplitFactoryImpl(SplitClientConfig config) { _impressionsManager, null, null, null); // Evaluator - _evaluator = new EvaluatorImp(splitCache, segmentCache); + _evaluator = new EvaluatorImp(splitCache, segmentCache, ruleBasedSegmentCache); EventsStorage eventsStorage = new NoopEventsStorageImp(); @@ -595,7 +609,7 @@ private static HttpClientBuilder setupProxy(HttpClientBuilder httpClientbuilder, private SegmentSynchronizationTaskImp buildSegments(SplitClientConfig config, SegmentCacheProducer segmentCacheProducer, - SplitCacheConsumer splitCacheConsumer) throws URISyntaxException { + SplitCacheConsumer splitCacheConsumer, RuleBasedSegmentCacheConsumer ruleBasedSegmentCache) throws URISyntaxException { SegmentChangeFetcher segmentChangeFetcher = HttpSegmentChangeFetcher.create(_splitHttpClient, _rootTarget, _telemetryStorageProducer); @@ -605,15 +619,17 @@ private SegmentSynchronizationTaskImp buildSegments(SplitClientConfig config, segmentCacheProducer, _telemetryStorageProducer, splitCacheConsumer, - config.getThreadFactory()); + config.getThreadFactory(), + ruleBasedSegmentCache); } private SplitFetcher buildSplitFetcher(SplitCacheProducer splitCacheProducer, SplitParser splitParser, - FlagSetsFilter flagSetsFilter) throws URISyntaxException { + FlagSetsFilter flagSetsFilter, RuleBasedSegmentParser ruleBasedSegmentParser, + RuleBasedSegmentCacheProducer ruleBasedSegmentCache, boolean isRootURIOverriden) throws URISyntaxException { SplitChangeFetcher splitChangeFetcher = HttpSplitChangeFetcher.create(_splitHttpClient, _rootTarget, - _telemetryStorageProducer); + _telemetryStorageProducer, isRootURIOverriden); return new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, _telemetryStorageProducer, - flagSetsFilter); + flagSetsFilter, ruleBasedSegmentParser, ruleBasedSegmentCache); } private ImpressionsManagerImpl buildImpressionsManager(SplitClientConfig config, diff --git a/client/src/main/java/io/split/client/YamlLocalhostSplitChangeFetcher.java b/client/src/main/java/io/split/client/YamlLocalhostSplitChangeFetcher.java index e90ca1389..b2dccfdca 100644 --- a/client/src/main/java/io/split/client/YamlLocalhostSplitChangeFetcher.java +++ b/client/src/main/java/io/split/client/YamlLocalhostSplitChangeFetcher.java @@ -5,6 +5,7 @@ import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; import io.split.client.dtos.Status; +import io.split.client.dtos.ChangeDto; import io.split.client.utils.InputStreamProvider; import io.split.client.utils.LocalhostConstants; import io.split.engine.common.FetchOptions; @@ -32,17 +33,19 @@ public YamlLocalhostSplitChangeFetcher(InputStreamProvider inputStreamProvider) } @Override - public SplitChange fetch(long since, FetchOptions options) { + public SplitChange fetch(long since, long sinceRBS, FetchOptions options) { try { Yaml yaml = new Yaml(); List>> yamlSplits = yaml.load(_inputStreamProvider.get()); SplitChange splitChange = new SplitChange(); - splitChange.splits = new ArrayList<>(); + splitChange.featureFlags = new ChangeDto<>(); + splitChange.featureFlags.d = new ArrayList<>(); for(Map> aSplit : yamlSplits) { // The outter map is a map with one key, the split name Map.Entry> splitAndValues = aSplit.entrySet().iterator().next(); - Optional splitOptional = splitChange.splits.stream().filter(split -> split.name.equals(splitAndValues.getKey())).findFirst(); + Optional splitOptional = splitChange.featureFlags.d.stream(). + filter(split -> split.name.equals(splitAndValues.getKey())).findFirst(); Split split = splitOptional.orElse(null); if(split == null) { split = new Split(); @@ -50,7 +53,7 @@ public SplitChange fetch(long since, FetchOptions options) { split.configurations = new HashMap<>(); split.conditions = new ArrayList<>(); } else { - splitChange.splits.remove(split); + splitChange.featureFlags.d.remove(split); } String treatment = (String) splitAndValues.getValue().get("treatment"); String configurations = splitAndValues.getValue().get("config") != null ? (String) splitAndValues.getValue().get("config") : null; @@ -68,11 +71,14 @@ public SplitChange fetch(long since, FetchOptions options) { split.trafficTypeName = LocalhostConstants.USER; split.trafficAllocation = LocalhostConstants.SIZE_100; split.trafficAllocationSeed = LocalhostConstants.SIZE_1; - - splitChange.splits.add(split); + splitChange.featureFlags.d.add(split); } - splitChange.till = since; - splitChange.since = since; + splitChange.featureFlags.t = since; + splitChange.featureFlags.s = since; + splitChange.ruleBasedSegments = new ChangeDto<>(); + splitChange.ruleBasedSegments.s = -1; + splitChange.ruleBasedSegments.t = -1; + splitChange.ruleBasedSegments.d = new ArrayList<>(); return splitChange; } catch (Exception e) { throw new IllegalStateException("Problem fetching splitChanges using a yaml file: " + e.getMessage(), e); diff --git a/client/src/main/java/io/split/client/api/SplitView.java b/client/src/main/java/io/split/client/api/SplitView.java index a77013274..cc217fe1f 100644 --- a/client/src/main/java/io/split/client/api/SplitView.java +++ b/client/src/main/java/io/split/client/api/SplitView.java @@ -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; @@ -27,6 +28,7 @@ public class SplitView { public List sets; public String defaultTreatment; public boolean impressionsDisabled; + public List prerequisites; public static SplitView fromParsedSplit(ParsedSplit parsedSplit) { SplitView splitView = new SplitView(); @@ -48,6 +50,8 @@ public static SplitView fromParsedSplit(ParsedSplit parsedSplit) { splitView.treatments = new ArrayList(treatments); splitView.configs = parsedSplit.configurations() == null? Collections.emptyMap() : parsedSplit.configurations() ; splitView.impressionsDisabled = parsedSplit.impressionsDisabled(); + splitView.prerequisites = parsedSplit.prerequisitesMatcher() != null ? + parsedSplit.prerequisitesMatcher().getPrerequisites(): new ArrayList<>(); return splitView; } diff --git a/client/src/main/java/io/split/client/dtos/ChangeDto.java b/client/src/main/java/io/split/client/dtos/ChangeDto.java new file mode 100644 index 000000000..596c05e0e --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/ChangeDto.java @@ -0,0 +1,9 @@ +package io.split.client.dtos; + +import java.util.List; + +public class ChangeDto { + public long s; + public long t; + public List d; +} \ No newline at end of file diff --git a/client/src/main/java/io/split/client/dtos/Excluded.java b/client/src/main/java/io/split/client/dtos/Excluded.java new file mode 100644 index 000000000..e23afa4b0 --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/Excluded.java @@ -0,0 +1,8 @@ +package io.split.client.dtos; + +import java.util.List; + +public class Excluded { + public List keys; + public List segments; +} diff --git a/client/src/main/java/io/split/client/dtos/ExcludedSegments.java b/client/src/main/java/io/split/client/dtos/ExcludedSegments.java new file mode 100644 index 000000000..9e65fa60f --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/ExcludedSegments.java @@ -0,0 +1,27 @@ +package io.split.client.dtos; + +public class ExcludedSegments { + static final String STANDARD_TYPE = "standard"; + static final String RULE_BASED_TYPE = "rule-based"; + + public ExcludedSegments() {} + public ExcludedSegments(String type, String name) { + this.type = type; + this.name = name; + } + + public String type; + public String name; + + public boolean isStandard() { + return STANDARD_TYPE.equals(type); + } + + public boolean isRuleBased() { + return RULE_BASED_TYPE.equals(type); + } + + public String getSegmentName(){ + return name; + } +} diff --git a/client/src/main/java/io/split/client/dtos/MatcherType.java b/client/src/main/java/io/split/client/dtos/MatcherType.java index b8c78a7bd..22f22adb3 100644 --- a/client/src/main/java/io/split/client/dtos/MatcherType.java +++ b/client/src/main/java/io/split/client/dtos/MatcherType.java @@ -37,5 +37,8 @@ public enum MatcherType { GREATER_THAN_OR_EQUAL_TO_SEMVER, LESS_THAN_OR_EQUAL_TO_SEMVER, IN_LIST_SEMVER, - BETWEEN_SEMVER + BETWEEN_SEMVER, + + /* Rule based segment */ + IN_RULE_BASED_SEGMENT } diff --git a/client/src/main/java/io/split/client/dtos/Prerequisites.java b/client/src/main/java/io/split/client/dtos/Prerequisites.java new file mode 100644 index 000000000..644cb5fc4 --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/Prerequisites.java @@ -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 treatments; +} diff --git a/client/src/main/java/io/split/client/dtos/RuleBasedSegment.java b/client/src/main/java/io/split/client/dtos/RuleBasedSegment.java new file mode 100644 index 000000000..56c4756de --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/RuleBasedSegment.java @@ -0,0 +1,37 @@ +package io.split.client.dtos; + +import java.util.ArrayList; +import java.util.List; + +public class RuleBasedSegment { + public String name; + public Status status; + public String trafficTypeName; + public long changeNumber; + public List conditions; + public Excluded excluded; + + @Override + public String toString() { + return "RuleBasedSegment{" + + "name='" + name + '\'' + + ", status=" + status + + ", trafficTypeName='" + trafficTypeName + '\'' + + ", changeNumber=" + changeNumber + '\'' + + excludedToString() + '\'' + + '}'; + } + + public String excludedToString() { + Excluded ts = excluded != null ? excluded : new Excluded(); + if (ts.keys == null) { + ts.keys = new ArrayList<>(); + } + + if (ts.segments == null) { + ts.segments = new ArrayList<>(); + } + + return ", excludedKeys=" + ts.keys + '\'' + ", excludedSegments=" + ts.segments; + } +} diff --git a/client/src/main/java/io/split/client/dtos/Split.java b/client/src/main/java/io/split/client/dtos/Split.java index 866300d37..1b9a01e38 100644 --- a/client/src/main/java/io/split/client/dtos/Split.java +++ b/client/src/main/java/io/split/client/dtos/Split.java @@ -19,6 +19,7 @@ public class Split { public Map configurations; public HashSet sets; public Boolean impressionsDisabled = null; + public List prerequisites; @Override public String toString() { diff --git a/client/src/main/java/io/split/client/dtos/SplitChange.java b/client/src/main/java/io/split/client/dtos/SplitChange.java index ba1130886..f3676bf75 100644 --- a/client/src/main/java/io/split/client/dtos/SplitChange.java +++ b/client/src/main/java/io/split/client/dtos/SplitChange.java @@ -1,9 +1,11 @@ package io.split.client.dtos; -import java.util.List; +import com.google.gson.annotations.SerializedName; public class SplitChange { - public List splits; - public long since; - public long till; + @SerializedName("ff") + public ChangeDto featureFlags; + @SerializedName("rbs") + public ChangeDto ruleBasedSegments; + public boolean clearCache; } diff --git a/client/src/main/java/io/split/client/dtos/SplitChangesOldPayloadDto.java b/client/src/main/java/io/split/client/dtos/SplitChangesOldPayloadDto.java new file mode 100644 index 000000000..aa292f918 --- /dev/null +++ b/client/src/main/java/io/split/client/dtos/SplitChangesOldPayloadDto.java @@ -0,0 +1,34 @@ +package io.split.client.dtos; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.List; + +public class SplitChangesOldPayloadDto { + @SerializedName("since") + public long s; + + @SerializedName("till") + public long t; + + @SerializedName("splits") + public List d; + + public SplitChange toSplitChange() { + SplitChange splitChange = new SplitChange(); + ChangeDto ff = new ChangeDto<>(); + ff.s = this.s; + ff.t = this.t; + ff.d = this.d; + ChangeDto rbs = new ChangeDto<>(); + rbs.d = new ArrayList<>(); + rbs.t = -1; + rbs.s = -1; + + splitChange.featureFlags = ff; + splitChange.ruleBasedSegments = rbs; + + return splitChange; + } +} diff --git a/client/src/main/java/io/split/client/utils/GenericClientUtil.java b/client/src/main/java/io/split/client/utils/GenericClientUtil.java index ac631df80..7953fe5bb 100644 --- a/client/src/main/java/io/split/client/utils/GenericClientUtil.java +++ b/client/src/main/java/io/split/client/utils/GenericClientUtil.java @@ -41,3 +41,4 @@ public static void process(List data, URI endpoint, CloseableHttpClient cl } } + diff --git a/client/src/main/java/io/split/client/utils/LocalhostSanitizer.java b/client/src/main/java/io/split/client/utils/LocalhostSanitizer.java index 309b73759..28282adcb 100644 --- a/client/src/main/java/io/split/client/utils/LocalhostSanitizer.java +++ b/client/src/main/java/io/split/client/utils/LocalhostSanitizer.java @@ -14,6 +14,8 @@ import io.split.client.dtos.SplitChange; import io.split.client.dtos.Status; import io.split.client.dtos.WhitelistMatcherData; +import io.split.client.dtos.RuleBasedSegment; +import io.split.client.dtos.ChangeDto; import java.security.SecureRandom; import java.util.ArrayList; @@ -28,68 +30,158 @@ private LocalhostSanitizer() { } public static SplitChange sanitization(SplitChange splitChange) { - SecureRandom random = new SecureRandom(); - List splitsToRemove = new ArrayList<>(); - if (splitChange.till < LocalhostConstants.DEFAULT_TS || splitChange.till == 0) { - splitChange.till = LocalhostConstants.DEFAULT_TS; - } - if (splitChange.since < LocalhostConstants.DEFAULT_TS || splitChange.since > splitChange.till) { - splitChange.since = splitChange.till; + sanitizeTillAndSince(splitChange); + splitChange.featureFlags.d = sanitizeFeatureFlags(splitChange.featureFlags.d); + splitChange.ruleBasedSegments.d = sanitizeRuleBasedSegments(splitChange.ruleBasedSegments.d); + + return splitChange; + } + + private static List sanitizeRuleBasedSegments(List ruleBasedSegments) { + List ruleBasedSegmentsToRemove = new ArrayList<>(); + if (ruleBasedSegments != null) { + for (RuleBasedSegment ruleBasedSegment : ruleBasedSegments) { + if (ruleBasedSegment.name == null) { + ruleBasedSegmentsToRemove.add(ruleBasedSegment); + continue; + } + ruleBasedSegment.trafficTypeName = sanitizeIfNullOrEmpty(ruleBasedSegment.trafficTypeName, LocalhostConstants.USER); + ruleBasedSegment.status = sanitizeStatus(ruleBasedSegment.status); + ruleBasedSegment.changeNumber = sanitizeChangeNumber(ruleBasedSegment.changeNumber, 0); + ruleBasedSegment.conditions = sanitizeConditions((ArrayList) ruleBasedSegment.conditions, false, + ruleBasedSegment.trafficTypeName); + ruleBasedSegment.excluded.segments = sanitizeExcluded((ArrayList) ruleBasedSegment.excluded.segments); + ruleBasedSegment.excluded.keys = sanitizeExcluded((ArrayList) ruleBasedSegment.excluded.keys); + } + ruleBasedSegments.removeAll(ruleBasedSegmentsToRemove); + } else { + ruleBasedSegments = new ArrayList<>(); } - if (splitChange.splits != null) { - for (Split split: splitChange.splits) { - if (split.name == null){ + return ruleBasedSegments; + } + + private static List sanitizeFeatureFlags(List featureFlags) { + List splitsToRemove = new ArrayList<>(); + if (featureFlags != null) { + for (Split split : featureFlags) { + if (split.name == null) { splitsToRemove.add(split); continue; } - if (split.trafficTypeName == null || split.trafficTypeName.isEmpty()) { - split.trafficTypeName = LocalhostConstants.USER; - } - if (split.trafficAllocation == null || split.trafficAllocation < 0 || split.trafficAllocation > LocalhostConstants.SIZE_100) { - split.trafficAllocation = LocalhostConstants.SIZE_100; - } - if (split.trafficAllocationSeed == null || split.trafficAllocationSeed == 0) { - split.trafficAllocationSeed = - random.nextInt(10) * LocalhostConstants.MILLI_SECONDS; - } - if (split.seed == 0) { - split.seed = - random.nextInt(10) * LocalhostConstants.MILLI_SECONDS; - } - if (split.status == null || split.status != Status.ACTIVE && split.status != Status.ARCHIVED) { - split.status = Status.ACTIVE; - } - if (split.defaultTreatment == null || split.defaultTreatment.isEmpty()) { - split.defaultTreatment = LocalhostConstants.CONTROL; - } - if (split.changeNumber < 0) { - split.changeNumber = 0; - } - if (split.algo != LocalhostConstants.ALGO){ - split.algo = LocalhostConstants.ALGO; - } - if (split.conditions == null) { - split.conditions = new ArrayList<>(); - } + split.trafficTypeName = sanitizeIfNullOrEmpty(split.trafficTypeName, LocalhostConstants.USER); + split.status = sanitizeStatus(split.status); + split.defaultTreatment = sanitizeIfNullOrEmpty(split.defaultTreatment, LocalhostConstants.CONTROL); + split.changeNumber = sanitizeChangeNumber(split.changeNumber, 0); + split.trafficAllocation = sanitizeTrafficAllocation(split.trafficAllocation); + split.trafficAllocationSeed = sanitizeSeed(split.trafficAllocationSeed); + split.seed = sanitizeSeed(split.seed); + split.algo = sanitizeAlgo(split.algo); + split.conditions = sanitizeConditions((ArrayList) split.conditions, false, split.trafficTypeName); + } + featureFlags.removeAll(splitsToRemove); + } else { + featureFlags = new ArrayList<>(); + } + return featureFlags; + } - Condition condition = new Condition(); - if (!split.conditions.isEmpty()){ - condition = split.conditions.get(split.conditions.size() - 1); - } + private static int sanitizeSeed(Integer seed) { + SecureRandom random = new SecureRandom(); + if (seed == null || seed == 0) { + seed = -random.nextInt(10) * LocalhostConstants.MILLI_SECONDS; + } + return seed; + } - if (split.conditions.isEmpty() || !condition.conditionType.equals(ConditionType.ROLLOUT) || - condition.matcherGroup.matchers == null || - condition.matcherGroup.matchers.isEmpty() || - !condition.matcherGroup.matchers.get(0).matcherType.equals(MatcherType.ALL_KEYS)) { - Condition rolloutCondition = new Condition(); - split.conditions.add(createRolloutCondition(rolloutCondition, split.trafficTypeName, null)); - } - } - splitChange.splits.removeAll(splitsToRemove); - return splitChange; + private static int sanitizeAlgo(int algo) { + if (algo != LocalhostConstants.ALGO) { + algo = LocalhostConstants.ALGO; + } + return algo; + } + + private static int sanitizeTrafficAllocation(Integer trafficAllocation) { + if (trafficAllocation == null || trafficAllocation < 0 || trafficAllocation > LocalhostConstants.SIZE_100) { + trafficAllocation = LocalhostConstants.SIZE_100; + } + return trafficAllocation; + } + + private static ArrayList sanitizeConditions(ArrayList conditions, boolean createPartition, String trafficTypeName) { + if (conditions == null) { + conditions = new ArrayList<>(); + } + + Condition condition = new Condition(); + if (!conditions.isEmpty()){ + condition = conditions.get(conditions.size() - 1); + } + + if (conditions.isEmpty() || !condition.conditionType.equals(ConditionType.ROLLOUT) || + condition.matcherGroup.matchers == null || + condition.matcherGroup.matchers.isEmpty() || + !condition.matcherGroup.matchers.get(0).matcherType.equals(MatcherType.ALL_KEYS)) { + Condition rolloutCondition = new Condition(); + conditions.add(createRolloutCondition(rolloutCondition, trafficTypeName, null, createPartition)); + } + return conditions; + } + private static String sanitizeIfNullOrEmpty(String toBeSanitized, String defaultValue) { + if (toBeSanitized == null || toBeSanitized.isEmpty()) { + return defaultValue; + } + return toBeSanitized; + } + + private static long sanitizeChangeNumber(long toBeSanitized, long defaultValue) { + if (toBeSanitized < 0) { + return defaultValue; + } + return toBeSanitized; + } + + private static Status sanitizeStatus(Status toBeSanitized) { + if (toBeSanitized == null || toBeSanitized != Status.ACTIVE && toBeSanitized != Status.ARCHIVED) { + return Status.ACTIVE; + } + return toBeSanitized; + + } + + private static ArrayList sanitizeExcluded(ArrayList excluded) + { + if (excluded == null) { + return new ArrayList<>(); + } + return excluded; + } + + private static SplitChange sanitizeTillAndSince(SplitChange splitChange) { + if (checkTillConditions(splitChange.featureFlags)) { + splitChange.featureFlags.t = LocalhostConstants.DEFAULT_TS; + } + if (checkSinceConditions(splitChange.featureFlags)) { + splitChange.featureFlags.s = splitChange.featureFlags.t; + } + + if (checkTillConditions(splitChange.ruleBasedSegments)) { + splitChange.ruleBasedSegments.t = LocalhostConstants.DEFAULT_TS; + } + if (checkSinceConditions(splitChange.ruleBasedSegments)) { + splitChange.ruleBasedSegments.s = splitChange.ruleBasedSegments.t; } - splitChange.splits = new ArrayList<>(); return splitChange; } - public static SegmentChange sanitization(SegmentChange segmentChange) { + + private static boolean checkTillConditions(ChangeDto change) { + return change.t < LocalhostConstants.DEFAULT_TS || change.t == 0; + } + + private static boolean checkSinceConditions(ChangeDto change) { + return change.s < LocalhostConstants.DEFAULT_TS || change.s > change.t; + } + + public static SegmentChange sanitization(SegmentChange segmentChange) { if (segmentChange.name == null || segmentChange.name.isEmpty()) { return null; } @@ -111,7 +203,7 @@ public static SegmentChange sanitization(SegmentChange segmentChange) { return segmentChange; } - public static Condition createRolloutCondition(Condition condition, String trafficType, String treatment) { + public static Condition createRolloutCondition(Condition condition, String trafficType, String treatment, boolean createPartition) { condition.conditionType = ConditionType.ROLLOUT; condition.matcherGroup = new MatcherGroup(); condition.matcherGroup.combiner = MatcherCombiner.AND; @@ -126,19 +218,21 @@ public static Condition createRolloutCondition(Condition condition, String traff condition.matcherGroup.matchers = new ArrayList<>(); condition.matcherGroup.matchers.add(matcher); - condition.partitions = new ArrayList<>(); - Partition partition1 = new Partition(); - Partition partition2 = new Partition(); - partition1.size = LocalhostConstants.SIZE_100; - partition2.size = LocalhostConstants.SIZE_0; - if (treatment != null) { - partition1.treatment = treatment; - } else { - partition1.treatment = LocalhostConstants.TREATMENT_OFF; - partition2.treatment = LocalhostConstants.TREATMENT_ON; + if (createPartition) { + condition.partitions = new ArrayList<>(); + Partition partition1 = new Partition(); + Partition partition2 = new Partition(); + partition1.size = LocalhostConstants.SIZE_100; + partition2.size = LocalhostConstants.SIZE_0; + if (treatment != null) { + partition1.treatment = treatment; + } else { + partition1.treatment = LocalhostConstants.TREATMENT_OFF; + partition2.treatment = LocalhostConstants.TREATMENT_ON; + } + condition.partitions.add(partition1); + condition.partitions.add(partition2); } - condition.partitions.add(partition1); - condition.partitions.add(partition2); condition.label = DEFAULT_RULE; return condition; @@ -147,7 +241,7 @@ public static Condition createRolloutCondition(Condition condition, String traff public static Condition createCondition(Object keyOrKeys, String treatment) { Condition condition = new Condition(); if (keyOrKeys == null) { - return LocalhostSanitizer.createRolloutCondition(condition, "user", treatment); + return LocalhostSanitizer.createRolloutCondition(condition, "user", treatment, true); } else { if (keyOrKeys instanceof String) { List keys = new ArrayList<>(); diff --git a/client/src/main/java/io/split/client/utils/RuleBasedSegmentProcessor.java b/client/src/main/java/io/split/client/utils/RuleBasedSegmentProcessor.java new file mode 100644 index 000000000..7720367b1 --- /dev/null +++ b/client/src/main/java/io/split/client/utils/RuleBasedSegmentProcessor.java @@ -0,0 +1,65 @@ +package io.split.client.utils; + +import io.split.client.dtos.Excluded; +import io.split.client.dtos.RuleBasedSegment; +import io.split.client.dtos.Status; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.engine.experiments.RuleBasedSegmentParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class RuleBasedSegmentProcessor { + private static final Logger _log = LoggerFactory.getLogger(RuleBasedSegmentProcessor.class); + + private RuleBasedSegmentProcessor() { + throw new IllegalStateException("Utility class"); + } + + public static RuleBasedSegmentsToUpdate processRuleBasedSegmentChanges(RuleBasedSegmentParser ruleBasedSegmentParser, + List ruleBasedSegments) { + List toAdd = new ArrayList<>(); + List toRemove = new ArrayList<>(); + Set segments = new HashSet<>(); + for (RuleBasedSegment ruleBasedSegment : ruleBasedSegments) { + ruleBasedSegment.excluded = checkExcluded(ruleBasedSegment.excluded); + if (ruleBasedSegment.status != Status.ACTIVE) { + // archive. + toRemove.add(ruleBasedSegment.name); + continue; + } + ParsedRuleBasedSegment parsedRuleBasedSegment = ruleBasedSegmentParser.parse(ruleBasedSegment); + if (parsedRuleBasedSegment == null) { + _log.debug(String.format("We could not parse the rule based segment definition for: %s", ruleBasedSegment.name)); + } else { + segments.addAll(parsedRuleBasedSegment.getSegmentsNames()); + toAdd.add(parsedRuleBasedSegment); + } + } + return new RuleBasedSegmentsToUpdate(toAdd, toRemove, segments); + } + + private static Excluded createEmptyExcluded() { + Excluded excluded = new Excluded(); + excluded.segments = new ArrayList<>(); + excluded.keys = new ArrayList<>(); + return excluded; + } + + private static Excluded checkExcluded(Excluded excluded) { + if (excluded == null) { + excluded = createEmptyExcluded(); + } + if (excluded.segments == null) { + excluded.segments = new ArrayList<>(); + } + if (excluded.keys == null) { + excluded.keys = new ArrayList<>(); + } + return excluded; + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/client/utils/RuleBasedSegmentsToUpdate.java b/client/src/main/java/io/split/client/utils/RuleBasedSegmentsToUpdate.java new file mode 100644 index 000000000..850ae8493 --- /dev/null +++ b/client/src/main/java/io/split/client/utils/RuleBasedSegmentsToUpdate.java @@ -0,0 +1,30 @@ +package io.split.client.utils; + +import io.split.engine.experiments.ParsedRuleBasedSegment; + +import java.util.List; +import java.util.Set; + +public class RuleBasedSegmentsToUpdate { + List toAdd; + List toRemove; + Set segments; + + public RuleBasedSegmentsToUpdate(List toAdd, List toRemove, Set segments) { + this.toAdd = toAdd; + this.toRemove = toRemove; + this.segments = segments; + } + + public List getToAdd() { + return toAdd; + } + + public List getToRemove() { + return toRemove; + } + + public Set getSegments() { + return segments; + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/client/utils/Utils.java b/client/src/main/java/io/split/client/utils/Utils.java index 43de59f9d..9a386db55 100644 --- a/client/src/main/java/io/split/client/utils/Utils.java +++ b/client/src/main/java/io/split/client/utils/Utils.java @@ -1,5 +1,6 @@ package io.split.client.utils; +import io.split.client.dtos.ChangeDto; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; @@ -40,4 +41,8 @@ public static URI appendPath(URI root, String pathToAppend) throws URISyntaxExce String path = String.format("%s%s%s", root.getPath(), root.getPath().endsWith("/") ? "" : "/", pathToAppend); return new URIBuilder(root).setPath(path).build(); } + + public static boolean checkExitConditions(ChangeDto change, long cn) { + return change.t < cn && change.t != -1; + } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/common/ConsumerSynchronizer.java b/client/src/main/java/io/split/engine/common/ConsumerSynchronizer.java index 7bcee43a5..c92092537 100644 --- a/client/src/main/java/io/split/engine/common/ConsumerSynchronizer.java +++ b/client/src/main/java/io/split/engine/common/ConsumerSynchronizer.java @@ -35,7 +35,7 @@ public void stopPeriodicFetching() { } @Override - public void refreshSplits(Long targetChangeNumber) { + public void refreshSplits(Long targetChangeNumber, Long ruleBasedSegmentChangeNumber) { //No-Op } diff --git a/client/src/main/java/io/split/engine/common/FetchOptions.java b/client/src/main/java/io/split/engine/common/FetchOptions.java index 926137135..ff7a3d49d 100644 --- a/client/src/main/java/io/split/engine/common/FetchOptions.java +++ b/client/src/main/java/io/split/engine/common/FetchOptions.java @@ -12,6 +12,7 @@ public Builder() {} public Builder(FetchOptions opts) { _targetCN = opts._targetCN; + _targetCnRBS = opts._targetCnRBS; _cacheControlHeaders = opts._cacheControlHeaders; _flagSetsFilter = opts._flagSetsFilter; } @@ -26,16 +27,22 @@ public Builder targetChangeNumber(long targetCN) { return this; } + public Builder targetChangeNumberRBS(long targetCnRBS) { + _targetCnRBS = targetCnRBS; + return this; + } + public Builder flagSetsFilter(String flagSetsFilter) { _flagSetsFilter = flagSetsFilter; return this; } public FetchOptions build() { - return new FetchOptions(_cacheControlHeaders, _targetCN, _flagSetsFilter); + return new FetchOptions(_cacheControlHeaders, _targetCN, _targetCnRBS, _flagSetsFilter); } private long _targetCN = DEFAULT_TARGET_CHANGENUMBER; + private long _targetCnRBS = DEFAULT_TARGET_CHANGENUMBER; private boolean _cacheControlHeaders = false; private String _flagSetsFilter = ""; } @@ -46,6 +53,8 @@ public boolean cacheControlHeadersEnabled() { public long targetCN() { return _targetCN; } + public long targetCnRBS() { return _targetCnRBS; } + public boolean hasCustomCN() { return _targetCN != DEFAULT_TARGET_CHANGENUMBER; } public String flagSetsFilter() { @@ -54,9 +63,11 @@ public String flagSetsFilter() { private FetchOptions(boolean cacheControlHeaders, long targetCN, + long targetCnRBS, String flagSetsFilter) { _cacheControlHeaders = cacheControlHeaders; _targetCN = targetCN; + _targetCnRBS = targetCnRBS; _flagSetsFilter = flagSetsFilter; } @@ -70,6 +81,7 @@ public boolean equals(Object obj) { return Objects.equals(_cacheControlHeaders, other._cacheControlHeaders) && Objects.equals(_targetCN, other._targetCN) + && Objects.equals(_targetCnRBS, other._targetCnRBS) && Objects.equals(_flagSetsFilter, other._flagSetsFilter); } @@ -81,5 +93,6 @@ public int hashCode() { private final boolean _cacheControlHeaders; private final long _targetCN; + private final long _targetCnRBS; private final String _flagSetsFilter; } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/common/LocalhostSynchronizer.java b/client/src/main/java/io/split/engine/common/LocalhostSynchronizer.java index e92151846..211148804 100644 --- a/client/src/main/java/io/split/engine/common/LocalhostSynchronizer.java +++ b/client/src/main/java/io/split/engine/common/LocalhostSynchronizer.java @@ -56,7 +56,7 @@ public void stopPeriodicFetching() { } @Override - public void refreshSplits(Long targetChangeNumber) { + public void refreshSplits(Long targetChangeNumber, Long ruleBasedSegmentChangeNumber) { FetchResult fetchResult = _splitFetcher.forceRefresh(new FetchOptions.Builder().cacheControlHeaders(true).build()); if (fetchResult.isSuccess()){ _log.debug("Refresh feature flags completed"); diff --git a/client/src/main/java/io/split/engine/common/PushManagerImp.java b/client/src/main/java/io/split/engine/common/PushManagerImp.java index 653249308..4862765f4 100644 --- a/client/src/main/java/io/split/engine/common/PushManagerImp.java +++ b/client/src/main/java/io/split/engine/common/PushManagerImp.java @@ -2,6 +2,7 @@ import com.google.common.annotations.VisibleForTesting; import io.split.client.interceptors.FlagSetsFilter; +import io.split.engine.experiments.RuleBasedSegmentParser; import io.split.engine.experiments.SplitParser; import io.split.engine.sse.AuthApiClient; import io.split.engine.sse.AuthApiClientImp; @@ -17,6 +18,7 @@ import io.split.engine.sse.workers.FeatureFlagWorkerImp; import io.split.engine.sse.workers.Worker; +import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SplitCacheProducer; import io.split.telemetry.domain.StreamingEvent; import io.split.telemetry.domain.enums.StreamEventsEnum; @@ -79,9 +81,11 @@ public static PushManagerImp build(Synchronizer synchronizer, ThreadFactory threadFactory, SplitParser splitParser, SplitCacheProducer splitCacheProducer, - FlagSetsFilter flagSetsFilter) { - FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, splitCacheProducer, - telemetryRuntimeProducer, flagSetsFilter); + FlagSetsFilter flagSetsFilter, + RuleBasedSegmentCache ruleBasedSegmentCache, + RuleBasedSegmentParser ruleBasedSegmentParser) { + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, ruleBasedSegmentParser, splitCacheProducer, + ruleBasedSegmentCache, telemetryRuntimeProducer, flagSetsFilter); Worker segmentWorker = new SegmentsWorkerImp(synchronizer); PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(statusMessages, telemetryRuntimeProducer); diff --git a/client/src/main/java/io/split/engine/common/SyncManagerImp.java b/client/src/main/java/io/split/engine/common/SyncManagerImp.java index fb77ad0bd..57faa07b3 100644 --- a/client/src/main/java/io/split/engine/common/SyncManagerImp.java +++ b/client/src/main/java/io/split/engine/common/SyncManagerImp.java @@ -5,10 +5,12 @@ import io.split.client.SplitClientConfig; import io.split.client.interceptors.FlagSetsFilter; import io.split.engine.SDKReadinessGates; +import io.split.engine.experiments.RuleBasedSegmentParser; import io.split.engine.experiments.SplitFetcher; import io.split.engine.experiments.SplitParser; import io.split.engine.experiments.SplitSynchronizationTask; import io.split.engine.segments.SegmentSynchronizationTask; +import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SegmentCacheProducer; import io.split.storages.SplitCacheProducer; import io.split.telemetry.domain.StreamingEvent; @@ -89,12 +91,15 @@ public static SyncManagerImp build(SplitTasks splitTasks, TelemetrySynchronizer telemetrySynchronizer, SplitClientConfig config, SplitParser splitParser, - FlagSetsFilter flagSetsFilter) { + RuleBasedSegmentParser ruleBasedSegmentParser, + FlagSetsFilter flagSetsFilter, + RuleBasedSegmentCache ruleBasedSegmentCache) { LinkedBlockingQueue pushMessages = new LinkedBlockingQueue<>(); Synchronizer synchronizer = new SynchronizerImp(splitTasks, splitFetcher, splitCacheProducer, segmentCacheProducer, + ruleBasedSegmentCache, config.streamingRetryDelay(), config.streamingFetchMaxRetries(), config.failedAttemptsBeforeLogging(), @@ -109,7 +114,9 @@ public static SyncManagerImp build(SplitTasks splitTasks, config.getThreadFactory(), splitParser, splitCacheProducer, - flagSetsFilter); + flagSetsFilter, + ruleBasedSegmentCache, + ruleBasedSegmentParser); return new SyncManagerImp(splitTasks, config.streamingEnabled(), diff --git a/client/src/main/java/io/split/engine/common/Synchronizer.java b/client/src/main/java/io/split/engine/common/Synchronizer.java index 8885e8b16..d685a3ed7 100644 --- a/client/src/main/java/io/split/engine/common/Synchronizer.java +++ b/client/src/main/java/io/split/engine/common/Synchronizer.java @@ -6,7 +6,7 @@ public interface Synchronizer { boolean syncAll(); void startPeriodicFetching(); void stopPeriodicFetching(); - void refreshSplits(Long targetChangeNumber); + void refreshSplits(Long targetChangeNumber, Long ruleBasedSegmentChangeNumber); void localKillSplit(SplitKillNotification splitKillNotification); void refreshSegment(String segmentName, Long targetChangeNumber); void startPeriodicDataRecording(); diff --git a/client/src/main/java/io/split/engine/common/SynchronizerImp.java b/client/src/main/java/io/split/engine/common/SynchronizerImp.java index 81f26ccd4..d9210578c 100644 --- a/client/src/main/java/io/split/engine/common/SynchronizerImp.java +++ b/client/src/main/java/io/split/engine/common/SynchronizerImp.java @@ -9,6 +9,7 @@ import io.split.engine.segments.SegmentFetcher; import io.split.engine.segments.SegmentSynchronizationTask; import io.split.engine.sse.dtos.SplitKillNotification; +import io.split.storages.RuleBasedSegmentCacheProducer; import io.split.storages.SegmentCacheProducer; import io.split.storages.SplitCacheProducer; import io.split.telemetry.synchronizer.TelemetrySyncTask; @@ -34,6 +35,7 @@ public class SynchronizerImp implements Synchronizer { private final SplitFetcher _splitFetcher; private final SegmentSynchronizationTask _segmentSynchronizationTaskImp; private final SplitCacheProducer _splitCacheProducer; + private final RuleBasedSegmentCacheProducer _ruleBasedSegmentCacheProducer; private final SegmentCacheProducer segmentCacheProducer; private final ImpressionsManager _impressionManager; private final EventsTask _eventsTask; @@ -48,6 +50,7 @@ public SynchronizerImp(SplitTasks splitTasks, SplitFetcher splitFetcher, SplitCacheProducer splitCacheProducer, SegmentCacheProducer segmentCacheProducer, + RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer, int onDemandFetchRetryDelayMs, int onDemandFetchMaxRetries, int failedAttemptsBeforeLogging, @@ -56,6 +59,7 @@ public SynchronizerImp(SplitTasks splitTasks, _splitFetcher = checkNotNull(splitFetcher); _segmentSynchronizationTaskImp = checkNotNull(splitTasks.getSegmentSynchronizationTask()); _splitCacheProducer = checkNotNull(splitCacheProducer); + _ruleBasedSegmentCacheProducer = checkNotNull(ruleBasedSegmentCacheProducer); this.segmentCacheProducer = checkNotNull(segmentCacheProducer); _onDemandFetchRetryDelayMs = checkNotNull(onDemandFetchRetryDelayMs); _onDemandFetchMaxRetries = onDemandFetchMaxRetries; @@ -103,7 +107,7 @@ private static class SyncResult { private final FetchResult _fetchResult; } - private SyncResult attemptSplitsSync(long targetChangeNumber, + private SyncResult attemptSplitsSync(long targetChangeNumber, long ruleBasedSegmentChangeNumber, FetchOptions opts, Function nextWaitMs, int maxRetries) { @@ -114,7 +118,8 @@ private SyncResult attemptSplitsSync(long targetChangeNumber, if (fetchResult != null && !fetchResult.retry() && !fetchResult.isSuccess()) { return new SyncResult(false, remainingAttempts, fetchResult); } - if (targetChangeNumber <= _splitCacheProducer.getChangeNumber()) { + if (targetChangeNumber <= _splitCacheProducer.getChangeNumber() + && ruleBasedSegmentChangeNumber <= _ruleBasedSegmentCacheProducer.getChangeNumber()) { return new SyncResult(true, remainingAttempts, fetchResult); } else if (remainingAttempts <= 0) { return new SyncResult(false, remainingAttempts, fetchResult); @@ -130,9 +135,17 @@ private SyncResult attemptSplitsSync(long targetChangeNumber, } @Override - public void refreshSplits(Long targetChangeNumber) { + public void refreshSplits(Long targetChangeNumber, Long ruleBasedSegmentChangeNumber) { - if (targetChangeNumber <= _splitCacheProducer.getChangeNumber()) { + if (targetChangeNumber == null || targetChangeNumber == 0) { + targetChangeNumber = _splitCacheProducer.getChangeNumber(); + } + if (ruleBasedSegmentChangeNumber == null || ruleBasedSegmentChangeNumber == 0) { + ruleBasedSegmentChangeNumber = _ruleBasedSegmentCacheProducer.getChangeNumber(); + } + + if (targetChangeNumber <= _splitCacheProducer.getChangeNumber() + && ruleBasedSegmentChangeNumber <= _ruleBasedSegmentCacheProducer.getChangeNumber()) { return; } @@ -142,7 +155,7 @@ public void refreshSplits(Long targetChangeNumber) { .flagSetsFilter(_sets) .build(); - SyncResult regularResult = attemptSplitsSync(targetChangeNumber, opts, + SyncResult regularResult = attemptSplitsSync(targetChangeNumber, ruleBasedSegmentChangeNumber, opts, (discard) -> (long) _onDemandFetchRetryDelayMs, _onDemandFetchMaxRetries); int attempts = _onDemandFetchMaxRetries - regularResult.remainingAttempts(); @@ -157,7 +170,7 @@ public void refreshSplits(Long targetChangeNumber) { _log.info(String.format("No changes fetched after %s attempts. Will retry bypassing CDN.", attempts)); FetchOptions withCdnBypass = new FetchOptions.Builder(opts).targetChangeNumber(targetChangeNumber).build(); Backoff backoff = new Backoff(ON_DEMAND_FETCH_BACKOFF_BASE_MS, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_MS); - SyncResult withCDNBypassed = attemptSplitsSync(targetChangeNumber, withCdnBypass, + SyncResult withCDNBypassed = attemptSplitsSync(targetChangeNumber, ruleBasedSegmentChangeNumber, withCdnBypass, (discard) -> backoff.interval(), ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); int withoutCDNAttempts = ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES - withCDNBypassed._remainingAttempts; @@ -175,7 +188,7 @@ public void localKillSplit(SplitKillNotification splitKillNotification) { if (splitKillNotification.getChangeNumber() > _splitCacheProducer.getChangeNumber()) { _splitCacheProducer.kill(splitKillNotification.getSplitName(), splitKillNotification.getDefaultTreatment(), splitKillNotification.getChangeNumber()); - refreshSplits(splitKillNotification.getChangeNumber()); + refreshSplits(splitKillNotification.getChangeNumber(), 0L); } } diff --git a/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java b/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java index 7aab69578..540acc5d3 100644 --- a/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java +++ b/client/src/main/java/io/split/engine/evaluator/EvaluationContext.java @@ -1,5 +1,6 @@ package io.split.engine.evaluator; +import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheConsumer; import static com.google.common.base.Preconditions.checkNotNull; @@ -7,10 +8,13 @@ public class EvaluationContext { private final Evaluator _evaluator; private final SegmentCacheConsumer _segmentCacheConsumer; + private final RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; - public EvaluationContext(Evaluator evaluator, SegmentCacheConsumer segmentCacheConsumer) { + public EvaluationContext(Evaluator evaluator, SegmentCacheConsumer segmentCacheConsumer, + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { _evaluator = checkNotNull(evaluator); _segmentCacheConsumer = checkNotNull(segmentCacheConsumer); + _ruleBasedSegmentCacheConsumer = checkNotNull(ruleBasedSegmentCacheConsumer); } public Evaluator getEvaluator() { @@ -20,4 +24,8 @@ public Evaluator getEvaluator() { public SegmentCacheConsumer getSegmentCache() { return _segmentCacheConsumer; } + + public RuleBasedSegmentCacheConsumer getRuleBasedSegmentCache() { + return _ruleBasedSegmentCacheConsumer; + } } diff --git a/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java b/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java index af165ca36..6d31952c3 100644 --- a/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java +++ b/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java @@ -6,6 +6,7 @@ import io.split.engine.experiments.ParsedSplit; import io.split.engine.splitter.Splitter; import io.split.grammar.Treatments; +import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheConsumer; import io.split.storages.SplitCacheConsumer; import org.slf4j.Logger; @@ -26,10 +27,11 @@ public class EvaluatorImp implements Evaluator { private final EvaluationContext _evaluationContext; private final SplitCacheConsumer _splitCacheConsumer; - public EvaluatorImp(SplitCacheConsumer splitCacheConsumer, SegmentCacheConsumer segmentCache) { + public EvaluatorImp(SplitCacheConsumer splitCacheConsumer, SegmentCacheConsumer segmentCache, + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { _splitCacheConsumer = checkNotNull(splitCacheConsumer); _segmentCacheConsumer = checkNotNull(segmentCache); - _evaluationContext = new EvaluationContext(this, _segmentCacheConsumer); + _evaluationContext = new EvaluationContext(this, _segmentCacheConsumer, ruleBasedSegmentCacheConsumer); } @Override @@ -84,8 +86,8 @@ private List getFeatureFlagNamesByFlagSets(List flagSets) { private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bucketingKey, ParsedSplit parsedSplit, Map 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, @@ -94,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 @@ -102,11 +115,9 @@ 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. @@ -114,8 +125,7 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu 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()); } @@ -126,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(), @@ -136,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, @@ -148,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 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()); diff --git a/client/src/main/java/io/split/engine/evaluator/Labels.java b/client/src/main/java/io/split/engine/evaluator/Labels.java index ac5a465a9..9bda16a8b 100644 --- a/client/src/main/java/io/split/engine/evaluator/Labels.java +++ b/client/src/main/java/io/split/engine/evaluator/Labels.java @@ -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"; } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java index 5c2b06b61..ad2e32a50 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java @@ -53,11 +53,12 @@ public int hashCode() { result = 31 * result + _matcher.hashCode(); int partitionsHashCode = 17; - for (Partition p : _partitions) { - partitionsHashCode = 31 * partitionsHashCode + p.treatment.hashCode(); - partitionsHashCode = 31 * partitionsHashCode + p.size; + if (_partitions != null) { + for (Partition p : _partitions) { + partitionsHashCode = 31 * partitionsHashCode + p.treatment.hashCode(); + partitionsHashCode = 31 * partitionsHashCode + p.size; + } } - result = 31 * result + partitionsHashCode; return result; } @@ -75,7 +76,9 @@ public boolean equals(Object obj) { if (!result) { return result; } - + if (_partitions == null) { + return result & (_partitions == other._partitions); + } if (_partitions.size() != other._partitions.size()) { return result; } @@ -97,6 +100,9 @@ public String toString() { bldr.append(_matcher); bldr.append(" then split "); boolean first = true; + if (_partitions == null) { + return bldr.toString(); + } for (Partition partition : _partitions) { if (!first) { bldr.append(','); diff --git a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java new file mode 100644 index 000000000..c00439700 --- /dev/null +++ b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java @@ -0,0 +1,134 @@ +package io.split.engine.experiments; + +import com.google.common.collect.ImmutableList; +import io.split.client.dtos.ExcludedSegments; +import io.split.engine.matchers.AttributeMatcher; +import io.split.engine.matchers.UserDefinedSegmentMatcher; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class ParsedRuleBasedSegment { + + private final String _ruleBasedSegment; + private final ImmutableList _parsedCondition; + private final String _trafficTypeName; + private final long _changeNumber; + private final List _excludedKeys; + private final List _excludedSegments; + + public static ParsedRuleBasedSegment createParsedRuleBasedSegmentForTests( + String ruleBasedSegment, + List matcherAndSplits, + String trafficTypeName, + long changeNumber, + List excludedKeys, + List excludedSegments + ) { + return new ParsedRuleBasedSegment( + ruleBasedSegment, + matcherAndSplits, + trafficTypeName, + changeNumber, + excludedKeys, + excludedSegments + ); + } + + public ParsedRuleBasedSegment( + String ruleBasedSegment, + List matcherAndSplits, + String trafficTypeName, + long changeNumber, + List excludedKeys, + List excludedSegments + ) { + _ruleBasedSegment = ruleBasedSegment; + _parsedCondition = ImmutableList.copyOf(matcherAndSplits); + _trafficTypeName = trafficTypeName; + _changeNumber = changeNumber; + _excludedKeys = excludedKeys; + _excludedSegments = excludedSegments; + } + + public String ruleBasedSegment() { + return _ruleBasedSegment; + } + + public List parsedConditions() { + return _parsedCondition; + } + + public String trafficTypeName() {return _trafficTypeName;} + + public long changeNumber() {return _changeNumber;} + + public List excludedKeys() {return _excludedKeys;} + public List excludedSegments() {return _excludedSegments;} + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _ruleBasedSegment.hashCode(); + result = 31 * result + _parsedCondition.hashCode(); + result = 31 * result + (_trafficTypeName == null ? 0 : _trafficTypeName.hashCode()); + result = 31 * result + (int)(_changeNumber ^ (_changeNumber >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ParsedRuleBasedSegment)) return false; + + ParsedRuleBasedSegment other = (ParsedRuleBasedSegment) obj; + + return _ruleBasedSegment.equals(other._ruleBasedSegment) + && _parsedCondition.equals(other._parsedCondition) + && _trafficTypeName == null ? other._trafficTypeName == null : _trafficTypeName.equals(other._trafficTypeName) + && _changeNumber == other._changeNumber; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("name:"); + bldr.append(_ruleBasedSegment); + bldr.append(", parsedConditions:"); + bldr.append(_parsedCondition); + bldr.append(", trafficTypeName:"); + bldr.append(_trafficTypeName); + bldr.append(", changeNumber:"); + bldr.append(_changeNumber); + return bldr.toString(); + + } + + public Set getSegmentsNames() { + Set segmentNames = excludedSegments() + .stream() + .filter(ExcludedSegments::isStandard) + .map(ExcludedSegments::getSegmentName) + .collect(Collectors.toSet()); + + segmentNames.addAll(parsedConditions().stream() + .flatMap(parsedCondition -> parsedCondition.matcher().attributeMatchers().stream()) + .filter(ParsedRuleBasedSegment::isSegmentMatcher) + .map(ParsedRuleBasedSegment::asSegmentMatcherForEach) + .map(UserDefinedSegmentMatcher::getSegmentName) + .collect(Collectors.toSet())); + + return segmentNames; + } + + private static boolean isSegmentMatcher(AttributeMatcher attributeMatcher) { + return ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate() instanceof UserDefinedSegmentMatcher; + } + + private static UserDefinedSegmentMatcher asSegmentMatcherForEach(AttributeMatcher attributeMatcher) { + return (UserDefinedSegmentMatcher) ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate(); + } + +} diff --git a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java index b94b5d964..e202474f0 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedSplit.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedSplit.java @@ -2,6 +2,8 @@ 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; import java.util.HashSet; @@ -33,6 +35,7 @@ public class ParsedSplit { private final Map _configurations; private final HashSet _flagSets; private final boolean _impressionsDisabled; + private PrerequisitesMatcher _prerequisitesMatcher; public static ParsedSplit createParsedSplitForTests( String feature, @@ -44,7 +47,8 @@ public static ParsedSplit createParsedSplitForTests( long changeNumber, int algo, HashSet flagSets, - boolean impressionsDisabled + boolean impressionsDisabled, + PrerequisitesMatcher prerequisitesMatcher ) { return new ParsedSplit( feature, @@ -59,7 +63,8 @@ public static ParsedSplit createParsedSplitForTests( algo, null, flagSets, - impressionsDisabled + impressionsDisabled, + prerequisitesMatcher ); } @@ -74,7 +79,8 @@ public static ParsedSplit createParsedSplitForTests( int algo, Map configurations, HashSet flagSets, - boolean impressionsDisabled + boolean impressionsDisabled, + PrerequisitesMatcher prerequisitesMatcher ) { return new ParsedSplit( feature, @@ -89,7 +95,8 @@ public static ParsedSplit createParsedSplitForTests( algo, configurations, flagSets, - impressionsDisabled + impressionsDisabled, + prerequisitesMatcher ); } @@ -106,7 +113,8 @@ public ParsedSplit( int algo, Map configurations, HashSet flagSets, - boolean impressionsDisabled + boolean impressionsDisabled, + PrerequisitesMatcher prerequisitesMatcher ) { _split = feature; _seed = seed; @@ -124,6 +132,7 @@ public ParsedSplit( _configurations = configurations; _flagSets = flagSets; _impressionsDisabled = impressionsDisabled; + _prerequisitesMatcher = prerequisitesMatcher; } public String feature() { @@ -170,6 +179,7 @@ public Map configurations() { public boolean impressionsDisabled() { return _impressionsDisabled; } + public PrerequisitesMatcher prerequisitesMatcher() { return _prerequisitesMatcher; } @Override public int hashCode() { @@ -194,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 @@ -230,6 +243,9 @@ public String toString() { bldr.append(_configurations); bldr.append(", impressionsDisabled:"); bldr.append(_impressionsDisabled); + bldr.append(", prerequisites:"); + bldr.append(_prerequisitesMatcher); + return bldr.toString(); } @@ -243,6 +259,15 @@ public Set getSegmentsNames() { .collect(Collectors.toSet()); } + public Set getRuleBasedSegmentsNames() { + return parsedConditions().stream() + .flatMap(parsedCondition -> parsedCondition.matcher().attributeMatchers().stream()) + .filter(ParsedSplit::isRuleBasedSegmentMatcher) + .map(ParsedSplit::asRuleBasedSegmentMatcherForEach) + .map(RuleBasedSegmentMatcher::getSegmentName) + .collect(Collectors.toSet()); + } + private static boolean isSegmentMatcher(AttributeMatcher attributeMatcher) { return ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate() instanceof UserDefinedSegmentMatcher; } @@ -251,4 +276,11 @@ private static UserDefinedSegmentMatcher asSegmentMatcherForEach(AttributeMatche return (UserDefinedSegmentMatcher) ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate(); } + private static boolean isRuleBasedSegmentMatcher(AttributeMatcher attributeMatcher) { + return ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate() instanceof RuleBasedSegmentMatcher; + } + + private static RuleBasedSegmentMatcher asRuleBasedSegmentMatcherForEach(AttributeMatcher attributeMatcher) { + return (RuleBasedSegmentMatcher) ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate(); + } } diff --git a/client/src/main/java/io/split/engine/experiments/ParserUtils.java b/client/src/main/java/io/split/engine/experiments/ParserUtils.java new file mode 100644 index 000000000..3b1355123 --- /dev/null +++ b/client/src/main/java/io/split/engine/experiments/ParserUtils.java @@ -0,0 +1,204 @@ +package io.split.engine.experiments; + +import com.google.common.collect.Lists; +import io.split.client.dtos.MatcherType; +import io.split.client.dtos.Partition; +import io.split.client.dtos.MatcherGroup; +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.Matcher; +import io.split.engine.evaluator.Labels; +import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.AllKeysMatcher; +import io.split.engine.matchers.AttributeMatcher; +import io.split.engine.matchers.UserDefinedSegmentMatcher; +import io.split.engine.matchers.EqualToMatcher; +import io.split.engine.matchers.GreaterThanOrEqualToMatcher; +import io.split.engine.matchers.LessThanOrEqualToMatcher; +import io.split.engine.matchers.BetweenMatcher; +import io.split.engine.matchers.DependencyMatcher; +import io.split.engine.matchers.BooleanMatcher; +import io.split.engine.matchers.EqualToSemverMatcher; +import io.split.engine.matchers.GreaterThanOrEqualToSemverMatcher; +import io.split.engine.matchers.LessThanOrEqualToSemverMatcher; +import io.split.engine.matchers.InListSemverMatcher; +import io.split.engine.matchers.BetweenSemverMatcher; +import io.split.engine.matchers.RuleBasedSegmentMatcher; +import io.split.engine.matchers.collections.ContainsAllOfSetMatcher; +import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; +import io.split.engine.matchers.collections.EqualToSetMatcher; +import io.split.engine.matchers.collections.PartOfSetMatcher; +import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.engine.matchers.strings.StartsWithAnyOfMatcher; +import io.split.engine.matchers.strings.EndsWithAnyOfMatcher; +import io.split.engine.matchers.strings.ContainsAnyOfMatcher; +import io.split.engine.matchers.strings.RegularExpressionMatcher; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public final class ParserUtils { + + private ParserUtils() { + throw new IllegalStateException("Utility class"); + } + + public static boolean checkUnsupportedMatcherExist(List matchers) { + MatcherType typeCheck = null; + for (Matcher matcher : matchers) { + typeCheck = null; + try { + typeCheck = matcher.matcherType; + } catch (NullPointerException e) { + // If the exception is caught, it means unsupported matcher + break; + } + } + return (typeCheck == null); + } + + public static ParsedCondition getTemplateCondition() { + List templatePartitions = Lists.newArrayList(); + Partition partition = new Partition(); + partition.treatment = "control"; + partition.size = 100; + templatePartitions.add(partition); + return new ParsedCondition( + ConditionType.ROLLOUT, + CombiningMatcher.of(new AllKeysMatcher()), + templatePartitions, + Labels.UNSUPPORTED_MATCHER); + } + + public static CombiningMatcher toMatcher(MatcherGroup matcherGroup) { + List matchers = matcherGroup.matchers; + checkArgument(!matchers.isEmpty()); + + List toCombine = Lists.newArrayList(); + + for (Matcher matcher : matchers) { + toCombine.add(toMatcher(matcher)); + } + + return new CombiningMatcher(matcherGroup.combiner, toCombine); + } + + + public static AttributeMatcher toMatcher(Matcher matcher) { + io.split.engine.matchers.Matcher delegate = null; + switch (matcher.matcherType) { + case ALL_KEYS: + delegate = new AllKeysMatcher(); + break; + case IN_SEGMENT: + checkNotNull(matcher.userDefinedSegmentMatcherData); + String segmentName = matcher.userDefinedSegmentMatcherData.segmentName; + delegate = new UserDefinedSegmentMatcher(segmentName); + break; + case WHITELIST: + checkNotNull(matcher.whitelistMatcherData); + delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); + break; + case EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); + delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + break; + case GREATER_THAN_OR_EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); + delegate = new GreaterThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + break; + case LESS_THAN_OR_EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); + delegate = new LessThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + break; + case BETWEEN: + checkNotNull(matcher.betweenMatcherData); + delegate = new BetweenMatcher(matcher.betweenMatcherData.start, matcher.betweenMatcherData.end, matcher.betweenMatcherData.dataType); + break; + case EQUAL_TO_SET: + checkNotNull(matcher.whitelistMatcherData); + delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case PART_OF_SET: + checkNotNull(matcher.whitelistMatcherData); + delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_ALL_OF_SET: + checkNotNull(matcher.whitelistMatcherData); + delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_ANY_OF_SET: + checkNotNull(matcher.whitelistMatcherData); + delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case STARTS_WITH: + checkNotNull(matcher.whitelistMatcherData); + delegate = new StartsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case ENDS_WITH: + checkNotNull(matcher.whitelistMatcherData); + delegate = new EndsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_STRING: + checkNotNull(matcher.whitelistMatcherData); + delegate = new ContainsAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case MATCHES_STRING: + checkNotNull(matcher.stringMatcherData); + delegate = new RegularExpressionMatcher(matcher.stringMatcherData); + break; + case IN_SPLIT_TREATMENT: + checkNotNull(matcher.dependencyMatcherData, + "MatcherType is " + matcher.matcherType + + ". matcher.dependencyMatcherData() MUST NOT BE null"); + delegate = new DependencyMatcher(matcher.dependencyMatcherData.split, matcher.dependencyMatcherData.treatments); + break; + case EQUAL_TO_BOOLEAN: + checkNotNull(matcher.booleanMatcherData, + "MatcherType is " + matcher.matcherType + + ". matcher.booleanMatcherData() MUST NOT BE null"); + delegate = new BooleanMatcher(matcher.booleanMatcherData); + break; + case EQUAL_TO_SEMVER: + checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for EQUAL_TO_SEMVER matcher type"); + delegate = new EqualToSemverMatcher(matcher.stringMatcherData); + break; + case GREATER_THAN_OR_EQUAL_TO_SEMVER: + checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for GREATER_THAN_OR_EQUAL_TO_SEMVER matcher type"); + delegate = new GreaterThanOrEqualToSemverMatcher(matcher.stringMatcherData); + break; + case LESS_THAN_OR_EQUAL_TO_SEMVER: + checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for LESS_THAN_OR_EQUAL_SEMVER matcher type"); + delegate = new LessThanOrEqualToSemverMatcher(matcher.stringMatcherData); + break; + case IN_LIST_SEMVER: + checkNotNull(matcher.whitelistMatcherData, "whitelistMatcherData is required for IN_LIST_SEMVER matcher type"); + delegate = new InListSemverMatcher(matcher.whitelistMatcherData.whitelist); + break; + case BETWEEN_SEMVER: + checkNotNull(matcher.betweenStringMatcherData, "betweenStringMatcherData is required for BETWEEN_SEMVER matcher type"); + delegate = new BetweenSemverMatcher(matcher.betweenStringMatcherData.start, matcher.betweenStringMatcherData.end); + break; + case IN_RULE_BASED_SEGMENT: + checkNotNull(matcher.userDefinedSegmentMatcherData); + String ruleBasedSegmentName = matcher.userDefinedSegmentMatcherData.segmentName; + delegate = new RuleBasedSegmentMatcher(ruleBasedSegmentName); + break; + default: + throw new IllegalArgumentException("Unknown matcher type: " + matcher.matcherType); + } + + checkNotNull(delegate, "We were not able to create a matcher for: " + matcher.matcherType); + + String attribute = null; + if (matcher.keySelector != null && matcher.keySelector.attribute != null) { + attribute = matcher.keySelector.attribute; + } + + boolean negate = matcher.negate; + + + return new AttributeMatcher(attribute, delegate, negate); + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java b/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java new file mode 100644 index 000000000..b67c5e354 --- /dev/null +++ b/client/src/main/java/io/split/engine/experiments/RuleBasedSegmentParser.java @@ -0,0 +1,54 @@ +package io.split.engine.experiments; + +import com.google.common.collect.Lists; +import io.split.client.dtos.Condition; +import io.split.client.dtos.RuleBasedSegment; +import io.split.engine.matchers.CombiningMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import static io.split.engine.experiments.ParserUtils.checkUnsupportedMatcherExist; +import static io.split.engine.experiments.ParserUtils.getTemplateCondition; +import static io.split.engine.experiments.ParserUtils.toMatcher; + +public final class RuleBasedSegmentParser { + + private static final Logger _log = LoggerFactory.getLogger(RuleBasedSegmentParser.class); + + public RuleBasedSegmentParser() { + } + + public ParsedRuleBasedSegment parse(RuleBasedSegment ruleBasedSegment) { + try { + return parseWithoutExceptionHandling(ruleBasedSegment); + } catch (Throwable t) { + _log.error("Could not parse rule based segment: " + ruleBasedSegment, t); + return null; + } + } + + private ParsedRuleBasedSegment parseWithoutExceptionHandling(RuleBasedSegment ruleBasedSegment) { + List parsedConditionList = Lists.newArrayList(); + for (Condition condition : ruleBasedSegment.conditions) { + if (checkUnsupportedMatcherExist(condition.matcherGroup.matchers)) { + _log.error("Unsupported matcher type found for rule based segment: " + ruleBasedSegment.name + + " , will revert to default template matcher."); + parsedConditionList.clear(); + parsedConditionList.add(getTemplateCondition()); + break; + } + CombiningMatcher matcher = toMatcher(condition.matcherGroup); + parsedConditionList.add(new ParsedCondition(condition.conditionType, matcher, null, condition.label)); + } + + return new ParsedRuleBasedSegment( + ruleBasedSegment.name, + parsedConditionList, + ruleBasedSegment.trafficTypeName, + ruleBasedSegment.changeNumber, + ruleBasedSegment.excluded.keys, + ruleBasedSegment.excluded.segments); + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/experiments/SplitChangeFetcher.java b/client/src/main/java/io/split/engine/experiments/SplitChangeFetcher.java index 7c5fbe76e..da6e185fa 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitChangeFetcher.java +++ b/client/src/main/java/io/split/engine/experiments/SplitChangeFetcher.java @@ -32,5 +32,5 @@ public interface SplitChangeFetcher { * @return SegmentChange * @throws java.lang.RuntimeException if there was a problem computing split changes */ - SplitChange fetch(long since, FetchOptions options); + SplitChange fetch(long since, long sinceRBS, FetchOptions options); } diff --git a/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java b/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java index 84b6a287d..a2d8681db 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java +++ b/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java @@ -4,6 +4,8 @@ import io.split.client.exceptions.UriTooLongException; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.utils.FeatureFlagsToUpdate; +import io.split.client.utils.RuleBasedSegmentsToUpdate; +import io.split.storages.RuleBasedSegmentCacheProducer; import io.split.storages.SplitCacheProducer; import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; import io.split.telemetry.storage.TelemetryRuntimeProducer; @@ -16,6 +18,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.split.client.utils.FeatureFlagProcessor.processFeatureFlagChanges; +import static io.split.client.utils.RuleBasedSegmentProcessor.processRuleBasedSegmentChanges; +import static io.split.client.utils.Utils.checkExitConditions; /** * An ExperimentFetcher that refreshes experiment definitions periodically. @@ -32,6 +36,8 @@ public class SplitFetcherImp implements SplitFetcher { private final Object _lock = new Object(); private final TelemetryRuntimeProducer _telemetryRuntimeProducer; private final FlagSetsFilter _flagSetsFilter; + private final RuleBasedSegmentCacheProducer _ruleBasedSegmentCacheProducer; + private final RuleBasedSegmentParser _parserRBS; /** * Contains all the traffic types that are currently being used by the splits and also the count @@ -44,10 +50,13 @@ public class SplitFetcherImp implements SplitFetcher { */ public SplitFetcherImp(SplitChangeFetcher splitChangeFetcher, SplitParser parser, SplitCacheProducer splitCacheProducer, - TelemetryRuntimeProducer telemetryRuntimeProducer, FlagSetsFilter flagSetsFilter) { + TelemetryRuntimeProducer telemetryRuntimeProducer, FlagSetsFilter flagSetsFilter, + RuleBasedSegmentParser parserRBS, RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer) { _splitChangeFetcher = checkNotNull(splitChangeFetcher); _parser = checkNotNull(parser); + _parserRBS = checkNotNull(parserRBS); _splitCacheProducer = checkNotNull(splitCacheProducer); + _ruleBasedSegmentCacheProducer = checkNotNull(ruleBasedSegmentCacheProducer); _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); _flagSetsFilter = flagSetsFilter; } @@ -56,21 +65,31 @@ public SplitFetcherImp(SplitChangeFetcher splitChangeFetcher, SplitParser parser public FetchResult forceRefresh(FetchOptions options) { _log.debug("Force Refresh feature flags starting ..."); final long INITIAL_CN = _splitCacheProducer.getChangeNumber(); + final long RBS_INITIAL_CN = _ruleBasedSegmentCacheProducer.getChangeNumber(); Set segments = new HashSet<>(); try { while (true) { long start = _splitCacheProducer.getChangeNumber(); + long startRBS = _ruleBasedSegmentCacheProducer.getChangeNumber(); segments.addAll(runWithoutExceptionHandling(options)); long end = _splitCacheProducer.getChangeNumber(); + long endRBS = _ruleBasedSegmentCacheProducer.getChangeNumber(); // If the previous execution was the first one, clear the `cdnBypass` flag // for the next fetches. (This will clear a local copy of the fetch options, // not the original object that was passed to this method). + FetchOptions.Builder optionsBuilder = new FetchOptions.Builder(options); if (INITIAL_CN == start) { - options = new FetchOptions.Builder(options).targetChangeNumber(FetchOptions.DEFAULT_TARGET_CHANGENUMBER).build(); + optionsBuilder.targetChangeNumber(FetchOptions.DEFAULT_TARGET_CHANGENUMBER); } - if (start >= end) { + if (RBS_INITIAL_CN == startRBS) { + optionsBuilder.targetChangeNumberRBS(FetchOptions.DEFAULT_TARGET_CHANGENUMBER); + } + + options = optionsBuilder.build(); + + if (start >= end && startRBS >= endRBS) { return new FetchResult(true, false, segments); } } @@ -95,36 +114,56 @@ public void run() { } private Set runWithoutExceptionHandling(FetchOptions options) throws InterruptedException, UriTooLongException { - SplitChange change = _splitChangeFetcher.fetch(_splitCacheProducer.getChangeNumber(), options); + SplitChange change = _splitChangeFetcher.fetch(_splitCacheProducer.getChangeNumber(), + _ruleBasedSegmentCacheProducer.getChangeNumber(), options); Set segments = new HashSet<>(); if (change == null) { throw new IllegalStateException("SplitChange was null"); } - if (change.since != _splitCacheProducer.getChangeNumber() || change.till < _splitCacheProducer.getChangeNumber()) { - // some other thread may have updated the shared state. exit + if (change.clearCache) { + _splitCacheProducer.clear(); + _ruleBasedSegmentCacheProducer.clear(); + } + + if (checkExitConditions(change.featureFlags, _splitCacheProducer.getChangeNumber()) || + checkExitConditions(change.ruleBasedSegments, _ruleBasedSegmentCacheProducer.getChangeNumber())) { return segments; } - if (change.splits.isEmpty()) { - // there are no changes. weird! - _splitCacheProducer.setChangeNumber(change.till); + if (change.featureFlags.d.isEmpty()) { + _splitCacheProducer.setChangeNumber(change.featureFlags.t); + } + + if (change.ruleBasedSegments.d.isEmpty()) { + _ruleBasedSegmentCacheProducer.setChangeNumber(change.ruleBasedSegments.t); + } + + if (change.featureFlags.d.isEmpty() && change.ruleBasedSegments.d.isEmpty()) { return segments; } + synchronized (_lock) { // check state one more time. - if (change.since != _splitCacheProducer.getChangeNumber() - || change.till < _splitCacheProducer.getChangeNumber()) { + if (checkExitConditions(change.featureFlags, _splitCacheProducer.getChangeNumber()) || + checkExitConditions(change.ruleBasedSegments, _ruleBasedSegmentCacheProducer.getChangeNumber())) { // some other thread may have updated the shared state. exit return segments; } - FeatureFlagsToUpdate featureFlagsToUpdate = processFeatureFlagChanges(_parser, change.splits, _flagSetsFilter); + FeatureFlagsToUpdate featureFlagsToUpdate = processFeatureFlagChanges(_parser, change.featureFlags.d, _flagSetsFilter); segments = featureFlagsToUpdate.getSegments(); - _splitCacheProducer.update(featureFlagsToUpdate.getToAdd(), featureFlagsToUpdate.getToRemove(), change.till); + _splitCacheProducer.update(featureFlagsToUpdate.getToAdd(), featureFlagsToUpdate.getToRemove(), change.featureFlags.t); + + RuleBasedSegmentsToUpdate ruleBasedSegmentsToUpdate = processRuleBasedSegmentChanges(_parserRBS, + change.ruleBasedSegments.d); + segments.addAll(ruleBasedSegmentsToUpdate.getSegments()); + _ruleBasedSegmentCacheProducer.update(ruleBasedSegmentsToUpdate.getToAdd(), + ruleBasedSegmentsToUpdate.getToRemove(), change.ruleBasedSegments.t); _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.SPLITS, System.currentTimeMillis()); } + return segments; } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/experiments/SplitParser.java b/client/src/main/java/io/split/engine/experiments/SplitParser.java index 00f1761ef..5771c9ae4 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitParser.java +++ b/client/src/main/java/io/split/engine/experiments/SplitParser.java @@ -1,47 +1,21 @@ package io.split.engine.experiments; import com.google.common.collect.Lists; + import io.split.client.dtos.Condition; -import io.split.client.dtos.Matcher; -import io.split.client.dtos.MatcherGroup; import io.split.client.dtos.Partition; import io.split.client.dtos.Split; -import io.split.client.dtos.ConditionType; -import io.split.client.dtos.MatcherType; -import io.split.engine.evaluator.Labels; -import io.split.engine.matchers.AllKeysMatcher; -import io.split.engine.matchers.AttributeMatcher; -import io.split.engine.matchers.BetweenMatcher; -import io.split.engine.matchers.BooleanMatcher; import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.DependencyMatcher; -import io.split.engine.matchers.EqualToMatcher; -import io.split.engine.matchers.GreaterThanOrEqualToMatcher; -import io.split.engine.matchers.LessThanOrEqualToMatcher; -import io.split.engine.matchers.UserDefinedSegmentMatcher; -import io.split.engine.matchers.EqualToSemverMatcher; -import io.split.engine.matchers.collections.ContainsAllOfSetMatcher; -import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; -import io.split.engine.matchers.collections.EqualToSetMatcher; -import io.split.engine.matchers.collections.PartOfSetMatcher; -import io.split.engine.matchers.strings.ContainsAnyOfMatcher; -import io.split.engine.matchers.strings.EndsWithAnyOfMatcher; -import io.split.engine.matchers.strings.RegularExpressionMatcher; -import io.split.engine.matchers.strings.StartsWithAnyOfMatcher; -import io.split.engine.matchers.strings.WhitelistMatcher; -import io.split.engine.matchers.GreaterThanOrEqualToSemverMatcher; -import io.split.engine.matchers.LessThanOrEqualToSemverMatcher; -import io.split.engine.matchers.InListSemverMatcher; -import io.split.engine.matchers.BetweenSemverMatcher; - +import io.split.engine.matchers.PrerequisitesMatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Objects; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; +import static io.split.engine.experiments.ParserUtils.checkUnsupportedMatcherExist; +import static io.split.engine.experiments.ParserUtils.getTemplateCondition; +import static io.split.engine.experiments.ParserUtils.toMatcher; /** * Converts io.codigo.dtos.Experiment to io.codigo.engine.splits.ParsedExperiment. @@ -95,160 +69,7 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) { split.algo, split.configurations, split.sets, - split.impressionsDisabled); - } - - private boolean checkUnsupportedMatcherExist(List matchers) { - MatcherType typeCheck = null; - for (io.split.client.dtos.Matcher matcher : matchers) { - typeCheck = null; - try { - typeCheck = matcher.matcherType; - } catch (NullPointerException e) { - // If the exception is caught, it means unsupported matcher - break; - } - } - if (typeCheck != null) return false; - return true; - } - - private ParsedCondition getTemplateCondition() { - List templatePartitions = Lists.newArrayList(); - Partition partition = new Partition(); - partition.treatment = "control"; - partition.size = 100; - templatePartitions.add(partition); - return new ParsedCondition( - ConditionType.ROLLOUT, - CombiningMatcher.of(new AllKeysMatcher()), - templatePartitions, - Labels.UNSUPPORTED_MATCHER); - } - - private CombiningMatcher toMatcher(MatcherGroup matcherGroup) { - List matchers = matcherGroup.matchers; - checkArgument(!matchers.isEmpty()); - - List toCombine = Lists.newArrayList(); - - for (io.split.client.dtos.Matcher matcher : matchers) { - toCombine.add(toMatcher(matcher)); - } - - return new CombiningMatcher(matcherGroup.combiner, toCombine); - } - - - private AttributeMatcher toMatcher(Matcher matcher) { - io.split.engine.matchers.Matcher delegate = null; - switch (matcher.matcherType) { - case ALL_KEYS: - delegate = new AllKeysMatcher(); - break; - case IN_SEGMENT: - checkNotNull(matcher.userDefinedSegmentMatcherData); - String segmentName = matcher.userDefinedSegmentMatcherData.segmentName; - delegate = new UserDefinedSegmentMatcher(segmentName); - break; - case WHITELIST: - checkNotNull(matcher.whitelistMatcherData); - delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); - break; - case EQUAL_TO: - checkNotNull(matcher.unaryNumericMatcherData); - delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); - break; - case GREATER_THAN_OR_EQUAL_TO: - checkNotNull(matcher.unaryNumericMatcherData); - delegate = new GreaterThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); - break; - case LESS_THAN_OR_EQUAL_TO: - checkNotNull(matcher.unaryNumericMatcherData); - delegate = new LessThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); - break; - case BETWEEN: - checkNotNull(matcher.betweenMatcherData); - delegate = new BetweenMatcher(matcher.betweenMatcherData.start, matcher.betweenMatcherData.end, matcher.betweenMatcherData.dataType); - break; - case EQUAL_TO_SET: - checkNotNull(matcher.whitelistMatcherData); - delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case PART_OF_SET: - checkNotNull(matcher.whitelistMatcherData); - delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_ALL_OF_SET: - checkNotNull(matcher.whitelistMatcherData); - delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_ANY_OF_SET: - checkNotNull(matcher.whitelistMatcherData); - delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case STARTS_WITH: - checkNotNull(matcher.whitelistMatcherData); - delegate = new StartsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case ENDS_WITH: - checkNotNull(matcher.whitelistMatcherData); - delegate = new EndsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_STRING: - checkNotNull(matcher.whitelistMatcherData); - delegate = new ContainsAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case MATCHES_STRING: - checkNotNull(matcher.stringMatcherData); - delegate = new RegularExpressionMatcher(matcher.stringMatcherData); - break; - case IN_SPLIT_TREATMENT: - checkNotNull(matcher.dependencyMatcherData, - "MatcherType is " + matcher.matcherType - + ". matcher.dependencyMatcherData() MUST NOT BE null"); - delegate = new DependencyMatcher(matcher.dependencyMatcherData.split, matcher.dependencyMatcherData.treatments); - break; - case EQUAL_TO_BOOLEAN: - checkNotNull(matcher.booleanMatcherData, - "MatcherType is " + matcher.matcherType - + ". matcher.booleanMatcherData() MUST NOT BE null"); - delegate = new BooleanMatcher(matcher.booleanMatcherData); - break; - case EQUAL_TO_SEMVER: - checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for EQUAL_TO_SEMVER matcher type"); - delegate = new EqualToSemverMatcher(matcher.stringMatcherData); - break; - case GREATER_THAN_OR_EQUAL_TO_SEMVER: - checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for GREATER_THAN_OR_EQUAL_TO_SEMVER matcher type"); - delegate = new GreaterThanOrEqualToSemverMatcher(matcher.stringMatcherData); - break; - case LESS_THAN_OR_EQUAL_TO_SEMVER: - checkNotNull(matcher.stringMatcherData, "stringMatcherData is required for LESS_THAN_OR_EQUAL_SEMVER matcher type"); - delegate = new LessThanOrEqualToSemverMatcher(matcher.stringMatcherData); - break; - case IN_LIST_SEMVER: - checkNotNull(matcher.whitelistMatcherData, "whitelistMatcherData is required for IN_LIST_SEMVER matcher type"); - delegate = new InListSemverMatcher(matcher.whitelistMatcherData.whitelist); - break; - case BETWEEN_SEMVER: - checkNotNull(matcher.betweenStringMatcherData, "betweenStringMatcherData is required for BETWEEN_SEMVER matcher type"); - delegate = new BetweenSemverMatcher(matcher.betweenStringMatcherData.start, matcher.betweenStringMatcherData.end); - break; - default: - throw new IllegalArgumentException("Unknown matcher type: " + matcher.matcherType); - } - - checkNotNull(delegate, "We were not able to create a matcher for: " + matcher.matcherType); - - String attribute = null; - if (matcher.keySelector != null && matcher.keySelector.attribute != null) { - attribute = matcher.keySelector.attribute; - } - - boolean negate = matcher.negate; - - - return new AttributeMatcher(attribute, delegate, negate); + split.impressionsDisabled, + new PrerequisitesMatcher(split.prerequisites)); } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java b/client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java new file mode 100644 index 000000000..122784498 --- /dev/null +++ b/client/src/main/java/io/split/engine/matchers/PrerequisitesMatcher.java @@ -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; + + public PrerequisitesMatcher(List prerequisites) { + _prerequisites = prerequisites; + } + + public List getPrerequisites() { return _prerequisites; } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map 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; + } +} diff --git a/client/src/main/java/io/split/engine/matchers/RuleBasedSegmentMatcher.java b/client/src/main/java/io/split/engine/matchers/RuleBasedSegmentMatcher.java new file mode 100644 index 000000000..4c74527be --- /dev/null +++ b/client/src/main/java/io/split/engine/matchers/RuleBasedSegmentMatcher.java @@ -0,0 +1,105 @@ +package io.split.engine.matchers; + +import io.split.client.dtos.ExcludedSegments; +import io.split.engine.evaluator.EvaluationContext; +import io.split.engine.experiments.ParsedCondition; +import io.split.engine.experiments.ParsedRuleBasedSegment; + +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A matcher that checks if the key is part of a user defined segment. This class + * assumes that the logic for refreshing what keys are part of a segment is delegated + * to SegmentFetcher. + * + * @author adil + */ +public class RuleBasedSegmentMatcher implements Matcher { + private final String _segmentName; + + public RuleBasedSegmentMatcher(String segmentName) { + _segmentName = checkNotNull(segmentName); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, EvaluationContext evaluationContext) { + if (!(matchValue instanceof String)) { + return false; + } + ParsedRuleBasedSegment parsedRuleBasedSegment = evaluationContext.getRuleBasedSegmentCache().get(_segmentName); + if (parsedRuleBasedSegment == null) { + return false; + } + + if (parsedRuleBasedSegment.excludedKeys().contains(matchValue)) { + return false; + } + + if (matchExcludedSegments(parsedRuleBasedSegment.excludedSegments(), matchValue, bucketingKey, attributes, evaluationContext)) { + return false; + } + + return matchConditions(parsedRuleBasedSegment.parsedConditions(), matchValue, bucketingKey, attributes, evaluationContext); + } + + private boolean matchExcludedSegments(List excludedSegments, Object matchValue, String bucketingKey, + Map attributes, EvaluationContext evaluationContext) { + for (ExcludedSegments excludedSegment: excludedSegments) { + if (excludedSegment.isStandard() && evaluationContext.getSegmentCache().isInSegment(excludedSegment.name, (String) matchValue)) { + return true; + } + + if (excludedSegment.isRuleBased()) { + RuleBasedSegmentMatcher excludedRbsMatcher = new RuleBasedSegmentMatcher(excludedSegment.name); + if (excludedRbsMatcher.match(matchValue, bucketingKey, attributes, evaluationContext)) { + return true; + } + } + } + + return false; + } + + private boolean matchConditions(List conditions, Object matchValue, String bucketingKey, + Map attributes, EvaluationContext evaluationContext) { + for (ParsedCondition parsedCondition : conditions) { + if (parsedCondition.matcher().match((String) matchValue, bucketingKey, attributes, evaluationContext)) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _segmentName.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof RuleBasedSegmentMatcher)) return false; + + RuleBasedSegmentMatcher other = (RuleBasedSegmentMatcher) obj; + + return _segmentName.equals(other._segmentName); + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("in segment "); + bldr.append(_segmentName); + return bldr.toString(); + } + + public String getSegmentName() { + return _segmentName; + } +} diff --git a/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java b/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java index fc7db7a98..857493087 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java +++ b/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java @@ -3,6 +3,7 @@ import com.google.common.collect.Maps; import io.split.client.utils.SplitExecutorFactory; import io.split.engine.common.FetchOptions; +import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheProducer; import io.split.storages.SplitCacheConsumer; import io.split.telemetry.storage.TelemetryRuntimeProducer; @@ -10,8 +11,10 @@ import org.slf4j.LoggerFactory; import java.io.Closeable; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -38,12 +41,14 @@ public class SegmentSynchronizationTaskImp implements SegmentSynchronizationTask private final ScheduledExecutorService _scheduledExecutorService; private final TelemetryRuntimeProducer _telemetryRuntimeProducer; private final SplitCacheConsumer _splitCacheConsumer; + private final RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; private ScheduledFuture _scheduledFuture; public SegmentSynchronizationTaskImp(SegmentChangeFetcher segmentChangeFetcher, long refreshEveryNSeconds, int numThreads, SegmentCacheProducer segmentCacheProducer, TelemetryRuntimeProducer telemetryRuntimeProducer, - SplitCacheConsumer splitCacheConsumer, ThreadFactory threadFactory) { + SplitCacheConsumer splitCacheConsumer, ThreadFactory threadFactory, + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer) { _segmentChangeFetcher = checkNotNull(segmentChangeFetcher); checkArgument(refreshEveryNSeconds >= 0L); @@ -54,6 +59,7 @@ public SegmentSynchronizationTaskImp(SegmentChangeFetcher segmentChangeFetcher, _segmentCacheProducer = checkNotNull(segmentCacheProducer); _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); _splitCacheConsumer = checkNotNull(splitCacheConsumer); + _ruleBasedSegmentCacheConsumer = checkNotNull(ruleBasedSegmentCacheConsumer); } public void initializeSegment(String segmentName) { @@ -136,7 +142,8 @@ public boolean isRunning() { } public void fetchAll(boolean addCacheHeader) { - _splitCacheConsumer.getSegments().forEach(this::initialize); + Set names = getSegmentNames(); + names.forEach(this::initialize); for (Map.Entry entry : _segmentFetchers.entrySet()) { SegmentFetcher fetcher = entry.getValue(); @@ -154,7 +161,8 @@ public void fetchAll(boolean addCacheHeader) { } public boolean fetchAllSynchronous() { - _splitCacheConsumer.getSegments().forEach(this::initialize); + Set names = getSegmentNames(); + names.forEach(this::initialize); List> segmentFetchExecutions = _segmentFetchers.entrySet() .stream().map(e -> _scheduledExecutorService.submit(e.getValue()::runWhitCacheHeader)) .collect(Collectors.toList()); @@ -192,4 +200,11 @@ private void initialize(String segmentName) { _segmentFetchers.putIfAbsent(segmentName, segment); } } + + private Set getSegmentNames() { + Set names = new HashSet<>(_splitCacheConsumer.getSegments()); + names.addAll(_ruleBasedSegmentCacheConsumer.getSegments()); + + return names; + } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/AuthApiClientImp.java b/client/src/main/java/io/split/engine/sse/AuthApiClientImp.java index 28464ebda..5c45e1b7f 100644 --- a/client/src/main/java/io/split/engine/sse/AuthApiClientImp.java +++ b/client/src/main/java/io/split/engine/sse/AuthApiClientImp.java @@ -1,6 +1,7 @@ package io.split.engine.sse; import com.google.gson.JsonObject; +import io.split.Spec; import io.split.client.dtos.SplitHttpResponse; import io.split.client.utils.Json; import io.split.engine.common.FetchOptions; @@ -18,7 +19,6 @@ import java.net.URI; import static com.google.common.base.Preconditions.checkNotNull; -import static io.split.Spec.SPEC_VERSION; public class AuthApiClientImp implements AuthApiClient { private static final Logger _log = LoggerFactory.getLogger(AuthApiClientImp.class); @@ -38,7 +38,7 @@ public AuthApiClientImp(String url, SplitHttpClient httpClient, TelemetryRuntime public AuthenticationResponse Authenticate() { try { long initTime = System.currentTimeMillis(); - URI uri = new URIBuilder(_target).addParameter(SPEC, "" + SPEC_VERSION).build(); + URI uri = new URIBuilder(_target).addParameter(SPEC, "" + Spec.SPEC_1_3).build(); SplitHttpResponse response = _httpClient.get(uri, new FetchOptions.Builder().cacheControlHeaders(false).build(), null); Integer statusCode = response.statusCode(); diff --git a/client/src/main/java/io/split/engine/sse/NotificationParserImp.java b/client/src/main/java/io/split/engine/sse/NotificationParserImp.java index e5fee7502..8bfaf886c 100644 --- a/client/src/main/java/io/split/engine/sse/NotificationParserImp.java +++ b/client/src/main/java/io/split/engine/sse/NotificationParserImp.java @@ -1,10 +1,12 @@ package io.split.engine.sse; +import io.split.client.dtos.RuleBasedSegment; +import io.split.client.dtos.Split; import io.split.client.utils.Json; import io.split.engine.sse.dtos.ControlNotification; import io.split.engine.sse.dtos.ErrorNotification; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.CommonChangeNotification; import io.split.engine.sse.dtos.GenericNotificationData; import io.split.engine.sse.dtos.IncomingNotification; import io.split.engine.sse.dtos.OccupancyNotification; @@ -47,7 +49,9 @@ public ErrorNotification parseError(String payload) throws EventParsingException private IncomingNotification parseNotification(GenericNotificationData genericNotificationData) throws Exception { switch (genericNotificationData.getType()) { case SPLIT_UPDATE: - return new FeatureFlagChangeNotification(genericNotificationData); + return new CommonChangeNotification(genericNotificationData, Split.class); + case RB_SEGMENT_UPDATE: + return new CommonChangeNotification(genericNotificationData, RuleBasedSegment.class); case SPLIT_KILL: return new SplitKillNotification(genericNotificationData); case SEGMENT_UPDATE: diff --git a/client/src/main/java/io/split/engine/sse/NotificationProcessor.java b/client/src/main/java/io/split/engine/sse/NotificationProcessor.java index bdd842455..fce86757c 100644 --- a/client/src/main/java/io/split/engine/sse/NotificationProcessor.java +++ b/client/src/main/java/io/split/engine/sse/NotificationProcessor.java @@ -1,13 +1,12 @@ package io.split.engine.sse; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; import io.split.engine.sse.dtos.IncomingNotification; import io.split.engine.sse.dtos.SplitKillNotification; import io.split.engine.sse.dtos.StatusNotification; public interface NotificationProcessor { void process(IncomingNotification notification); - void processSplitUpdate(FeatureFlagChangeNotification featureFlagChangeNotification); + void processUpdates(IncomingNotification notification); void processSplitKill(SplitKillNotification splitKillNotification); void processSegmentUpdate(long changeNumber, String segmentName); void processStatus(StatusNotification statusNotification); diff --git a/client/src/main/java/io/split/engine/sse/NotificationProcessorImp.java b/client/src/main/java/io/split/engine/sse/NotificationProcessorImp.java index b21a7344a..b833efc31 100644 --- a/client/src/main/java/io/split/engine/sse/NotificationProcessorImp.java +++ b/client/src/main/java/io/split/engine/sse/NotificationProcessorImp.java @@ -1,12 +1,13 @@ package io.split.engine.sse; import com.google.common.annotations.VisibleForTesting; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.client.dtos.Split; import io.split.engine.sse.dtos.GenericNotificationData; import io.split.engine.sse.dtos.IncomingNotification; import io.split.engine.sse.dtos.SplitKillNotification; import io.split.engine.sse.dtos.StatusNotification; import io.split.engine.sse.dtos.SegmentQueueDto; +import io.split.engine.sse.dtos.CommonChangeNotification; import io.split.engine.sse.workers.FeatureFlagsWorker; import io.split.engine.sse.workers.Worker; @@ -31,23 +32,22 @@ public static NotificationProcessorImp build(FeatureFlagsWorker featureFlagsWork return new NotificationProcessorImp(featureFlagsWorker, segmentWorker, pushStatusTracker); } - @Override - public void process(IncomingNotification notification) { - notification.handler(this); + public void processUpdates(IncomingNotification notification) { + _featureFlagsWorker.addToQueue(notification); } @Override - public void processSplitUpdate(FeatureFlagChangeNotification featureFlagChangeNotification) { - _featureFlagsWorker.addToQueue(featureFlagChangeNotification); + public void process(IncomingNotification notification) { + notification.handler(this); } @Override public void processSplitKill(SplitKillNotification splitKillNotification) { _featureFlagsWorker.kill(splitKillNotification); - _featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + _featureFlagsWorker.addToQueue(new CommonChangeNotification<>(GenericNotificationData.builder() .changeNumber(splitKillNotification.getChangeNumber()) .channel(splitKillNotification.getChannel()) - .build())); + .build(), Split.class)); } @Override diff --git a/client/src/main/java/io/split/engine/sse/dtos/FeatureFlagChangeNotification.java b/client/src/main/java/io/split/engine/sse/dtos/CommonChangeNotification.java similarity index 65% rename from client/src/main/java/io/split/engine/sse/dtos/FeatureFlagChangeNotification.java rename to client/src/main/java/io/split/engine/sse/dtos/CommonChangeNotification.java index 05f79abec..f5d335ae5 100644 --- a/client/src/main/java/io/split/engine/sse/dtos/FeatureFlagChangeNotification.java +++ b/client/src/main/java/io/split/engine/sse/dtos/CommonChangeNotification.java @@ -1,6 +1,5 @@ package io.split.engine.sse.dtos; -import io.split.client.dtos.Split; import io.split.client.utils.Json; import io.split.engine.segments.SegmentSynchronizationTaskImp; import io.split.engine.sse.NotificationProcessor; @@ -10,31 +9,36 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.zip.DataFormatException; import static io.split.engine.sse.utils.DecompressionUtil.gZipDecompress; import static io.split.engine.sse.utils.DecompressionUtil.zLibDecompress; -public class FeatureFlagChangeNotification extends IncomingNotification { +public class CommonChangeNotification extends IncomingNotification { private static final Logger _log = LoggerFactory.getLogger(SegmentSynchronizationTaskImp.class); private final long changeNumber; private long previousChangeNumber; - private Split featureFlagDefinition; private CompressType compressType; + private Y definition; + private Class _definitionClass; - public FeatureFlagChangeNotification(GenericNotificationData genericNotificationData) { - super(Type.SPLIT_UPDATE, genericNotificationData.getChannel()); + public CommonChangeNotification(GenericNotificationData genericNotificationData, + Class definitionClass) { + super(genericNotificationData.getType(), genericNotificationData.getChannel()); changeNumber = genericNotificationData.getChangeNumber(); + _definitionClass = definitionClass; + if(genericNotificationData.getPreviousChangeNumber() != null) { previousChangeNumber = genericNotificationData.getPreviousChangeNumber(); } compressType = CompressType.from(genericNotificationData.getCompressType()); - if (compressType == null || genericNotificationData.getFeatureFlagDefinition() == null) { + if (compressType == null || genericNotificationData.getDefinition() == null) { return; } try { - byte[] decodedBytes = Base64.getDecoder().decode(genericNotificationData.getFeatureFlagDefinition()); + byte[] decodedBytes = Base64.getDecoder().decode(genericNotificationData.getDefinition()); switch (compressType) { case GZIP: decodedBytes = gZipDecompress(decodedBytes); @@ -43,13 +47,14 @@ public FeatureFlagChangeNotification(GenericNotificationData genericNotification decodedBytes = zLibDecompress(decodedBytes); break; } - featureFlagDefinition = Json.fromJson(new String(decodedBytes, 0, decodedBytes.length, "UTF-8"), Split.class); + + updateDefinition(decodedBytes); } catch (UnsupportedEncodingException | IllegalArgumentException e) { - _log.warn("Could not decode base64 data in flag definition", e); + _log.warn("Could not decode base64 data in definition", e); } catch (DataFormatException d) { - _log.warn("Could not decompress feature flag definition with zlib algorithm", d); + _log.warn("Could not decompress definition with zlib algorithm", d); } catch (IOException i) { - _log.warn("Could not decompress feature flag definition with gzip algorithm", i); + _log.warn("Could not decompress definition with gzip algorithm", i); } } @@ -59,22 +64,25 @@ public long getChangeNumber() { public long getPreviousChangeNumber() { return previousChangeNumber; } - - public Split getFeatureFlagDefinition() { - return featureFlagDefinition; - } - public CompressType getCompressType() { return compressType; } + public Y getDefinition() { + return definition; + } + @Override public void handler(NotificationProcessor notificationProcessor) { - notificationProcessor.processSplitUpdate(this); + notificationProcessor.processUpdates(this); } @Override public String toString() { return String.format("Type: %s; Channel: %s; ChangeNumber: %s", getType(), getChannel(), getChangeNumber()); } + + private void updateDefinition(byte[] decodedBytes) { + definition = (Y) Json.fromJson(new String(decodedBytes, StandardCharsets.UTF_8), _definitionClass); + } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/dtos/GenericNotificationData.java b/client/src/main/java/io/split/engine/sse/dtos/GenericNotificationData.java index 7fd3dc1bd..998434ec9 100644 --- a/client/src/main/java/io/split/engine/sse/dtos/GenericNotificationData.java +++ b/client/src/main/java/io/split/engine/sse/dtos/GenericNotificationData.java @@ -75,7 +75,7 @@ public Long getPreviousChangeNumber() { return previousChangeNumber; } - public String getFeatureFlagDefinition() { + public String getDefinition() { return featureFlagDefinition; } diff --git a/client/src/main/java/io/split/engine/sse/dtos/IncomingNotification.java b/client/src/main/java/io/split/engine/sse/dtos/IncomingNotification.java index aa476e431..ee00bafe4 100644 --- a/client/src/main/java/io/split/engine/sse/dtos/IncomingNotification.java +++ b/client/src/main/java/io/split/engine/sse/dtos/IncomingNotification.java @@ -5,6 +5,7 @@ public abstract class IncomingNotification { public enum Type { SPLIT_UPDATE, + RB_SEGMENT_UPDATE, SPLIT_KILL, SEGMENT_UPDATE, CONTROL, diff --git a/client/src/main/java/io/split/engine/sse/workers/FeatureFlagWorkerImp.java b/client/src/main/java/io/split/engine/sse/workers/FeatureFlagWorkerImp.java index f66656495..d15d2a438 100644 --- a/client/src/main/java/io/split/engine/sse/workers/FeatureFlagWorkerImp.java +++ b/client/src/main/java/io/split/engine/sse/workers/FeatureFlagWorkerImp.java @@ -1,12 +1,17 @@ package io.split.engine.sse.workers; +import io.split.client.dtos.RuleBasedSegment; import io.split.client.dtos.Split; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.utils.FeatureFlagsToUpdate; +import io.split.client.utils.RuleBasedSegmentsToUpdate; import io.split.engine.common.Synchronizer; +import io.split.engine.experiments.RuleBasedSegmentParser; import io.split.engine.experiments.SplitParser; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.CommonChangeNotification; +import io.split.engine.sse.dtos.IncomingNotification; import io.split.engine.sse.dtos.SplitKillNotification; +import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SplitCacheProducer; import io.split.telemetry.domain.enums.UpdatesFromSSEEnum; import io.split.telemetry.storage.TelemetryRuntimeProducer; @@ -18,23 +23,30 @@ import static com.google.common.base.Preconditions.checkNotNull; import static io.split.client.utils.FeatureFlagProcessor.processFeatureFlagChanges; +import static io.split.client.utils.RuleBasedSegmentProcessor.processRuleBasedSegmentChanges; -public class FeatureFlagWorkerImp extends Worker implements FeatureFlagsWorker { +public class FeatureFlagWorkerImp extends Worker implements FeatureFlagsWorker { private static final Logger _log = LoggerFactory.getLogger(FeatureFlagWorkerImp.class); private final Synchronizer _synchronizer; private final SplitParser _splitParser; + private final RuleBasedSegmentParser _ruleBasedSegmentParser; private final SplitCacheProducer _splitCacheProducer; + private final RuleBasedSegmentCache _ruleBasedSegmentCache; private final TelemetryRuntimeProducer _telemetryRuntimeProducer; private final FlagSetsFilter _flagSetsFilter; - public FeatureFlagWorkerImp(Synchronizer synchronizer, SplitParser splitParser, SplitCacheProducer splitCacheProducer, + public FeatureFlagWorkerImp(Synchronizer synchronizer, SplitParser splitParser, RuleBasedSegmentParser ruleBasedSegmentParser, + SplitCacheProducer splitCacheProducer, + RuleBasedSegmentCache ruleBasedSegmentCache, TelemetryRuntimeProducer telemetryRuntimeProducer, FlagSetsFilter flagSetsFilter) { super("Feature flags"); _synchronizer = checkNotNull(synchronizer); _splitParser = splitParser; + _ruleBasedSegmentParser = ruleBasedSegmentParser; _splitCacheProducer = splitCacheProducer; _telemetryRuntimeProducer = telemetryRuntimeProducer; _flagSetsFilter = flagSetsFilter; + _ruleBasedSegmentCache = ruleBasedSegmentCache; } @Override @@ -49,21 +61,56 @@ public void kill(SplitKillNotification splitKillNotification) { } @Override - protected void executeRefresh(FeatureFlagChangeNotification featureFlagChangeNotification) { - boolean success = addOrUpdateFeatureFlag(featureFlagChangeNotification); - + protected void executeRefresh(IncomingNotification incomingNotification) { + boolean success; + long changeNumber = 0L; + long changeNumberRBS = 0L; + if (incomingNotification.getType() == IncomingNotification.Type.SPLIT_UPDATE) { + CommonChangeNotification featureFlagChangeNotification = (CommonChangeNotification) incomingNotification; + success = addOrUpdateFeatureFlag(featureFlagChangeNotification); + changeNumber = featureFlagChangeNotification.getChangeNumber(); + } else { + CommonChangeNotification ruleBasedSegmentChangeNotification = (CommonChangeNotification) incomingNotification; + success = addOrUpdateRuleBasedSegment(ruleBasedSegmentChangeNotification); + changeNumberRBS = ruleBasedSegmentChangeNotification.getChangeNumber(); + } if (!success) - _synchronizer.refreshSplits(featureFlagChangeNotification.getChangeNumber()); + _synchronizer.refreshSplits(changeNumber, changeNumberRBS); } - private boolean addOrUpdateFeatureFlag(FeatureFlagChangeNotification featureFlagChangeNotification) { + private boolean addOrUpdateRuleBasedSegment(CommonChangeNotification ruleBasedSegmentChangeNotification) { + if (ruleBasedSegmentChangeNotification.getChangeNumber() <= _ruleBasedSegmentCache.getChangeNumber()) { + return true; + } + try { + if (ruleBasedSegmentChangeNotification.getDefinition() != null && + ruleBasedSegmentChangeNotification.getPreviousChangeNumber() == _ruleBasedSegmentCache.getChangeNumber()) { + RuleBasedSegment ruleBasedSegment = (RuleBasedSegment) ruleBasedSegmentChangeNotification.getDefinition(); + RuleBasedSegmentsToUpdate ruleBasedSegmentsToUpdate = processRuleBasedSegmentChanges(_ruleBasedSegmentParser, + Collections.singletonList(ruleBasedSegment)); + _ruleBasedSegmentCache.update(ruleBasedSegmentsToUpdate.getToAdd(), ruleBasedSegmentsToUpdate.getToRemove(), + ruleBasedSegmentChangeNotification.getChangeNumber()); + Set segments = ruleBasedSegmentsToUpdate.getSegments(); + for (String segmentName: segments) { + _synchronizer.forceRefreshSegment(segmentName); + } + // TODO: Add Telemetry once it is spec'd +// _telemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.RULE_BASED_SEGMENTS); + return true; + } + } catch (Exception e) { + _log.warn("Something went wrong processing a Rule based Segment notification", e); + } + return false; + } + private boolean addOrUpdateFeatureFlag(CommonChangeNotification featureFlagChangeNotification) { if (featureFlagChangeNotification.getChangeNumber() <= _splitCacheProducer.getChangeNumber()) { return true; } try { - if (featureFlagChangeNotification.getFeatureFlagDefinition() != null && + if (featureFlagChangeNotification.getDefinition() != null && featureFlagChangeNotification.getPreviousChangeNumber() == _splitCacheProducer.getChangeNumber()) { - Split featureFlag = featureFlagChangeNotification.getFeatureFlagDefinition(); + Split featureFlag = (Split) featureFlagChangeNotification.getDefinition(); FeatureFlagsToUpdate featureFlagsToUpdate = processFeatureFlagChanges(_splitParser, Collections.singletonList(featureFlag), _flagSetsFilter); _splitCacheProducer.update(featureFlagsToUpdate.getToAdd(), featureFlagsToUpdate.getToRemove(), @@ -72,6 +119,12 @@ private boolean addOrUpdateFeatureFlag(FeatureFlagChangeNotification featureFlag for (String segmentName: segments) { _synchronizer.forceRefreshSegment(segmentName); } + if (featureFlagsToUpdate.getToAdd().stream().count() > 0) { + Set ruleBasedSegments = featureFlagsToUpdate.getToAdd().get(0).getRuleBasedSegmentsNames(); + if (!ruleBasedSegments.isEmpty() && !_ruleBasedSegmentCache.contains(ruleBasedSegments)) { + return false; + } + } _telemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); return true; } diff --git a/client/src/main/java/io/split/engine/sse/workers/FeatureFlagsWorker.java b/client/src/main/java/io/split/engine/sse/workers/FeatureFlagsWorker.java index 354dbd7e1..b2cc1fbbc 100644 --- a/client/src/main/java/io/split/engine/sse/workers/FeatureFlagsWorker.java +++ b/client/src/main/java/io/split/engine/sse/workers/FeatureFlagsWorker.java @@ -1,10 +1,10 @@ package io.split.engine.sse.workers; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.IncomingNotification; import io.split.engine.sse.dtos.SplitKillNotification; public interface FeatureFlagsWorker { - void addToQueue(FeatureFlagChangeNotification featureFlagChangeNotification); + void addToQueue(IncomingNotification incomingNotification); void start(); void stop(); void kill(SplitKillNotification splitKillNotification); diff --git a/client/src/main/java/io/split/service/SplitHttpClientImpl.java b/client/src/main/java/io/split/service/SplitHttpClientImpl.java index 7f0674411..7d0939777 100644 --- a/client/src/main/java/io/split/service/SplitHttpClientImpl.java +++ b/client/src/main/java/io/split/service/SplitHttpClientImpl.java @@ -11,6 +11,7 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.HttpEntities; @@ -19,7 +20,6 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import org.apache.hc.core5.http.HttpRequest; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -87,19 +87,20 @@ public SplitHttpResponse get(URI uri, FetchOptions options, Map= HttpStatus.SC_MULTIPLE_CHOICES) { - _log.warn(String.format("Response status was: %s. Reason: %s", response.getCode(), - response.getReasonPhrase())); + int code = response.getCode(); + if (code < HttpStatus.SC_OK || code >= HttpStatus.SC_MULTIPLE_CHOICES) { statusMessage = response.getReasonPhrase(); + _log.warn(String.format("Response status was: %s. Reason: %s", code, statusMessage)); } - return new SplitHttpResponse(response.getCode(), + String body = extractBodyFromResponse(response); + + return new SplitHttpResponse(code, statusMessage, - EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8), + body, Arrays.stream(response.getHeaders()).map( h -> new SplitHttpResponse.Header(h.getName(), Collections.singletonList(h.getValue()))) .collect(Collectors.toList())); - // response.getHeaders()); } catch (Exception e) { throw new IllegalStateException(String.format("Problem in http get operation: %s", e), e); } finally { @@ -107,6 +108,16 @@ public SplitHttpResponse get(URI uri, FetchOptions options, Map> additionalHeaders) throws IOException { diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCache.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCache.java new file mode 100644 index 000000000..5ba55b819 --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCache.java @@ -0,0 +1,4 @@ +package io.split.storages; + +public interface RuleBasedSegmentCache extends RuleBasedSegmentCacheConsumer, RuleBasedSegmentCacheProducer { +} diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java new file mode 100644 index 000000000..348159dd9 --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java @@ -0,0 +1,16 @@ +package io.split.storages; + +import io.split.engine.experiments.ParsedRuleBasedSegment; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public interface RuleBasedSegmentCacheConsumer { + ParsedRuleBasedSegment get(String name); + Collection getAll(); + List ruleBasedSegmentNames(); + boolean contains(Set ruleBasedSegmentNames); + long getChangeNumber(); + Set getSegments(); +} \ No newline at end of file diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java new file mode 100644 index 000000000..a87d38a6f --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java @@ -0,0 +1,13 @@ +package io.split.storages; + +import io.split.engine.experiments.ParsedRuleBasedSegment; + +import java.util.List; + +public interface RuleBasedSegmentCacheProducer { + boolean remove(String name); + void setChangeNumber(long changeNumber); + long getChangeNumber(); + void update(List toAdd, List toRemove, long changeNumber); + void clear(); +} diff --git a/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java b/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java index 8b89d6a64..83e9f3b77 100644 --- a/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java +++ b/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java @@ -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); @@ -140,7 +141,9 @@ public void kill(String splitName, String defaultTreatment, long changeNumber) { @Override public void clear() { _concurrentMap.clear(); + _changeNumber.set(-1); _concurrentTrafficTypeNameSet.clear(); + _flagSets.clear(); } @Override diff --git a/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java b/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java new file mode 100644 index 000000000..53730cf94 --- /dev/null +++ b/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java @@ -0,0 +1,109 @@ +package io.split.storages.memory; + +import com.google.common.collect.Maps; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.storages.RuleBasedSegmentCache; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.ArrayList; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.Map; + +public class RuleBasedSegmentCacheInMemoryImp implements RuleBasedSegmentCache { + + private static final Logger _log = LoggerFactory.getLogger(RuleBasedSegmentCacheInMemoryImp.class); + + private final ConcurrentMap _concurrentMap; + + private AtomicLong _changeNumber; + + public RuleBasedSegmentCacheInMemoryImp() { + this(-1); + } + + public RuleBasedSegmentCacheInMemoryImp(long startingChangeNumber) { + _concurrentMap = Maps.newConcurrentMap(); + _changeNumber = new AtomicLong(startingChangeNumber); + } + + @Override + public boolean remove(String name) { + ParsedRuleBasedSegment removed = _concurrentMap.remove(name); + return removed != null; + } + + @Override + public ParsedRuleBasedSegment get(String name) { + return _concurrentMap.get(name); + } + + @Override + public Collection getAll() { + return _concurrentMap.values(); + } + + @Override + public long getChangeNumber() { + return _changeNumber.get(); + } + + @Override + public void setChangeNumber(long changeNumber) { + if (changeNumber < _changeNumber.get()) { + _log.error("ChangeNumber for feature flags cache is less than previous"); + } + + _changeNumber.set(changeNumber); + } + + @Override + public List ruleBasedSegmentNames() { + List ruleBasedSegmentNamesList = new ArrayList<>(); + for (Map.Entry key: _concurrentMap.entrySet()) { + ruleBasedSegmentNamesList.add(key.getValue().ruleBasedSegment()); + } + return ruleBasedSegmentNamesList; + } + + @Override + public void clear() { + _changeNumber.set(-1); + _concurrentMap.clear(); + } + + private void putMany(List ruleBasedSegments) { + for (ParsedRuleBasedSegment ruleBasedSegment : ruleBasedSegments) { + _concurrentMap.put(ruleBasedSegment.ruleBasedSegment(), ruleBasedSegment); + } + } + + @Override + public void update(List toAdd, List toRemove, long changeNumber) { + if(toAdd != null) { + putMany(toAdd); + } + if(toRemove != null) { + for(String ruleBasedSegment : toRemove) { + remove(ruleBasedSegment); + } + } + setChangeNumber(changeNumber); + } + + public Set getSegments() { + return _concurrentMap.values().stream() + .flatMap(parsedRuleBasedSegment -> parsedRuleBasedSegment.getSegmentsNames().stream()).collect(Collectors.toSet()); + } + + @Override + public boolean contains(Set ruleBasedSegmentNames) { + return getSegments().containsAll(ruleBasedSegmentNames); + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomRuleBasedSegmentAdapterConsumer.java b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomRuleBasedSegmentAdapterConsumer.java new file mode 100644 index 000000000..438b7bf87 --- /dev/null +++ b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomRuleBasedSegmentAdapterConsumer.java @@ -0,0 +1,102 @@ +package io.split.storages.pluggable.adapters; + +import io.split.client.dtos.RuleBasedSegment; +import io.split.client.utils.Json; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.engine.experiments.RuleBasedSegmentParser; +import io.split.storages.RuleBasedSegmentCacheConsumer; +import io.split.storages.pluggable.domain.PrefixAdapter; +import io.split.storages.pluggable.domain.UserStorageWrapper; +import io.split.storages.pluggable.utils.Helper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pluggable.CustomStorageWrapper; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class UserCustomRuleBasedSegmentAdapterConsumer implements RuleBasedSegmentCacheConsumer { + + private static final Logger _log = LoggerFactory.getLogger(UserCustomRuleBasedSegmentAdapterConsumer.class); + + private final RuleBasedSegmentParser _ruleBasedSegmentParser; + private final UserStorageWrapper _userStorageWrapper; + + public UserCustomRuleBasedSegmentAdapterConsumer(CustomStorageWrapper customStorageWrapper) { + _ruleBasedSegmentParser = new RuleBasedSegmentParser(); + _userStorageWrapper = new UserStorageWrapper(checkNotNull(customStorageWrapper)); + } + + @Override + public long getChangeNumber() { + String wrapperResponse = _userStorageWrapper.get(PrefixAdapter.buildRuleBasedSegmentChangeNumber()); + return Helper.responseToLong(wrapperResponse, -1L); + } + + @Override + public ParsedRuleBasedSegment get(String name) { + String wrapperResponse = _userStorageWrapper.get(PrefixAdapter.buildRuleBasedSegmentKey(name)); + if(wrapperResponse == null) { + return null; + } + RuleBasedSegment ruleBasedSegment = Json.fromJson(wrapperResponse, RuleBasedSegment.class); + if(ruleBasedSegment == null) { + _log.warn("Could not parse RuleBasedSegment."); + return null; + } + return _ruleBasedSegmentParser.parse(ruleBasedSegment); + } + + @Override + public Collection getAll() { + Set keys = _userStorageWrapper.getKeysByPrefix(PrefixAdapter.buildGetAllRuleBasedSegment()); + if(keys == null) { + return new ArrayList<>(); + } + List wrapperResponse = _userStorageWrapper.getMany(new ArrayList<>(keys)); + if(wrapperResponse == null) { + return new ArrayList<>(); + } + return stringsToParsedRuleBasedSegments(wrapperResponse); + } + + @Override + public List ruleBasedSegmentNames() { + Set ruleBasedSegmentNamesWithPrefix = _userStorageWrapper.getKeysByPrefix(PrefixAdapter.buildGetAllRuleBasedSegment()); + ruleBasedSegmentNamesWithPrefix = ruleBasedSegmentNamesWithPrefix.stream(). + map(key -> key.replace(PrefixAdapter.buildRuleBasedSegmentsPrefix(), "")). + collect(Collectors.toSet()); + return new ArrayList<>(ruleBasedSegmentNamesWithPrefix); + } + + @Override + public Set getSegments() { + return getAll().stream() + .flatMap(parsedRuleBasedSegment -> parsedRuleBasedSegment. + getSegmentsNames().stream()).collect(Collectors.toSet()); + } + + + private List stringsToParsedRuleBasedSegments(List elements) { + List result = new ArrayList<>(); + for(String s : elements) { + if(s != null) { + result.add(_ruleBasedSegmentParser.parse(Json.fromJson(s, RuleBasedSegment.class))); + continue; + } + result.add(null); + } + return result; + } + + @Override + public boolean contains(Set ruleBasedSegmentNames) { + return getSegments().containsAll(ruleBasedSegmentNames); + } + +} \ No newline at end of file diff --git a/client/src/main/java/io/split/storages/pluggable/domain/PrefixAdapter.java b/client/src/main/java/io/split/storages/pluggable/domain/PrefixAdapter.java index 83842ef03..a785fbe74 100644 --- a/client/src/main/java/io/split/storages/pluggable/domain/PrefixAdapter.java +++ b/client/src/main/java/io/split/storages/pluggable/domain/PrefixAdapter.java @@ -20,6 +20,8 @@ public class PrefixAdapter { private static final String EXCEPTIONS = "exceptions"; private static final String INIT = "init"; private static final String FLAG_SET = "flagSet"; + private static final String RULE_BASED_SEGMENT_PREFIX = "rbsegment"; + private static final String RULE_BASED_SEGMENTS_PREFIX = "rbsegments"; public static String buildSplitKey(String name) { return String.format(DEFAULT_PREFIX+ SPLIT_PREFIX +"%s", name); @@ -37,6 +39,22 @@ public static String buildSplitsPrefix(){ return DEFAULT_PREFIX+SPLIT_PREFIX; } + public static String buildRuleBasedSegmentKey(String name) { + return String.format(DEFAULT_PREFIX+ RULE_BASED_SEGMENT_PREFIX +"%s", name); + } + + public static String buildRuleBasedSegmentsPrefix(){ + return DEFAULT_PREFIX+RULE_BASED_SEGMENT_PREFIX; + } + + public static String buildRuleBasedSegmentChangeNumber() { + return DEFAULT_PREFIX+RULE_BASED_SEGMENTS_PREFIX+"till"; + } + + public static String buildGetAllRuleBasedSegment() { + return DEFAULT_PREFIX+RULE_BASED_SEGMENT_PREFIX+"*"; + } + public static String buildTrafficTypeExists(String trafficType) { return String.format(DEFAULT_PREFIX+TRAFFIC_TYPE_PREFIX+"%s", trafficType); } diff --git a/client/src/test/java/io/split/TestHelper.java b/client/src/test/java/io/split/TestHelper.java index 449a692f6..577a1d00f 100644 --- a/client/src/test/java/io/split/TestHelper.java +++ b/client/src/test/java/io/split/TestHelper.java @@ -1,5 +1,9 @@ package io.split; +import io.split.client.dtos.Condition; +import io.split.client.dtos.Excluded; +import io.split.client.dtos.RuleBasedSegment; +import io.split.client.dtos.Status; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.ClassicHttpResponse; @@ -12,6 +16,8 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; public class TestHelper { public static CloseableHttpClient mockHttpClient(String jsonName, int httpStatus) throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { @@ -36,4 +42,20 @@ public static CloseableHttpResponse classicResponseToCloseableMock(ClassicHttpRe adaptMethod.setAccessible(true); return (CloseableHttpResponse) adaptMethod.invoke(null, mocked); } + + public static RuleBasedSegment makeRuleBasedSegment(String name, List conditions, long changeNumber) { + Excluded excluded = new Excluded(); + excluded.segments = new ArrayList<>(); + excluded.keys = new ArrayList<>(); + + RuleBasedSegment ruleBasedSegment = new RuleBasedSegment(); + ruleBasedSegment.name = name; + ruleBasedSegment.status = Status.ACTIVE; + ruleBasedSegment.conditions = conditions; + ruleBasedSegment.trafficTypeName = "user"; + ruleBasedSegment.changeNumber = changeNumber; + ruleBasedSegment.excluded = excluded; + return ruleBasedSegment; + } + } diff --git a/client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java b/client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java index 8d18f8456..e1198cd0f 100644 --- a/client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java +++ b/client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java @@ -1,11 +1,14 @@ package io.split.client; +import io.split.Spec; import io.split.TestHelper; import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; +import io.split.client.utils.Json; import io.split.client.utils.SDKMetadata; import io.split.engine.common.FetchOptions; import io.split.engine.metrics.Metrics; +import io.split.engine.sse.client.SSEClient; import io.split.service.SplitHttpClient; import io.split.service.SplitHttpClientImpl; import io.split.telemetry.storage.InMemoryTelemetryStorage; @@ -18,6 +21,7 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpStatus; +import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -25,12 +29,13 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.sql.Array; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.mockito.Mockito.when; @@ -46,7 +51,7 @@ public void testDefaultURL() throws URISyntaxException { SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClient, new RequestDecorator(null), "qwerty", metadata()); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE, false); Assert.assertEquals("https://api.split.io/api/splitChanges", fetcher.getTarget().toString()); } @@ -57,7 +62,7 @@ public void testCustomURLNoPathNoBackslash() throws URISyntaxException { SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClient, new RequestDecorator(null), "qwerty", metadata()); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE, false); Assert.assertEquals("https://kubernetesturl.com/split/api/splitChanges", fetcher.getTarget().toString()); } @@ -67,7 +72,7 @@ public void testCustomURLAppendingPath() throws URISyntaxException { CloseableHttpClient httpClient = HttpClients.custom().build(); SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClient, new RequestDecorator(null), "qwerty", metadata()); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE, false); Assert.assertEquals("https://kubernetesturl.com/split/api/splitChanges", fetcher.getTarget().toString()); } @@ -77,7 +82,7 @@ public void testCustomURLAppendingPathNoBackslash() throws URISyntaxException { CloseableHttpClient httpClient = HttpClients.custom().build(); SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClient, new RequestDecorator(null), "qwerty", metadata()); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE, false); Assert.assertEquals("https://kubernetesturl.com/split/api/splitChanges", fetcher.getTarget().toString()); } @@ -92,15 +97,15 @@ public void testFetcherWithSpecialCharacters() throws URISyntaxException, Invoca "qwerty", metadata()); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, TELEMETRY_STORAGE, false); - SplitChange change = fetcher.fetch(1234567, new FetchOptions.Builder().cacheControlHeaders(true).build()); + SplitChange change = fetcher.fetch(1234567, -1, new FetchOptions.Builder().cacheControlHeaders(true).build()); Assert.assertNotNull(change); - Assert.assertEquals(1, change.splits.size()); - Assert.assertNotNull(change.splits.get(0)); + Assert.assertEquals(1, change.featureFlags.d.size()); + Assert.assertNotNull(change.featureFlags.d.get(0)); - Split split = change.splits.get(0); + Split split = change.featureFlags.d.get(0); Map configs = split.configurations; Assert.assertEquals(2, configs.size()); Assert.assertEquals("{\"test\": \"blue\",\"grüne Straße\": 13}", configs.get("on")); @@ -115,7 +120,8 @@ public void testFetcherWithCDNBypassOption() throws IOException, URISyntaxExcept HttpEntity entityMock = Mockito.mock(HttpEntity.class); when(entityMock.getContent()) - .thenReturn(new ByteArrayInputStream("{\"till\": 1}".getBytes(StandardCharsets.UTF_8))); + .thenReturn(new ByteArrayInputStream("{\"ff\":{\"t\": 1,\"s\": -1,\"d\": []},\"rbs\":{\"t\": -1,\"s\": -1,\"d\": []}}". + getBytes(StandardCharsets.UTF_8))); ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class); when(response.getCode()).thenReturn(200); when(response.getEntity()).thenReturn(entityMock); @@ -129,14 +135,13 @@ public void testFetcherWithCDNBypassOption() throws IOException, URISyntaxExcept "qwerty", metadata()); HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, - Mockito.mock(TelemetryRuntimeProducer.class)); + Mockito.mock(TelemetryRuntimeProducer.class), false); - fetcher.fetch(-1, new FetchOptions.Builder().targetChangeNumber(123).build()); - fetcher.fetch(-1, new FetchOptions.Builder().build()); + fetcher.fetch(-1, -1, new FetchOptions.Builder().targetChangeNumber(123).build()); + // TODO: Fix the test with integration tests update List captured = requestCaptor.getAllValues(); - Assert.assertEquals(captured.size(), 2); + Assert.assertEquals(1, captured.size()); Assert.assertTrue(captured.get(0).getUri().toString().contains("till=123")); - Assert.assertFalse(captured.get(1).getUri().toString().contains("till=")); } @Test @@ -146,7 +151,7 @@ public void testRandomNumberGeneration() throws URISyntaxException { SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, new RequestDecorator(null), "qwerty", metadata()); HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, - Mockito.mock(TelemetryRuntimeProducer.class)); + Mockito.mock(TelemetryRuntimeProducer.class), false); Set seen = new HashSet<>(); long min = (long) Math.pow(2, 63) * (-1); @@ -180,7 +185,7 @@ public void testURLTooLong() throws IOException, URISyntaxException, IllegalAcce SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, new RequestDecorator(null), "qwerty", metadata()); HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, - Mockito.mock(TelemetryRuntimeProducer.class)); + Mockito.mock(TelemetryRuntimeProducer.class), false); List sets = new ArrayList(); for (Integer i = 0; i < 100; i++) { sets.add("set" + i.toString()); @@ -188,7 +193,104 @@ public void testURLTooLong() throws IOException, URISyntaxException, IllegalAcce String result = sets.stream() .map(n -> String.valueOf(n)) .collect(Collectors.joining(",", "", "")); - fetcher.fetch(-1, new FetchOptions.Builder().flagSetsFilter(result).cacheControlHeaders(false).build()); + fetcher.fetch(-1, -1, new FetchOptions.Builder().flagSetsFilter(result).cacheControlHeaders(false).build()); + } + + @Test + public void testSwitchingToOldSpec() throws URISyntaxException, InvocationTargetException, + NoSuchMethodException, IllegalAccessException, IOException, NoSuchFieldException, InterruptedException { + URI rootTarget = URI.create("https://api.split.io"); + CloseableHttpClient httpClientMock = Mockito.mock(CloseableHttpClient.class); + HttpEntity entityMock = Mockito.mock(HttpEntity.class); + when(entityMock.getContent()) + .thenReturn(new ByteArrayInputStream("{\"till\": -1, \"since\": -1, \"splits\": []}".getBytes(StandardCharsets.UTF_8))); + HttpEntity entityMock2 = Mockito.mock(HttpEntity.class); + when(entityMock2.getContent()) + .thenReturn(new ByteArrayInputStream("{\"till\": 123, \"since\": 122, \"splits\": [{\"name\":\"some\"}, {\"name\":\"some2\"}]}".getBytes(StandardCharsets.UTF_8))); + HttpEntity entityMock3 = Mockito.mock(HttpEntity.class); + when(entityMock3.getContent()) + .thenReturn(new ByteArrayInputStream("{\"till\": 123, \"since\": 122, \"splits\": [{\"name\":\"some\"}, {\"name\":\"some2\"}]}".getBytes(StandardCharsets.UTF_8))); + HttpEntity entityMock4 = Mockito.mock(HttpEntity.class); + when(entityMock4.getContent()) + .thenReturn(new ByteArrayInputStream("{\"ff\":{\"t\": 123, \"s\": 122, \"d\": [{\"name\":\"some\"}, {\"name\":\"some2\"}]}, \"rbs\":{\"t\": -1, \"s\": -1, \"d\": []}}".getBytes(StandardCharsets.UTF_8))); + ClassicHttpResponse response1 = Mockito.mock(ClassicHttpResponse.class); + when(response1.getCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); + when(response1.getEntity()).thenReturn(entityMock); + when(response1.getHeaders()).thenReturn(new Header[0]); + + ClassicHttpResponse response2 = Mockito.mock(ClassicHttpResponse.class); + when(response2.getCode()).thenReturn(HttpStatus.SC_OK); + when(response2.getEntity()).thenReturn(entityMock2); + when(response2.getHeaders()).thenReturn(new Header[0]); + + ClassicHttpResponse response3 = Mockito.mock(ClassicHttpResponse.class); + when(response3.getCode()).thenReturn(HttpStatus.SC_OK); + when(response3.getEntity()).thenReturn(entityMock3); + when(response3.getHeaders()).thenReturn(new Header[0]); + + ClassicHttpResponse response4 = Mockito.mock(ClassicHttpResponse.class); + when(response4.getCode()).thenReturn(HttpStatus.SC_OK); + when(response4.getEntity()).thenReturn(entityMock4); + when(response4.getHeaders()).thenReturn(new Header[0]); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class); + + when(httpClientMock.execute(requestCaptor.capture())) + .thenReturn(TestHelper.classicResponseToCloseableMock(response1)) + .thenReturn(TestHelper.classicResponseToCloseableMock(response2)) + .thenReturn(TestHelper.classicResponseToCloseableMock(response1)) + .thenReturn(TestHelper.classicResponseToCloseableMock(response3)) + .thenReturn(TestHelper.classicResponseToCloseableMock(response4)); + + SplitHttpClient splitHtpClient = SplitHttpClientImpl.create(httpClientMock, new RequestDecorator(null), + "qwerty", metadata()); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(splitHtpClient, rootTarget, + Mockito.mock(TelemetryRuntimeProducer.class), true); + + SplitChange change = fetcher.fetch(-1, -1, new FetchOptions.Builder().cacheControlHeaders(true).build()); + + List captured = requestCaptor.getAllValues(); + Assert.assertEquals(2, captured.size()); + Assert.assertTrue(captured.get(0).getUri().toString().contains("s=1.3")); + Assert.assertTrue(captured.get(1).getUri().toString().contains("s=1.1")); + Assert.assertEquals(122, change.featureFlags.s); + Assert.assertEquals(123, change.featureFlags.t); + Assert.assertEquals(2, change.featureFlags.d.size()); + Assert.assertEquals(Json.fromJson("{\"name\":\"some\"}", Split.class).name, change.featureFlags.d.get(0).name); + Assert.assertEquals(Json.fromJson("{\"name\":\"some2\"}", Split.class).name, change.featureFlags.d.get(1).name); + Assert.assertEquals(0, change.ruleBasedSegments.d.size()); + Assert.assertEquals(-1, change.ruleBasedSegments.s); + Assert.assertEquals(-1, change.ruleBasedSegments.t); + + // Set proxy interval to low number to force check for spec 1.3 + Field proxyInterval = fetcher.getClass().getDeclaredField("PROXY_CHECK_INTERVAL_MILLISECONDS_SS"); + proxyInterval.setAccessible(true); + proxyInterval.set(fetcher, 5); + Awaitility.await() + .atMost(1L, TimeUnit.SECONDS) + .untilAsserted(() -> Assert.assertTrue(proxyInterval.get(fetcher).equals(5))); + + change = fetcher.fetch(-1, -1, new FetchOptions.Builder().cacheControlHeaders(true).build()); + + Assert.assertTrue(captured.get(2).getUri().toString().contains("s=1.3")); + Assert.assertTrue(captured.get(3).getUri().toString().contains("s=1.1")); + Assert.assertEquals(122, change.featureFlags.s); + Assert.assertEquals(123, change.featureFlags.t); + Assert.assertEquals(2, change.featureFlags.d.size()); + Assert.assertEquals(Json.fromJson("{\"name\":\"some\"}", Split.class).name, change.featureFlags.d.get(0).name); + Assert.assertEquals(Json.fromJson("{\"name\":\"some2\"}", Split.class).name, change.featureFlags.d.get(1).name); + + // test if proxy is upgraded and spec 1.3 now works. + Awaitility.await() + .atMost(5L, TimeUnit.SECONDS) + .untilAsserted(() -> Assert.assertTrue(captured.size() >= 4)); + change = fetcher.fetch(-1, -1, new FetchOptions.Builder().cacheControlHeaders(true).build()); + Assert.assertTrue(captured.get(4).getUri().toString().contains("s=1.3")); + Assert.assertEquals(122, change.featureFlags.s); + Assert.assertEquals(123, change.featureFlags.t); + Assert.assertEquals(2, change.featureFlags.d.size()); + Assert.assertEquals(Json.fromJson("{\"name\":\"some\"}", Split.class).name, change.featureFlags.d.get(0).name); + Assert.assertEquals(Json.fromJson("{\"name\":\"some2\"}", Split.class).name, change.featureFlags.d.get(1).name); } private SDKMetadata metadata() { diff --git a/client/src/test/java/io/split/client/JsonLocalhostSplitChangeFetcherTest.java b/client/src/test/java/io/split/client/JsonLocalhostSplitChangeFetcherTest.java index c355734aa..583dddab8 100644 --- a/client/src/test/java/io/split/client/JsonLocalhostSplitChangeFetcherTest.java +++ b/client/src/test/java/io/split/client/JsonLocalhostSplitChangeFetcherTest.java @@ -1,14 +1,12 @@ package io.split.client; -import io.split.client.dtos.ConditionType; -import io.split.client.dtos.Split; -import io.split.client.dtos.SplitChange; -import io.split.client.dtos.Status; +import io.split.client.dtos.*; import io.split.client.utils.FileInputStreamProvider; import io.split.client.utils.InputStreamProvider; import io.split.client.utils.StaticContentInputStreamProvider; import io.split.engine.common.FetchOptions; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -19,6 +17,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -26,12 +25,13 @@ public class JsonLocalhostSplitChangeFetcherTest { @Rule public TemporaryFolder folder = new TemporaryFolder(); - private String TEST_0 = "{\"splits\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":-1}"; - private String TEST_1 = "{\"splits\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":-1}"; - private String TEST_2 = "{\"splits\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":2323}"; - private String TEST_3 = "{\"splits\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":2323}"; - private String TEST_4 = "{\"splits\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":445345}"; - private String TEST_5 = "{\"splits\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":-1}"; + private String TEST_0 = "{\"ff\":{\"d\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":-1},\"rbs\":{\"d\":[{\"changeNumber\":5,\"name\":\"sample_rule_based_segment\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[\"mauro@split.io\"],\"segments\":[]},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}}]}],\"s\":-1,\"t\":-1}}"; + private String TEST_1 = "{\"ff\":{\"d\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":-1},\"rbs\":{\"d\":[{\"changeNumber\":5,\"name\":\"sample_rule_based_segment\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[\"mauro@split.io\"],\"segments\":[]},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}}]}],\"s\":-1,\"t\":-1}}"; + private String TEST_2 = "{\"ff\":{\"d\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":2323},\"rbs\":{\"d\":[{\"changeNumber\":5,\"name\":\"sample_rule_based_segment\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[\"mauro@split.io\"],\"segments\":[]},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}}]}],\"s\":-1,\"t\":-1}}"; + private String TEST_3 = "{\"ff\":{\"d\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":2323},\"rbs\":{\"d\":[{\"changeNumber\":5,\"name\":\"sample_rule_based_segment\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[\"mauro@split.io\",\"gaston@split.io\"],\"segments\":[]},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}}]}],\"s\":-1,\"t\":1122}}"; + private String TEST_4 = "{\"ff\":{\"d\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":445345},\"rbs\":{\"d\":[{\"changeNumber\":5,\"name\":\"sample_rule_based_segment\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[\"mauro@split.io\",\"gaston@split.io\"],\"segments\":[]},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}}]}],\"s\":-1,\"t\":5566}}"; + private String TEST_5 = "{\"ff\":{\"d\":[{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_1\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"user\",\"name\":\"SPLIT_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1780071202,\"seed\":-1442762199,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1675443537882,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":-1},\"rbs\":{\"d\":[{\"changeNumber\":5,\"name\":\"sample_rule_based_segment\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[\"mauro@split.io\"],\"segments\":[]},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}}]}],\"s\":-1,\"t\":-1}}"; + @Test public void testParseSplitChange() throws FileNotFoundException { InputStream inputStream = new FileInputStream("src/test/resources/split_init.json"); @@ -39,12 +39,12 @@ public void testParseSplitChange() throws FileNotFoundException { JsonLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); - List split = splitChange.splits; + List split = splitChange.featureFlags.d; Assert.assertEquals(7, split.size()); - Assert.assertEquals(1660326991072L, splitChange.till); - Assert.assertEquals(-1L, splitChange.since); + Assert.assertEquals(1660326991072L, splitChange.featureFlags.t); + Assert.assertEquals(-1L, splitChange.featureFlags.s); } @Test @@ -54,10 +54,14 @@ public void testSinceAndTillSanitization() throws FileNotFoundException { JsonLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); + + Assert.assertEquals(-1L, splitChange.featureFlags.t); + Assert.assertEquals(-1L, splitChange.featureFlags.s); + + Assert.assertEquals(-1L, splitChange.ruleBasedSegments.t); + Assert.assertEquals(-1L, splitChange.ruleBasedSegments.s); - Assert.assertEquals(-1L, splitChange.till); - Assert.assertEquals(-1L, splitChange.since); } @Test @@ -67,9 +71,10 @@ public void testSplitChangeWithoutSplits() throws FileNotFoundException { JsonLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); - Assert.assertEquals(0, splitChange.splits.size()); + Assert.assertEquals(0, splitChange.featureFlags.d.size()); + Assert.assertEquals(0, splitChange.ruleBasedSegments.d.size()); } @Test @@ -79,14 +84,20 @@ public void testSplitChangeSplitsToSanitize() throws FileNotFoundException { JsonLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); - Assert.assertEquals(1, splitChange.splits.size()); - Split split = splitChange.splits.get(0); + Assert.assertEquals(1, splitChange.featureFlags.d.size()); + Split split = splitChange.featureFlags.d.get(0); Assert.assertEquals(Optional.of(100), Optional.of(split.trafficAllocation)); Assert.assertEquals(Status.ACTIVE, split.status); Assert.assertEquals("control", split.defaultTreatment); Assert.assertEquals(ConditionType.ROLLOUT, split.conditions.get(split.conditions.size() - 1).conditionType); + + Assert.assertEquals(1, splitChange.ruleBasedSegments.d.size()); + RuleBasedSegment ruleBasedSegment = splitChange.ruleBasedSegments.d.get(0); + Assert.assertEquals(Status.ACTIVE, split.status); + Assert.assertEquals(ConditionType.ROLLOUT, ruleBasedSegment.conditions.get(ruleBasedSegment.conditions.size() - 1).conditionType); + Assert.assertEquals(new ArrayList<>(), ruleBasedSegment.excluded.segments); } @Test @@ -96,10 +107,10 @@ public void testSplitChangeSplitsToSanitizeMatchersNull() throws FileNotFoundExc JsonLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); - Assert.assertEquals(1, splitChange.splits.size()); - Split split = splitChange.splits.get(0); + Assert.assertEquals(1, splitChange.featureFlags.d.size()); + Split split = splitChange.featureFlags.d.get(0); Assert.assertEquals(Optional.of(100), Optional.of(split.trafficAllocation)); Assert.assertEquals(Status.ACTIVE, split.status); Assert.assertEquals("off", split.defaultTreatment); @@ -119,55 +130,67 @@ public void testSplitChangeSplitsDifferentScenarios() throws IOException { FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); // 0) The CN from storage is -1, till and since are -1, and sha doesn't exist in the hash. It's going to return a split change with updates. - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); - Assert.assertEquals(1, splitChange.splits.size()); - Assert.assertEquals(-1, splitChange.till); - Assert.assertEquals(-1, splitChange.since); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); + Assert.assertEquals(1, splitChange.featureFlags.d.size()); + Assert.assertEquals(-1, splitChange.featureFlags.t); + Assert.assertEquals(-1, splitChange.featureFlags.s); + Assert.assertEquals(1, splitChange.ruleBasedSegments.d.size()); + Assert.assertEquals(-1, splitChange.ruleBasedSegments.t); + Assert.assertEquals(-1, splitChange.ruleBasedSegments.s); test = TEST_1.getBytes(); com.google.common.io.Files.write(test, file); // 1) The CN from storage is -1, till and since are -1, and sha is different than before. It's going to return a split change with updates. - splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); - Assert.assertEquals(2, splitChange.splits.size()); - Assert.assertEquals(-1, splitChange.till); - Assert.assertEquals(-1, splitChange.since); + splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); + Assert.assertEquals(2, splitChange.featureFlags.d.size()); + Assert.assertEquals(-1, splitChange.featureFlags.t); + Assert.assertEquals(-1, splitChange.featureFlags.s); test = TEST_2.getBytes(); com.google.common.io.Files.write(test, file); // 2) The CN from storage is -1, till is 2323, and since is -1, and sha is the same as before. It's going to return a split change with the same data. - splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); - Assert.assertEquals(2, splitChange.splits.size()); - Assert.assertEquals(-1, splitChange.till); - Assert.assertEquals(-1, splitChange.since); + splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); + Assert.assertEquals(2, splitChange.featureFlags.d.size()); + Assert.assertEquals(-1, splitChange.featureFlags.t); + Assert.assertEquals(-1, splitChange.featureFlags.s); test = TEST_3.getBytes(); com.google.common.io.Files.write(test, file); // 3) The CN from storage is -1, till is 2323, and since is -1, sha is different than before. It's going to return a split change with updates. - splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); - Assert.assertEquals(1, splitChange.splits.size()); - Assert.assertEquals(2323, splitChange.till); - Assert.assertEquals(-1, splitChange.since); + splitChange = localhostSplitChangeFetcher.fetch(-1L, -1L, fetchOptions); + Assert.assertEquals(1, splitChange.featureFlags.d.size()); + Assert.assertEquals(2323, splitChange.featureFlags.t); + Assert.assertEquals(-1, splitChange.featureFlags.s); + Assert.assertEquals(1, splitChange.ruleBasedSegments.d.size()); + Assert.assertEquals(1122, splitChange.ruleBasedSegments.t); + Assert.assertEquals(-1, splitChange.ruleBasedSegments.s); test = TEST_4.getBytes(); com.google.common.io.Files.write(test, file); // 4) The CN from storage is 2323, till is 445345, and since is -1, and sha is the same as before. It's going to return a split change with same data. - splitChange = localhostSplitChangeFetcher.fetch(2323, fetchOptions); - Assert.assertEquals(1, splitChange.splits.size()); - Assert.assertEquals(2323, splitChange.till); - Assert.assertEquals(2323, splitChange.since); + splitChange = localhostSplitChangeFetcher.fetch(2323, 1122, fetchOptions); + Assert.assertEquals(1, splitChange.featureFlags.d.size()); + Assert.assertEquals(2323, splitChange.featureFlags.t); + Assert.assertEquals(2323, splitChange.featureFlags.s); + Assert.assertEquals(1, splitChange.ruleBasedSegments.d.size()); + Assert.assertEquals(1122, splitChange.ruleBasedSegments.t); + Assert.assertEquals(1122, splitChange.ruleBasedSegments.s); test = TEST_5.getBytes(); com.google.common.io.Files.write(test, file); // 5) The CN from storage is 2323, till and since are -1, and sha is different than before. It's going to return a split change with updates. - splitChange = localhostSplitChangeFetcher.fetch(2323, fetchOptions); - Assert.assertEquals(2, splitChange.splits.size()); - Assert.assertEquals(2323, splitChange.till); - Assert.assertEquals(2323, splitChange.since); + splitChange = localhostSplitChangeFetcher.fetch(2323, 1122, fetchOptions); + Assert.assertEquals(2, splitChange.featureFlags.d.size()); + Assert.assertEquals(2323, splitChange.featureFlags.t); + Assert.assertEquals(2323, splitChange.featureFlags.s); + Assert.assertEquals(1, splitChange.ruleBasedSegments.d.size()); + Assert.assertEquals(1122, splitChange.ruleBasedSegments.t); + Assert.assertEquals(1122, splitChange.ruleBasedSegments.s); } @Test(expected = IllegalStateException.class) @@ -176,6 +199,25 @@ public void processTestForException() { JsonLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); + } + + @Test + public void testParseOldSpec() throws FileNotFoundException { + InputStream inputStream = new FileInputStream("src/test/resources/split_old_spec.json"); + InputStreamProvider inputStreamProvider = new StaticContentInputStreamProvider(inputStream); + JsonLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); + FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); + + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); + + List split = splitChange.featureFlags.d; + Assert.assertEquals(7, split.size()); + Assert.assertEquals(1660326991072L, splitChange.featureFlags.t); + Assert.assertEquals(-1L, splitChange.featureFlags.s); + + Assert.assertEquals(new ArrayList<>(), splitChange.ruleBasedSegments.d); + Assert.assertEquals(-1L, splitChange.ruleBasedSegments.t); + Assert.assertEquals(-1L, splitChange.ruleBasedSegments.s); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/client/JsonLocalhostSplitFactoryTest.java b/client/src/test/java/io/split/client/JsonLocalhostSplitFactoryTest.java new file mode 100644 index 000000000..1152615a2 --- /dev/null +++ b/client/src/test/java/io/split/client/JsonLocalhostSplitFactoryTest.java @@ -0,0 +1,52 @@ +package io.split.client; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.concurrent.TimeoutException; + +public class JsonLocalhostSplitFactoryTest { + + @Test + public void works() throws IOException, URISyntaxException, InterruptedException, TimeoutException { + SplitClientConfig config = SplitClientConfig.builder() + .splitFile("src/test/resources/splits_localhost.json") + .segmentDirectory("src/test/resources") + .setBlockUntilReadyTimeout(10000) + .build(); + SplitFactory splitFactory = SplitFactoryBuilder.build("localhost", config); + SplitClient client = splitFactory.client(); + client.blockUntilReady(); + + Assert.assertEquals("on", client.getTreatment("bilal@@split.io", "rbs_flag", new HashMap() {{ + put("email", "bilal@@split.io"); + }})); + Assert.assertEquals("off", client.getTreatment("mauro@split.io", "rbs_flag", new HashMap() {{ + put("email", "mauro@split.io"); + }})); + Assert.assertEquals("off", client.getTreatment("bilal", "test_split")); + Assert.assertEquals("on", client.getTreatment("bilal", "push_test")); + Assert.assertEquals("on_whitelist", client.getTreatment("admin", "push_test")); + client.destroy(); + } + + @Test + public void testOldSpec() throws IOException, URISyntaxException, InterruptedException, TimeoutException { + SplitClientConfig config = SplitClientConfig.builder() + .splitFile("src/test/resources/split_old_spec.json") + .segmentDirectory("src/test/resources") + .setBlockUntilReadyTimeout(10000) + .build(); + SplitFactory splitFactory = SplitFactoryBuilder.build("localhost", config); + SplitClient client = splitFactory.client(); + client.blockUntilReady(); + + Assert.assertEquals("on", client.getTreatment("bilal", "split_1")); + Assert.assertEquals("off", client.getTreatment("bilal", "split_2")); + Assert.assertEquals("v5", client.getTreatment("admin", "split_2")); + client.destroy(); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/client/LegacyLocalhostSplitChangeFetcherTest.java b/client/src/test/java/io/split/client/LegacyLocalhostSplitChangeFetcherTest.java index 90f958cc1..affee8010 100644 --- a/client/src/test/java/io/split/client/LegacyLocalhostSplitChangeFetcherTest.java +++ b/client/src/test/java/io/split/client/LegacyLocalhostSplitChangeFetcherTest.java @@ -32,10 +32,10 @@ public void testParseSplitChange() throws IOException { LegacyLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new LegacyLocalhostSplitChangeFetcher(folder.getRoot().getAbsolutePath()); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); - Assert.assertEquals(2, splitChange.splits.size()); - Assert.assertEquals(-1, splitChange.since); - Assert.assertEquals(-1, splitChange.till); + Assert.assertEquals(2, splitChange.featureFlags.d.size()); + Assert.assertEquals(-1, splitChange.featureFlags.s); + Assert.assertEquals(-1, splitChange.featureFlags.t); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java b/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java index 0a154f7d4..abcc551fe 100644 --- a/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java +++ b/client/src/test/java/io/split/client/LocalhostSplitFactoryYamlTest.java @@ -2,7 +2,6 @@ import io.split.client.utils.LocalhostUtils; import io.split.grammar.Treatments; -import io.split.service.SplitHttpClient; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; diff --git a/client/src/test/java/io/split/client/SplitClientImplTest.java b/client/src/test/java/io/split/client/SplitClientImplTest.java index 7d3d30642..5b56708d9 100644 --- a/client/src/test/java/io/split/client/SplitClientImplTest.java +++ b/client/src/test/java/io/split/client/SplitClientImplTest.java @@ -11,17 +11,19 @@ import io.split.client.impressions.ImpressionsManager; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; +import io.split.engine.matchers.PrerequisitesMatcher; +import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.EqualToMatcher; +import io.split.engine.matchers.GreaterThanOrEqualToMatcher; +import io.split.engine.matchers.AllKeysMatcher; +import io.split.engine.matchers.DependencyMatcher; +import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheConsumer; import io.split.storages.SplitCacheConsumer; import io.split.engine.evaluator.EvaluatorImp; import io.split.engine.SDKReadinessGates; import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedSplit; -import io.split.engine.matchers.AllKeysMatcher; -import io.split.engine.matchers.CombiningMatcher; -import io.split.engine.matchers.DependencyMatcher; -import io.split.engine.matchers.EqualToMatcher; -import io.split.engine.matchers.GreaterThanOrEqualToMatcher; import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; import io.split.engine.matchers.strings.WhitelistMatcher; import io.split.grammar.Treatments; @@ -82,11 +84,12 @@ public void nullKeyResultsInControl() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(), true); + null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -96,7 +99,7 @@ public void nullKeyResultsInControl() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); assertEquals(Treatments.CONTROL, client.getTreatment(null, "test1")); @@ -111,11 +114,12 @@ public void nullTestResultsInControl() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(), true); + null, 1, 1, new HashSet<>(), true, null); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -125,7 +129,7 @@ public void nullTestResultsInControl() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); assertEquals(Treatments.CONTROL, client.getTreatment("adil@relateiq.com", null)); @@ -138,6 +142,7 @@ public void exceptionsResultInControl() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(anyString())).thenThrow(RuntimeException.class); SplitClientImpl client = new SplitClientImpl( @@ -147,7 +152,7 @@ public void exceptionsResultInControl() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); assertEquals(Treatments.CONTROL, client.getTreatment("adil@relateiq.com", "test1")); @@ -163,11 +168,12 @@ public void works() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(), true); + null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); when(gates.isSDKReady()).thenReturn(true); @@ -178,7 +184,7 @@ public void works() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -201,11 +207,12 @@ public void worksNullConfig() { ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -215,7 +222,7 @@ public void worksNullConfig() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); String randomKey = RandomStringUtils.random(10); @@ -236,11 +243,12 @@ public void worksAndHasConfig() { Map configurations = new HashMap<>(); configurations.put(Treatments.ON, "{\"size\" : 30}"); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, configurations, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -250,7 +258,7 @@ public void worksAndHasConfig() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -272,11 +280,12 @@ public void lastConditionIsAlwaysDefault() { ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new WhitelistMatcher(Lists.newArrayList("adil@codigo.com"))), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -286,7 +295,7 @@ public void lastConditionIsAlwaysDefault() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -311,11 +320,12 @@ public void lastConditionIsAlwaysDefaultButWithTreatment() { configurations.put(Treatments.OFF, "{\"size\" : 30}"); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - "user", 1, 1, configurations, new HashSet<>(), true); + "user", 1, 1, configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -325,7 +335,7 @@ public void lastConditionIsAlwaysDefaultButWithTreatment() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -345,11 +355,12 @@ public void multipleConditionsWork() { ParsedCondition trevor_is_always_shown = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new WhitelistMatcher(Lists.newArrayList("trevor@codigo.com"))), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(adil_is_always_on, pato_is_never_shown, trevor_is_always_shown); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); when(gates.isSDKReady()).thenReturn(false); @@ -360,7 +371,7 @@ public void multipleConditionsWork() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -379,11 +390,12 @@ public void killedTestAlwaysGoesToDefault() { ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new WhitelistMatcher(Lists.newArrayList("adil@codigo.com"))), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, true, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, true, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -393,7 +405,7 @@ public void killedTestAlwaysGoesToDefault() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -418,11 +430,12 @@ public void killedTestAlwaysGoesToDefaultHasConfig() { configurations.put(Treatments.OFF, "{\"size\" : 30}"); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, true, Treatments.OFF, conditions, - "user", 1, 1, configurations, new HashSet<>(), true); + "user", 1, 1, configurations, new HashSet<>(), true, null); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -432,7 +445,7 @@ public void killedTestAlwaysGoesToDefaultHasConfig() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -450,15 +463,16 @@ public void dependencyMatcherOn() { ParsedCondition parent_is_on = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition(Treatments.ON, 100))); List parent_conditions = Lists.newArrayList(parent_is_on); - ParsedSplit parentSplit = ParsedSplit.createParsedSplitForTests(parent, 123, false, Treatments.OFF, parent_conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parentSplit = ParsedSplit.createParsedSplitForTests(parent, 123, false, Treatments.OFF, parent_conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); ParsedCondition dependent_needs_parent = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new DependencyMatcher(parent, Lists.newArrayList(Treatments.ON))), Lists.newArrayList(partition(Treatments.ON, 100))); List dependent_conditions = Lists.newArrayList(dependent_needs_parent); - ParsedSplit dependentSplit = ParsedSplit.createParsedSplitForTests(dependent, 123, false, Treatments.OFF, dependent_conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit dependentSplit = ParsedSplit.createParsedSplitForTests(dependent, 123, false, Treatments.OFF, dependent_conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(parent)).thenReturn(parentSplit); when(splitCacheConsumer.get(dependent)).thenReturn(dependentSplit); @@ -469,7 +483,7 @@ public void dependencyMatcherOn() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -484,15 +498,16 @@ public void dependencyMatcherOff() { ParsedCondition parent_is_on = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition(Treatments.ON, 100))); List parent_conditions = Lists.newArrayList(parent_is_on); - ParsedSplit parentSplit = ParsedSplit.createParsedSplitForTests(parent, 123, false, Treatments.OFF, parent_conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parentSplit = ParsedSplit.createParsedSplitForTests(parent, 123, false, Treatments.OFF, parent_conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); ParsedCondition dependent_needs_parent = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new DependencyMatcher(parent, Lists.newArrayList(Treatments.OFF))), Lists.newArrayList(partition(Treatments.ON, 100))); List dependent_conditions = Lists.newArrayList(dependent_needs_parent); - ParsedSplit dependentSplit = ParsedSplit.createParsedSplitForTests(dependent, 123, false, Treatments.OFF, dependent_conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit dependentSplit = ParsedSplit.createParsedSplitForTests(dependent, 123, false, Treatments.OFF, dependent_conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(parent)).thenReturn(parentSplit); when(splitCacheConsumer.get(dependent)).thenReturn(dependentSplit); @@ -503,7 +518,7 @@ public void dependencyMatcherOff() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -517,11 +532,12 @@ public void dependencyMatcherControl() { ParsedCondition dependent_needs_parent = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new DependencyMatcher("not-exists", Lists.newArrayList(Treatments.OFF))), Lists.newArrayList(partition(Treatments.OFF, 100))); List dependent_conditions = Lists.newArrayList(dependent_needs_parent); - ParsedSplit dependentSplit = ParsedSplit.createParsedSplitForTests(dependent, 123, false, Treatments.ON, dependent_conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit dependentSplit = ParsedSplit.createParsedSplitForTests(dependent, 123, false, Treatments.ON, dependent_conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(dependent)).thenReturn(dependentSplit); SplitClientImpl client = new SplitClientImpl( @@ -531,7 +547,7 @@ public void dependencyMatcherControl() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -546,11 +562,12 @@ public void attributesWork() { ParsedCondition users_with_age_greater_than_10_are_on = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of("age", new GreaterThanOrEqualToMatcher(10, DataType.NUMBER)), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(adil_is_always_on, users_with_age_greater_than_10_are_on); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -560,7 +577,7 @@ public void attributesWork() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -580,11 +597,12 @@ public void attributesWork2() { ParsedCondition age_equal_to_0_should_be_on = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of("age", new EqualToMatcher(0, DataType.NUMBER)), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(age_equal_to_0_should_be_on); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -594,7 +612,7 @@ public void attributesWork2() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -615,11 +633,12 @@ public void attributesGreaterThanNegativeNumber() { ParsedCondition age_equal_to_0_should_be_on = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of("age", new EqualToMatcher(-20, DataType.NUMBER)), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(age_equal_to_0_should_be_on); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -629,7 +648,7 @@ public void attributesGreaterThanNegativeNumber() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -652,11 +671,12 @@ public void attributesForSets() { ParsedCondition any_of_set = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of("products", new ContainsAnyOfSetMatcher(Lists.newArrayList("sms", "video"))), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(any_of_set); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -666,7 +686,7 @@ public void attributesForSets() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer ,segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer ,segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -695,10 +715,11 @@ public void labelsArePopulated() { ); List conditions = Lists.newArrayList(age_equal_to_0_should_be_on); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SDKReadinessGates gates = mock(SDKReadinessGates.class); @@ -710,7 +731,7 @@ public void labelsArePopulated() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -797,11 +818,12 @@ private void trafficAllocation(String key, int trafficAllocation, int trafficAll List conditions = Lists.newArrayList(whitelistCondition, rollOutToEveryone); ParsedSplit parsedSplit = new ParsedSplit(test, 123, false, Treatments.OFF, conditions, null, 1, - trafficAllocation, trafficAllocationSeed, 1, null, new HashSet<>(), true); + trafficAllocation, trafficAllocationSeed, 1, null, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); ImpressionsManager impressionsManager = mock(ImpressionsManager.class); @@ -812,7 +834,7 @@ private void trafficAllocation(String key, int trafficAllocation, int trafficAll NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -848,11 +870,12 @@ public void notInTrafficAllocationDefaultConfig() { List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = new ParsedSplit(test, 123, false, Treatments.OFF, conditions, null, - 1, trafficAllocation, trafficAllocationSeed, 1, configurations, new HashSet<>(), true); + 1, trafficAllocation, trafficAllocationSeed, 1, configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); ImpressionsManager impressionsManager = mock(ImpressionsManager.class); @@ -865,7 +888,7 @@ public void notInTrafficAllocationDefaultConfig() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -894,11 +917,12 @@ public void matchingBucketingKeysWork() { ParsedCondition aijaz_should_match = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new WhitelistMatcher(whitelist)), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(aijaz_should_match); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -908,7 +932,7 @@ public void matchingBucketingKeysWork() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -930,11 +954,12 @@ public void matchingBucketingKeysByFlagSetWork() { ParsedCondition aijaz_should_match = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new WhitelistMatcher(whitelist)), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(aijaz_should_match); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(Arrays.asList("set1")), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(Arrays.asList("set1")), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); HashMap> flagsBySets = new HashMap<>(); flagsBySets.put("set1", new HashSet<>(Arrays.asList(test))); when(splitCacheConsumer.getNamesByFlagSets(Arrays.asList("set1"))).thenReturn(flagsBySets); @@ -950,7 +975,7 @@ public void matchingBucketingKeysByFlagSetWork() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -970,11 +995,12 @@ public void matchingBucketingKeysByFlagSetsWork() { ParsedCondition aijaz_should_match = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new WhitelistMatcher(whitelist)), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(aijaz_should_match); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(Arrays.asList("set1")), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, "user", 1, 1, new HashSet<>(Arrays.asList("set1")), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); HashMap> flagsBySets = new HashMap<>(); flagsBySets.put("set1", new HashSet<>(Arrays.asList(test))); when(splitCacheConsumer.getNamesByFlagSets(Arrays.asList("set1"))).thenReturn(flagsBySets); @@ -990,7 +1016,7 @@ public void matchingBucketingKeysByFlagSetsWork() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1012,11 +1038,12 @@ public void impressionMetadataIsPropagated() { ); List conditions = Lists.newArrayList(age_equal_to_0_should_be_on); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); ImpressionsManager impressionsManager = mock(ImpressionsManager.class); @@ -1027,7 +1054,7 @@ public void impressionMetadataIsPropagated() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1060,6 +1087,7 @@ public void blockUntilReadyDoesNotTimeWhenSdkIsReady() throws TimeoutException, SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SDKReadinessGates ready = mock(SDKReadinessGates.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(ready.waitUntilInternalReady(100)).thenReturn(true); SplitClientImpl client = new SplitClientImpl( @@ -1069,7 +1097,7 @@ public void blockUntilReadyDoesNotTimeWhenSdkIsReady() throws TimeoutException, NoopEventsStorageImp.create(), config, ready, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1081,6 +1109,7 @@ public void blockUntilReadyTimesWhenSdkIsNotReady() throws TimeoutException, Int SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SDKReadinessGates ready = mock(SDKReadinessGates.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(ready.waitUntilInternalReady(100)).thenReturn(false); SplitClientImpl client = new SplitClientImpl( @@ -1090,7 +1119,7 @@ public void blockUntilReadyTimesWhenSdkIsNotReady() throws TimeoutException, Int NoopEventsStorageImp.create(), config, ready, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1102,6 +1131,7 @@ public void trackWithValidParameters() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(gates.isSDKReady()).thenReturn(false); SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), @@ -1110,7 +1140,7 @@ public void trackWithValidParameters() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1127,7 +1157,8 @@ public void trackWithInvalidEventTypeIds() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); - + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); + SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), splitCacheConsumer, @@ -1135,7 +1166,7 @@ public void trackWithInvalidEventTypeIds() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Assert.assertFalse(client.track("validKey", "valid_traffic_type", "")); @@ -1151,7 +1182,8 @@ public void trackWithInvalidTrafficTypeNames() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); - + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); + SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), splitCacheConsumer, @@ -1159,7 +1191,7 @@ public void trackWithInvalidTrafficTypeNames() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1172,7 +1204,8 @@ public void trackWithInvalidKeys() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); - + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); + SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), splitCacheConsumer, @@ -1180,7 +1213,7 @@ public void trackWithInvalidKeys() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1197,11 +1230,12 @@ public void getTreatmentWithInvalidKeys() { ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -1211,7 +1245,7 @@ public void getTreatmentWithInvalidKeys() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Assert.assertNotEquals(Treatments.CONTROL, client.getTreatment("valid", "split")); @@ -1251,6 +1285,7 @@ public void trackWithProperties() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); EventsStorageProducer eventClientMock = Mockito.mock(EventsStorageProducer.class); Mockito.when(eventClientMock.track((Event) Mockito.any(), Mockito.anyInt())).thenReturn(true); @@ -1261,7 +1296,7 @@ public void trackWithProperties() { eventClientMock, config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1354,11 +1389,12 @@ public void clientCannotPerformActionsWhenDestroyed() throws InterruptedExceptio ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitFactory mockFactory = new SplitFactory() { @@ -1384,7 +1420,7 @@ public void clientCannotPerformActionsWhenDestroyed() throws InterruptedExceptio NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1410,11 +1446,12 @@ public void worksAndHasConfigTryKetTreatmentWithKey() { configurations.put(Treatments.ON, "{\"size\" : 30}"); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, configurations, new HashSet<>(), true); + null, 1, 1, configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientImpl client = new SplitClientImpl( @@ -1424,7 +1461,7 @@ public void worksAndHasConfigTryKetTreatmentWithKey() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1454,11 +1491,12 @@ public void worksAndHasConfigByFlagSetTryKetTreatmentWithKey() { configurations.put(Treatments.ON, "{\"size\" : 30}"); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, configurations, new HashSet<>(Arrays.asList("set1")), true); + null, 1, 1, configurations, new HashSet<>(Arrays.asList("set1")), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); HashMap> flagsBySets = new HashMap<>(); flagsBySets.put("set1", new HashSet<>(Arrays.asList(test))); when(splitCacheConsumer.getNamesByFlagSets(Arrays.asList("set1"))).thenReturn(flagsBySets); @@ -1474,7 +1512,7 @@ public void worksAndHasConfigByFlagSetTryKetTreatmentWithKey() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1502,11 +1540,12 @@ public void worksAndHasConfigByFlagSetsTryKetTreatmentWithKey() { configurations.put(Treatments.ON, "{\"size\" : 30}"); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, configurations, new HashSet<>(Arrays.asList("set1")), true); + null, 1, 1, configurations, new HashSet<>(Arrays.asList("set1")), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); HashMap> flagsBySets = new HashMap<>(); flagsBySets.put("set1", new HashSet<>(Arrays.asList(test))); when(splitCacheConsumer.getNamesByFlagSets(Arrays.asList("set1"))).thenReturn(flagsBySets); @@ -1522,7 +1561,7 @@ public void worksAndHasConfigByFlagSetsTryKetTreatmentWithKey() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1544,11 +1583,12 @@ public void blockUntilReadyException() throws TimeoutException, InterruptedExcep Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(), true); + null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); SplitClientConfig config = SplitClientConfig.builder().setBlockUntilReadyTimeout(-100).build(); @@ -1559,7 +1599,7 @@ public void blockUntilReadyException() throws TimeoutException, InterruptedExcep NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1573,12 +1613,13 @@ public void nullKeyResultsInControlGetTreatments() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(), true); + null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); Map splits = new HashMap<>(); splits.put(test, parsedSplit); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(Collections.singletonList(test))).thenReturn(splits); SplitClientImpl client = new SplitClientImpl( @@ -1588,7 +1629,7 @@ public void nullKeyResultsInControlGetTreatments() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); assertEquals(Treatments.CONTROL, client.getTreatments(null, Collections.singletonList("test1")).get("test1")); @@ -1603,12 +1644,13 @@ public void nullSplitsResultsInEmptyGetTreatments() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(), true); + null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); Map splits = new HashMap<>(); splits.put(test, parsedSplit); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(Collections.singletonList(test))).thenReturn(splits); SplitClientImpl client = new SplitClientImpl( @@ -1618,7 +1660,7 @@ public void nullSplitsResultsInEmptyGetTreatments() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); assertEquals(0, client.getTreatments("key", null).size()); @@ -1631,6 +1673,7 @@ public void exceptionsResultInControlGetTreatments() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(anyList())).thenThrow(RuntimeException.class); SplitClientImpl client = new SplitClientImpl( @@ -1640,7 +1683,7 @@ public void exceptionsResultInControlGetTreatments() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Map result = client.getTreatments("adil@relateiq.com", Arrays.asList("test1", "test2")); @@ -1657,12 +1700,13 @@ public void getTreatmentsWorks() { ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); Map splits = new HashMap<>(); splits.put(test, parsedSplit); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(anyList())).thenReturn(splits); when(gates.isSDKReady()).thenReturn(true); @@ -1673,7 +1717,7 @@ public void getTreatmentsWorks() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Map result = client.getTreatments("randomKey", Arrays.asList(test, "test2")); @@ -1688,12 +1732,13 @@ public void emptySplitsResultsInNullGetTreatments() { String test = "test1"; ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); Map splits = new HashMap<>(); splits.put(test, parsedSplit); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(Collections.singletonList(test))).thenReturn(splits); SplitClientImpl client = new SplitClientImpl( @@ -1703,7 +1748,7 @@ public void emptySplitsResultsInNullGetTreatments() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Map result = client.getTreatments("key", new ArrayList<>()); @@ -1718,6 +1763,7 @@ public void exceptionsResultInControlTreatments() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(anyString())).thenThrow(RuntimeException.class); SplitClientImpl client = new SplitClientImpl( @@ -1727,7 +1773,7 @@ public void exceptionsResultInControlTreatments() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Map result = client.getTreatments("adil@relateiq.com", Arrays.asList("test1")); @@ -1744,9 +1790,9 @@ public void worksTreatments() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(), true); + null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); ParsedSplit parsedSplit2 = ParsedSplit.createParsedSplitForTests(test2, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(), true); + null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); Map parsedSplits = new HashMap<>(); parsedSplits.put(test, parsedSplit); parsedSplits.put(test2, parsedSplit2); @@ -1754,6 +1800,7 @@ public void worksTreatments() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(anyList())).thenReturn(parsedSplits); when(gates.isSDKReady()).thenReturn(true); @@ -1764,7 +1811,7 @@ public void worksTreatments() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Map result = client.getTreatments("anyKey", Arrays.asList(test, test2)); @@ -1784,13 +1831,14 @@ public void worksOneControlTreatments() { ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(), true, new PrerequisitesMatcher(null)); Map parsedSplits = new HashMap<>(); parsedSplits.put(test, parsedSplit); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(anyList())).thenReturn(parsedSplits); when(gates.isSDKReady()).thenReturn(true); @@ -1801,7 +1849,7 @@ public void worksOneControlTreatments() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1830,12 +1878,13 @@ public void treatmentsWorksAndHasConfig() { ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, configurations, new HashSet<>(), true); + null, 1, 1, configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); Map parsedSplits = new HashMap<>(); parsedSplits.put(test, parsedSplit); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(anyList())).thenReturn(parsedSplits); SplitClientImpl client = new SplitClientImpl( @@ -1845,7 +1894,7 @@ public void treatmentsWorksAndHasConfig() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Map attributes = new HashMap<>(); @@ -1866,11 +1915,12 @@ public void testTreatmentsByFlagSet() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(Arrays.asList("set1", "set2")), true); + null, 1, 1, new HashSet<>(Arrays.asList("set1", "set2")), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); Map fetchManyResult = new HashMap<>(); fetchManyResult.put(test, parsedSplit); when(splitCacheConsumer.fetchMany(new ArrayList<>(Arrays.asList(test)))).thenReturn(fetchManyResult); @@ -1887,7 +1937,7 @@ public void testTreatmentsByFlagSet() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); @@ -1912,11 +1962,12 @@ public void testTreatmentsByFlagSetInvalid() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(Arrays.asList("set1", "set2")), true); + null, 1, 1, new HashSet<>(Arrays.asList("set1", "set2")), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); List sets = new ArrayList<>(); when(gates.isSDKReady()).thenReturn(true); @@ -1927,7 +1978,7 @@ public void testTreatmentsByFlagSetInvalid() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); assertTrue(client.getTreatmentsByFlagSet(RandomStringUtils.random(10), "", new HashMap<>()).isEmpty()); @@ -1942,13 +1993,14 @@ public void testTreatmentsByFlagSets() { Lists.newArrayList(partition("on", 100))); List conditions = Lists.newArrayList(rollOutToEveryone); ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(Arrays.asList("set1", "set2")), true); + null, 1, 1, new HashSet<>(Arrays.asList("set1", "set2")), true, new PrerequisitesMatcher(null)); ParsedSplit parsedSplit2 = ParsedSplit.createParsedSplitForTests(test2, 123, false, Treatments.OFF, conditions, - null, 1, 1, new HashSet<>(Arrays.asList("set3", "set4")), true); + null, 1, 1, new HashSet<>(Arrays.asList("set3", "set4")), true, new PrerequisitesMatcher(null)); SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); Map fetchManyResult = new HashMap<>(); fetchManyResult.put(test, parsedSplit); @@ -1970,7 +2022,7 @@ public void testTreatmentsByFlagSets() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); int numKeys = 5; @@ -2003,12 +2055,13 @@ public void treatmentsWorksAndHasConfigFlagSet() { ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, configurations, new HashSet<>(Arrays.asList("set1")), true); + null, 1, 1, configurations, new HashSet<>(Arrays.asList("set1")), true, new PrerequisitesMatcher(null)); Map parsedSplits = new HashMap<>(); parsedSplits.put(test, parsedSplit); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(anyList())).thenReturn(parsedSplits); List sets = new ArrayList<>(Arrays.asList("set1")); @@ -2025,7 +2078,7 @@ public void treatmentsWorksAndHasConfigFlagSet() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Map attributes = new HashMap<>(); @@ -2059,12 +2112,13 @@ public void treatmentsWorksAndHasConfigFlagSets() { ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, - null, 1, 1, configurations, new HashSet<>(Arrays.asList("set1")), true); + null, 1, 1, configurations, new HashSet<>(Arrays.asList("set1")), true, new PrerequisitesMatcher(null)); Map parsedSplits = new HashMap<>(); parsedSplits.put(test, parsedSplit); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.fetchMany(anyList())).thenReturn(parsedSplits); List sets = new ArrayList<>(Arrays.asList("set1")); @@ -2081,7 +2135,7 @@ public void treatmentsWorksAndHasConfigFlagSets() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, flagSetsFilter ); Map attributes = new HashMap<>(); @@ -2105,12 +2159,13 @@ public void impressionPropertiesTest() { ); List conditions = Lists.newArrayList(age_equal_to_0_should_be_on); - ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(Arrays.asList("set")), true); + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, new HashSet<>(Arrays.asList("set")), true, new PrerequisitesMatcher(null)); Map parsedSplits = new HashMap<>(); parsedSplits.put(test, parsedSplit); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = mock(RuleBasedSegmentCacheConsumer.class); when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); when(splitCacheConsumer.fetchMany(Arrays.asList(test))).thenReturn(parsedSplits); Map> splits = new HashMap<>(); @@ -2126,7 +2181,7 @@ public void impressionPropertiesTest() { NoopEventsStorageImp.create(), config, gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer, ruleBasedSegmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE, new FlagSetsFilterImpl(new HashSet<>()) ); Map attributes = ImmutableMap.of("age", -20, "acv", "1000000"); diff --git a/client/src/test/java/io/split/client/SplitClientIntegrationTest.java b/client/src/test/java/io/split/client/SplitClientIntegrationTest.java index 2eb4c36b7..bba824527 100644 --- a/client/src/test/java/io/split/client/SplitClientIntegrationTest.java +++ b/client/src/test/java/io/split/client/SplitClientIntegrationTest.java @@ -25,8 +25,6 @@ import javax.ws.rs.sse.OutboundSseEvent; import java.io.IOException; import java.net.URISyntaxException; - -import java.nio.Buffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; @@ -46,9 +44,9 @@ public class SplitClientIntegrationTest { @Test public void getTreatmentWithStreamingEnabled() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); - MockResponse response2 = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850110, \"till\":1585948850110}"); - MockResponse response3 = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850111, \"till\":1585948850111}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); + MockResponse response2 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850110, \"t\":1585948850110}, \"rbs\":{\"d\":[],\"s\":1585948850110,\"t\":1585948850110}}"); + MockResponse response3 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850111, \"t\":1585948850111}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); Queue responses = new LinkedList<>(); responses.add(response); Queue responses2 = new LinkedList<>(); @@ -146,7 +144,7 @@ public void getTreatmentWithStreamingEnabled() throws Exception { @Test public void getTreatmentWithStreamingEnabledAndAuthDisabled() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"s\":1585948850109,\"t\":1585948850109,\"d\":[]}}"); Queue responses = new LinkedList<>(); responses.add(response); SplitMockServer splitServer = new SplitMockServer(CustomDispatcher.builder() @@ -167,14 +165,19 @@ public void getTreatmentWithStreamingEnabledAndAuthDisabled() throws Exception { String result = client.getTreatment("admin", "push_test"); Assert.assertEquals("on_whitelist", result); - + Assert.assertEquals("on", client.getTreatment("bilal@@split.io", "rbs_flag", new HashMap() {{ + put("email", "bilal@@split.io"); + }})); + Assert.assertEquals("off", client.getTreatment("mauro@split.io", "rbs_flag", new HashMap() {{ + put("email", "mauro@split.io"); + }})); client.destroy(); splitServer.stop(); } @Test public void getTreatmentWithStreamingDisabled() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); Queue responses = new LinkedList<>(); responses.add(response); SplitMockServer splitServer = new SplitMockServer(CustomDispatcher.builder() @@ -207,7 +210,7 @@ public void getTreatmentWithStreamingDisabled() throws Exception { @Test public void managerSplitsWithStreamingEnabled() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); Queue responses = new LinkedList<>(); responses.add(response); SplitMockServer splitServer = new SplitMockServer(CustomDispatcher.builder() @@ -226,8 +229,8 @@ public void managerSplitsWithStreamingEnabled() throws Exception { manager.blockUntilReady(); List results = manager.splits(); - Assert.assertEquals(4, results.size()); - Assert.assertEquals(3, results.stream().filter(r -> !r.killed).toArray().length); + Assert.assertEquals(5, results.size()); + Assert.assertEquals(4, results.stream().filter(r -> !r.killed).toArray().length); // SPLIT_KILL should fetch. OutboundSseEvent sseEventSplitKill = new OutboundEvent @@ -239,7 +242,7 @@ public void managerSplitsWithStreamingEnabled() throws Exception { Awaitility.await() .atMost(2L, TimeUnit.MINUTES) - .until(() -> 2 == manager.splits().stream().filter(r -> !r.killed).toArray().length); + .until(() -> 3 == manager.splits().stream().filter(r -> !r.killed).toArray().length); splitServer.stop(); sseServer.stop(); @@ -247,9 +250,9 @@ public void managerSplitsWithStreamingEnabled() throws Exception { @Test public void splitClientOccupancyNotifications() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); - MockResponse response2 = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850110, \"till\":1585948850110}"); - MockResponse response3 = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850111, \"till\":1585948850111}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); + MockResponse response2 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850110, \"t\":1585948850110}, \"rbs\":{\"d\":[],\"s\":1585948850110,\"t\":1585948850110}}"); + MockResponse response3 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850111, \"t\":1585948850111}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); Queue responses = new LinkedList<>(); responses.add(response); Queue responses2 = new LinkedList<>(); @@ -322,9 +325,9 @@ public void splitClientOccupancyNotifications() throws Exception { @Test public void splitClientControlNotifications() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); - MockResponse response2 = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850110, \"till\":1585948850110}"); - MockResponse response3 = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850111, \"till\":1585948850111}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); + MockResponse response2 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850110, \"t\":1585948850110}, \"rbs\":{\"d\":[],\"s\":1585948850110,\"t\":1585948850110}}"); + MockResponse response3 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850111, \"t\":1585948850111}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); Queue responses = new LinkedList<>(); responses.add(response); Queue responses2 = new LinkedList<>(); @@ -417,7 +420,7 @@ public void splitClientControlNotifications() throws Exception { @Test public void splitClientMultiFactory() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); Queue responses = new LinkedList<>(); responses.add(response); responses.add(response); @@ -525,7 +528,7 @@ public void splitClientMultiFactory() throws Exception { .until(() -> "on_whitelist".equals(client2.getTreatment("admin", "push_test"))); Awaitility.await() - .atMost(50L, TimeUnit.SECONDS) + .atMost(100L, TimeUnit.SECONDS) .until(() -> "on_whitelist".equals(client3.getTreatment("admin", "push_test"))); Awaitility.await() @@ -566,7 +569,7 @@ public void splitClientMultiFactory() throws Exception { @Test public void keepAlive() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); Queue responses = new LinkedList<>(); responses.add(response); @@ -604,7 +607,7 @@ public void keepAlive() throws Exception { @Test public void testConnectionClosedByRemoteHostIsProperlyHandled() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); Queue responses = new LinkedList<>(); responses.add(response); SplitMockServer splitServer = new SplitMockServer(CustomDispatcher.builder() @@ -642,7 +645,7 @@ public void testConnectionClosedByRemoteHostIsProperlyHandled() throws Exception @Test public void testConnectionClosedIsProperlyHandled() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850109}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109}, \"rbs\":{\"d\":[],\"s\":1585948850109,\"t\":1585948850109}}"); Queue responses = new LinkedList<>(); responses.add(response); SplitMockServer splitServer = new SplitMockServer(CustomDispatcher.builder() @@ -708,15 +711,21 @@ public void testPluggableMode() throws IOException, URISyntaxException { Assert.assertTrue(events.stream().anyMatch(e -> "keyValue".equals(e.getEventDto().key) && e.getEventDto().value == 12L)); Assert.assertTrue(events.stream().anyMatch(e -> "keyProperties".equals(e.getEventDto().key) && e.getEventDto().properties != null)); - Assert.assertEquals(2, splits.size()); + Assert.assertEquals(3, splits.size()); Assert.assertTrue(splits.stream().anyMatch(sw -> "first.name".equals(sw.name))); Assert.assertTrue(splits.stream().anyMatch(sw -> "second.name".equals(sw.name))); Assert.assertEquals("on", client.getTreatment("key", "first.name")); Assert.assertEquals("off", client.getTreatmentWithConfig("FakeKey", "second.name").treatment()); Assert.assertEquals("control", client.getTreatment("FakeKey", "noSplit")); + Assert.assertEquals("on", client.getTreatment("bilal@@split.io", "rbs_flag", new HashMap() {{ + put("email", "bilal@@split.io"); + }})); + Assert.assertEquals("off", client.getTreatment("mauro@split.io", "rbs_flag", new HashMap() {{ + put("email", "mauro@split.io"); + }})); List impressions = customStorageWrapper.getImps(); - Assert.assertEquals(2, impressions.size()); + Assert.assertEquals(4, impressions.size()); Assert.assertTrue(impressions.stream().anyMatch(imp -> "first.name".equals(imp.getKeyImpression().feature) && "on".equals(imp.getKeyImpression().treatment))); Assert.assertTrue(impressions.stream().anyMatch(imp -> "second.name".equals(imp.getKeyImpression().feature) && "off".equals(imp.getKeyImpression().treatment))); @@ -729,7 +738,7 @@ public void testPluggableMode() throws IOException, URISyntaxException { String key3 = keys.stream().filter(key -> key.contains("getTreatmentWithConfig/")).collect(Collectors.toList()).get(0); Assert.assertEquals(Optional.of(3L), Optional.ofNullable(latencies.get(key1))); - Assert.assertEquals(Optional.of(1L), Optional.of(latencies.get(key2))); + Assert.assertEquals(Optional.of(3L), Optional.of(latencies.get(key2))); Assert.assertEquals(Optional.of(1L), Optional.of(latencies.get(key3))); Thread.sleep(500); @@ -743,8 +752,8 @@ public void testPluggableMode() throws IOException, URISyntaxException { @Test public void getTreatmentFlagSetWithPolling() throws Exception { - MockResponse response = new MockResponse().setBody("{\"splits\":[{\"trafficTypeName\":\"client\",\"name\":\"workm\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set1\",\"set2\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"client\",\"name\":\"workm_set_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set3\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":1602796638344}"); - MockResponse responseFlag = new MockResponse().setBody("{\"splits\": [], \"since\":1602796638344, \"till\":1602796638344}"); + MockResponse response = new MockResponse().setBody("{\"ff\":{\"d\":[{\"trafficTypeName\":\"client\",\"name\":\"workm\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set1\",\"set2\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"client\",\"name\":\"workm_set_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set3\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":1602796638344},\"rbs\":{\"d\":[],\"t\":-1,\"s\":-1}}"); + MockResponse responseFlag = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1602796638344, \"t\":1602796638344},\"rbs\":{\"d\":[],\"t\":-1,\"s\":-1}}"); MockResponse segmentResponse = new MockResponse().setBody("{\"name\":\"new_segment\",\"added\":[\"user-1\"],\"removed\":[\"user-2\",\"user-3\"],\"since\":-1,\"till\":-1}"); Queue responses = new LinkedList<>(); responses.add(response); @@ -784,16 +793,15 @@ public void getTreatmentFlagSetWithPolling() throws Exception { public void ImpressionToggleOptimizedModeTest() throws Exception { String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_imp_toggle.json")), StandardCharsets.UTF_8); List allRequests = new ArrayList<>(); - Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { allRequests.add(request); switch (request.getPath()) { - case "/api/splitChanges?s=1.1&since=-1": + case "/api/splitChanges?s=1.3&since=-1&rbSince=-1": return new MockResponse().setResponseCode(200).setBody(splits); - case "/api/splitChanges?s=1.1&since=1602796638344": - return new MockResponse().setResponseCode(200).setBody("{\"splits\": [], \"since\":1602796638344, \"till\":1602796638344}"); + case "/api/splitChanges?s=1.3&since=1602796638344&rbSince=-1": + return new MockResponse().setResponseCode(200).setBody("{\"ff\":{\"d\":[], \"s\":1602796638344, \"t\":1602796638344}, \"rbs\":{\"d\":[],\"s\":-1,\"t\":-1}}"); case "/api/testImpressions/bulk": return new MockResponse().setResponseCode(200); case "/api/testImpressions/count": @@ -858,16 +866,15 @@ public MockResponse dispatch(RecordedRequest request) { public void ImpressionToggleDebugModeTest() throws Exception { String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_imp_toggle.json")), StandardCharsets.UTF_8); List allRequests = new ArrayList<>(); - Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { allRequests.add(request); switch (request.getPath()) { - case "/api/splitChanges?s=1.1&since=-1": + case "/api/splitChanges?s=1.3&since=-1&rbSince=-1": return new MockResponse().setResponseCode(200).setBody(splits); - case "/api/splitChanges?s=1.1&since=1602796638344": - return new MockResponse().setResponseCode(200).setBody("{\"splits\": [], \"since\":1602796638344, \"till\":1602796638344}"); + case "/api/splitChanges?s=1.3&since=1602796638344&rbSince=-1": + return new MockResponse().setResponseCode(200).setBody("{\"ff\":{\"d\":[], \"s\":1602796638344, \"t\":1602796638344}, \"rbs\":{\"d\":[],\"s\":-1,\"t\":-1}}"); case "/api/testImpressions/bulk": return new MockResponse().setResponseCode(200); case "/api/testImpressions/count": @@ -940,16 +947,15 @@ public MockResponse dispatch(RecordedRequest request) { public void ImpressionToggleNoneModeTest() throws Exception { String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_imp_toggle.json")), StandardCharsets.UTF_8); List allRequests = new ArrayList<>(); - Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { allRequests.add(request); switch (request.getPath()) { - case "/api/splitChanges?s=1.1&since=-1": + case "/api/splitChanges?s=1.3&since=-1&rbSince=-1": return new MockResponse().setResponseCode(200).setBody(splits); - case "/api/splitChanges?s=1.1&since=1602796638344": - return new MockResponse().setResponseCode(200).setBody("{\"splits\": [], \"since\":1602796638344, \"till\":1602796638344}"); + case "/api/splitChanges?s=1.3&since=1602796638344&rbSince=-1": + return new MockResponse().setResponseCode(200).setBody("{\"ff\":{\"d\":[], \"s\":1602796638344, \"t\":1602796638344}, \"rbs\":{\"d\":[],\"s\":-1,\"t\":-1}}"); case "/api/testImpressions/bulk": return new MockResponse().setResponseCode(200); case "/api/testImpressions/count": @@ -1018,16 +1024,15 @@ public MockResponse dispatch(RecordedRequest request) { public void ImpressionPropertiesTest() throws Exception { String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_imp_toggle.json")), StandardCharsets.UTF_8); List allRequests = new ArrayList<>(); - Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { allRequests.add(request); switch (request.getPath()) { - case "/api/splitChanges?s=1.1&since=-1": + case "/api/splitChanges?s=1.3&since=-1&rbSince=-1": return new MockResponse().setResponseCode(200).setBody(splits); - case "/api/splitChanges?s=1.1&since=1602796638344": - return new MockResponse().setResponseCode(200).setBody("{\"splits\": [], \"since\":1602796638344, \"till\":1602796638344}"); + case "/api/splitChanges?s=1.3&since=1602796638344&rbSince=-1": + return new MockResponse().setResponseCode(200).setBody("{\"ff\":{\"d\":[], \"s\":1602796638344, \"t\":1602796638344}, \"rbs\":{\"d\":[],\"s\":-1,\"t\":-1}}"); case "/api/testImpressions/bulk": return new MockResponse().setResponseCode(200); case "/api/testImpressions/count": @@ -1096,6 +1101,80 @@ public MockResponse dispatch(RecordedRequest request) { Assert.assertTrue(check2); } + @Test + public void getTreatmentWithPrerequisites() throws Exception { + String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_prereq.json")), StandardCharsets.UTF_8); + List allRequests = new ArrayList<>(); + Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + allRequests.add(request); + switch (request.getPath()) { + case "/api/splitChanges?s=1.3&since=-1&rbSince=-1": + return new MockResponse().setResponseCode(200).setBody(splits); + case "/api/splitChanges?s=1.3&since=1585948850109&rbSince=1585948850109": + return new MockResponse().setResponseCode(200).setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850109},\"rbs\":{\"d\":[],\"t\":1585948850109,\"s\":1585948850109}}"); + case "/api/segmentChanges/segment-test?since=-1": + return new MockResponse().setResponseCode(200).setBody("{\"name\":\"segment-test\",\"added\":[\"user-1\"],\"removed\":[],\"since\":-1,\"till\":-1}"); + case "/api/testImpressions/bulk": + return new MockResponse().setResponseCode(200); + case "/api/testImpressions/count": + return new MockResponse().setResponseCode(200); + case "/v1/keys/ss": + return new MockResponse().setResponseCode(200); + case "/v1/metrics/usage": + return new MockResponse().setResponseCode(200); + case "/v1/metrics/config": + return new MockResponse().setResponseCode(200); + } + return new MockResponse().setResponseCode(404); + } + }; + + MockWebServer splitServer = new MockWebServer(); + splitServer.setDispatcher(dispatcher); + splitServer.start(); + String serverURL = String.format("http://%s:%s", splitServer.getHostName(), splitServer.getPort()); + + SplitClientConfig config = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .endpoint(serverURL, serverURL) + .telemetryURL(serverURL + "/v1") + .authServiceURL(String.format("%s/api/auth/enabled", serverURL)) + .streamingEnabled(false) + .featuresRefreshRate(5) + .impressionsMode(ImpressionsManager.Mode.DEBUG) + .build(); + + SplitFactory factory = SplitFactoryBuilder.build("fake-api-token", config); + SplitClient client = factory.client(); + client.blockUntilReady(); + + Assert.assertEquals("on", client.getTreatment("bilal@split.io", "test_prereq", new HashMap() {{ + put("email", "bilal@@split.io"); + }})); + Assert.assertEquals("def_treatment", client.getTreatment("bilal@split.io", "test_prereq")); + Assert.assertEquals("def_treatment", client.getTreatment("mauro@split.io", "test_prereq", new HashMap() {{ + put("email", "mauro@@split.io"); + }})); + Assert.assertEquals("on", client.getTreatment("pato@split.io", "test_prereq", new HashMap() {{ + put("email", "pato@@split.io"); + }})); + + Assert.assertEquals("on_whitelist", client.getTreatment("bilal@split.io", "prereq_chain", new HashMap() {{ + put("email", "bilal@@split.io"); + }})); + Assert.assertEquals("on", client.getTreatment("pato@split.io", "prereq_chain", new HashMap() {{ + put("email", "pato@@split.io"); + }})); + Assert.assertEquals("on_default", client.getTreatment("mauro@split.io", "prereq_chain", new HashMap() {{ + put("email", "mauro@@split.io"); + }})); + + client.destroy(); + splitServer.shutdown(); + } + private SSEMockServer buildSSEMockServer(SSEMockServer.SseEventQueue eventQueue) { return new SSEMockServer(eventQueue, (token, version, channel) -> { if (!"1.1".equals(version)) { diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java index a65adc266..a6da10692 100644 --- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java +++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java @@ -8,6 +8,7 @@ import io.split.telemetry.storage.TelemetryStorage; import io.split.telemetry.synchronizer.TelemetrySynchronizer; import junit.framework.TestCase; +import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -23,6 +24,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URISyntaxException; +import java.util.concurrent.TimeUnit; public class SplitFactoryImplTest extends TestCase { @@ -197,7 +199,10 @@ public void testFactoryConsumerInstantiationRetryReadiness() throws Exception { splitFactoryImpl.set(splitFactory, userStorageWrapper); assertNotNull(splitFactory.client()); assertNotNull(splitFactory.manager()); - Thread.sleep(2000); + Awaitility.await() + .atMost(5L, TimeUnit.SECONDS) + .untilAsserted(() -> Assert.assertTrue(userStorageWrapper.connect())); + Mockito.verify(userStorageWrapper, Mockito.times(2)).connect(); } diff --git a/client/src/test/java/io/split/client/SplitManagerImplTest.java b/client/src/test/java/io/split/client/SplitManagerImplTest.java index 66b99c2c6..f3c04454f 100644 --- a/client/src/test/java/io/split/client/SplitManagerImplTest.java +++ b/client/src/test/java/io/split/client/SplitManagerImplTest.java @@ -1,7 +1,9 @@ package io.split.client; import com.google.common.collect.Lists; + import io.split.client.api.SplitView; +import io.split.client.dtos.Prerequisites; import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; import io.split.client.utils.Json; @@ -12,6 +14,7 @@ import io.split.engine.experiments.SplitParser; import io.split.engine.matchers.AllKeysMatcher; import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.PrerequisitesMatcher; import io.split.grammar.Treatments; import io.split.storages.SplitCacheConsumer; import io.split.telemetry.storage.InMemoryTelemetryStorage; @@ -65,7 +68,11 @@ public void splitCallWithExistentSplit() { String existent = "existent"; SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); - ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, new HashSet<>(), false); + Prerequisites prereq = new Prerequisites(); + prereq.featureFlagName = "feature1"; + prereq.treatments = Lists.newArrayList("on"); + ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, new HashSet<>(), false, + new PrerequisitesMatcher(Lists.newArrayList(prereq))); when(splitCacheConsumer.get(existent)).thenReturn(response); SplitManagerImpl splitManager = new SplitManagerImpl(splitCacheConsumer, @@ -80,6 +87,7 @@ public void splitCallWithExistentSplit() { Assert.assertEquals("off", theOne.treatments.get(0)); Assert.assertEquals(0, theOne.configs.size()); Assert.assertEquals("off", theOne.defaultTreatment); + Assert.assertEquals(Lists.newArrayList(prereq), theOne.prerequisites); } @Test @@ -91,7 +99,7 @@ public void splitCallWithExistentSplitAndConfigs() { Map configurations = new HashMap<>(); configurations.put(Treatments.OFF, "{\"size\" : 30}"); - ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, configurations, new HashSet<>(), false); + ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, configurations, new HashSet<>(), false, null); when(splitCacheConsumer.get(existent)).thenReturn(response); SplitManagerImpl splitManager = new SplitManagerImpl(splitCacheConsumer, @@ -127,7 +135,7 @@ public void splitsCallWithSplit() { List parsedSplits = Lists.newArrayList(); SDKReadinessGates gates = mock(SDKReadinessGates.class); when(gates.isSDKReady()).thenReturn(false); - ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, new HashSet<>(), false); + ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, new HashSet<>(), false, null); parsedSplits.add(response); when(splitCacheConsumer.getAll()).thenReturn(parsedSplits); @@ -202,7 +210,7 @@ public void splitCallWithExistentSets() { String existent = "existent"; SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", - Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, new HashSet<>(Arrays.asList("set1", "set2", "set3")), false); + Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, new HashSet<>(Arrays.asList("set1", "set2", "set3")), false, null); when(splitCacheConsumer.get(existent)).thenReturn(response); SplitManagerImpl splitManager = new SplitManagerImpl(splitCacheConsumer, @@ -217,7 +225,7 @@ public void splitCallWithEmptySets() { String existent = "existent"; SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", - Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, null, false); + Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1, null, false, null); when(splitCacheConsumer.get(existent)).thenReturn(response); SplitManagerImpl splitManager = new SplitManagerImpl(splitCacheConsumer, @@ -237,7 +245,7 @@ public void ImpressionToggleParseTest() throws IOException { String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_imp_toggle.json")), StandardCharsets.UTF_8); SplitChange change = Json.fromJson(splits, SplitChange.class); SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); - for (Split split : change.splits) { + for (Split split : change.featureFlags.d) { ParsedSplit parsedSplit = parser.parse(split); when(splitCacheConsumer.get(split.name)).thenReturn(parsedSplit); } diff --git a/client/src/test/java/io/split/client/YamlLocalhostSplitChangeFetcherTest.java b/client/src/test/java/io/split/client/YamlLocalhostSplitChangeFetcherTest.java index dabd96781..f37367ae4 100644 --- a/client/src/test/java/io/split/client/YamlLocalhostSplitChangeFetcherTest.java +++ b/client/src/test/java/io/split/client/YamlLocalhostSplitChangeFetcherTest.java @@ -63,14 +63,14 @@ public void testParseSplitChange() throws IOException { InputStreamProvider inputStreamProvider = new FileInputStreamProvider(file.getAbsolutePath()); YamlLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new YamlLocalhostSplitChangeFetcher(inputStreamProvider); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); - Assert.assertEquals(2, splitChange.splits.size()); - Assert.assertEquals(-1, splitChange.since); - Assert.assertEquals(-1, splitChange.till); + Assert.assertEquals(2, splitChange.featureFlags.d.size()); + Assert.assertEquals(-1, splitChange.featureFlags.s); + Assert.assertEquals(-1, splitChange.featureFlags.t); - for (Split split: splitChange.splits) { + for (Split split: splitChange.featureFlags.d) { Assert.assertEquals("control", split.defaultTreatment); } } @@ -81,6 +81,6 @@ public void processTestForException() { YamlLocalhostSplitChangeFetcher localhostSplitChangeFetcher = new YamlLocalhostSplitChangeFetcher(inputStreamProvider); FetchOptions fetchOptions = Mockito.mock(FetchOptions.class); - SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, fetchOptions); + SplitChange splitChange = localhostSplitChangeFetcher.fetch(-1L, -1, fetchOptions); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/client/utils/CustomDispatcher.java b/client/src/test/java/io/split/client/utils/CustomDispatcher.java index 0b680156b..ea2a22355 100644 --- a/client/src/test/java/io/split/client/utils/CustomDispatcher.java +++ b/client/src/test/java/io/split/client/utils/CustomDispatcher.java @@ -9,16 +9,16 @@ import java.util.*; public class CustomDispatcher extends Dispatcher { - public static final String INITIAL_SPLIT_CHANGES = "/api/splitChanges?s=1.1&since=-1"; - public static final String INITIAL_FLAGS_BY_SETS = "/api/splitChanges?s=1.1&since=-1&sets=set1%2Cset2"; - public static final String SINCE_1602796638344 = "/api/splitChanges?s=1.1&since=1602796638344&sets=set1%2Cset2"; - public static final String AUTH_ENABLED = "/api/auth/enabled?s=1.1"; - public static final String AUTH_DISABLED = "/api/auth/disabled?s=1.1"; - public static final String SINCE_1585948850109 = "/api/splitChanges?s=1.1&since=1585948850109"; - public static final String SINCE_1585948850109_FLAG_SET = "/api/splitChanges?s=1.1&since=-1&sets=set_1%2Cset_2"; - public static final String SINCE_1585948850110 = "/api/splitChanges?s=1.1&since=1585948850110"; - public static final String SINCE_1585948850111 = "/api/splitChanges?s=1.1&since=1585948850111"; - public static final String SINCE_1585948850112 = "/api/splitChanges?s=1.1&since=1585948850112"; + public static final String INITIAL_SPLIT_CHANGES = "/api/splitChanges?s=1.3&since=-1&rbSince=-1"; + public static final String INITIAL_FLAGS_BY_SETS = "/api/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set1%2Cset2"; + public static final String SINCE_1602796638344 = "/api/splitChanges?s=1.3&since=1602796638344&rbSince=-1&sets=set1%2Cset2"; + public static final String AUTH_ENABLED = "/api/auth/enabled?s=1.3"; + public static final String AUTH_DISABLED = "/api/auth/disabled?s=1.3"; + public static final String SINCE_1585948850109 = "/api/splitChanges?s=1.3&since=1585948850109&rbSince=1585948850109"; + public static final String SINCE_1585948850109_FLAG_SET = "/api/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_1%2Cset_2"; + public static final String SINCE_1585948850110 = "/api/splitChanges?s=1.3&since=1585948850110&rbSince=1585948850110"; + public static final String SINCE_1585948850111 = "/api/splitChanges?s=1.3&since=1585948850111&rbSince=1585948850111"; + public static final String SINCE_1585948850112 = "/api/splitChanges?s=1.3&since=1585948850112&rbSince=1585948850112"; public static final String SEGMENT_TEST_INITIAL = "/api/segmentChanges/segment-test?since=-1"; public static final String SEGMENT3_INITIAL = "/api/segmentChanges/segment3?since=-1"; public static final String SEGMENT3_SINCE_1585948850110 = "/api/segmentChanges/segment3?since=1585948850110"; @@ -44,23 +44,23 @@ public MockResponse dispatch(@NotNull RecordedRequest request) { case CustomDispatcher.INITIAL_SPLIT_CHANGES: return getResponse(CustomDispatcher.INITIAL_SPLIT_CHANGES, new MockResponse().setBody(inputStreamToString("splits.json"))); case CustomDispatcher.INITIAL_FLAGS_BY_SETS: - return getResponse(CustomDispatcher.INITIAL_FLAGS_BY_SETS, new MockResponse().setBody("{\"splits\":[{\"trafficTypeName\":\"client\",\"name\":\"workm\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set1\",\"set2\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"client\",\"name\":\"workm_set_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set_3\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":1602796638344}")); + return getResponse(CustomDispatcher.INITIAL_FLAGS_BY_SETS, new MockResponse().setBody("{\"ff\":{\"d\":[{\"trafficTypeName\":\"client\",\"name\":\"workm\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set1\",\"set2\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"client\",\"name\":\"workm_set_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set_3\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":1602796638344},\"rbs\":{\"s\":-1,\"t\":-1,\"d\":[]}}")); case CustomDispatcher.AUTH_ENABLED: return getResponse(CustomDispatcher.AUTH_ENABLED,new MockResponse().setBody(inputStreamToString("streaming-auth-push-enabled.json"))); case CustomDispatcher.AUTH_DISABLED: return getResponse(CustomDispatcher.AUTH_DISABLED,new MockResponse().setBody(inputStreamToString("streaming-auth-push-disabled.json"))); case CustomDispatcher.SINCE_1585948850109: - return getResponse(CustomDispatcher.SINCE_1585948850109, new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850110}")); + return getResponse(CustomDispatcher.SINCE_1585948850109, new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850110}, \"rbs\":{\"s\":1585948850109,\"t\":1585948850110,\"d\":[]}}")); case SINCE_1585948850109_FLAG_SET: - return getResponse(SINCE_1585948850109_FLAG_SET, new MockResponse().setBody("{\"splits\": [], \"since\":1585948850109, \"till\":1585948850110}")); + return getResponse(SINCE_1585948850109_FLAG_SET, new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850109, \"t\":1585948850110}, \"rbs\":{\"s\":1585948850109,\"t\":1585948850110,\"d\":[]}}")); case CustomDispatcher.SINCE_1585948850110: return getResponse(CustomDispatcher.SINCE_1585948850110, new MockResponse().setBody(inputStreamToString("splits2.json"))); case CustomDispatcher.SINCE_1585948850111: return getResponse(CustomDispatcher.SINCE_1585948850111, new MockResponse().setBody(inputStreamToString("splits_killed.json"))); case CustomDispatcher.SINCE_1585948850112: - return getResponse(CustomDispatcher.SINCE_1585948850112, new MockResponse().setBody("{\"splits\": [], \"since\":1585948850112, \"till\":1585948850112}")); + return getResponse(CustomDispatcher.SINCE_1585948850112, new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1585948850112, \"t\":1585948850112}, \"rbs\":{\"s\":1585948850112,\"t\":1585948850112,\"d\":[]}}")); case CustomDispatcher.SINCE_1602796638344: - return getResponse(CustomDispatcher.SINCE_1602796638344, new MockResponse().setBody("{\"splits\": [], \"since\":1602796638344, \"till\":1602796638344}")); + return getResponse(CustomDispatcher.SINCE_1602796638344, new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1602796638344, \"t\":1602796638344}, \"rbs\":{\"s\":1602796638344,\"t\":1602796638344,\"d\":[]}}")); case CustomDispatcher.SEGMENT_TEST_INITIAL: return getResponse(CustomDispatcher.SEGMENT_TEST_INITIAL, new MockResponse().setBody("{\"name\": \"segment3\",\"added\": [],\"removed\": [],\"since\": -1,\"till\": -1}")); case CustomDispatcher.SEGMENT3_INITIAL: diff --git a/client/src/test/java/io/split/client/utils/CustomDispatcher2.java b/client/src/test/java/io/split/client/utils/CustomDispatcher2.java new file mode 100644 index 000000000..15979ffc1 --- /dev/null +++ b/client/src/test/java/io/split/client/utils/CustomDispatcher2.java @@ -0,0 +1,181 @@ +package io.split.client.utils; + +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; +import org.jetbrains.annotations.NotNull; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.Scanner; + +public class CustomDispatcher2 extends Dispatcher { + public static final String SPLIT_FETCHER_1 = "/api/splitChanges?s=1.3&since=-1&rbSince=-1"; + public static final String SPLIT_FETCHER_2 = "/api/splitChanges?s=1.3&since=1675095324253&rbSince=1585948850111"; + public static final String SPLIT_FETCHER_3 = "/api/splitChanges?s=1.3&since=1685095324253&rbSince=1585948850111"; + public static final String SPLIT_FETCHER_4 = "/api/splitChanges?s=1.3&since=1695095324253&rbSince=1585948850111"; + public static final String SPLIT_FETCHER_5 = "/api/splitChanges?s=1.3&since=1775095324253&rbSince=1585948850111"; + + private final Map>_responses; + + public CustomDispatcher2(Map> responses){ + _responses = responses; + } + + public static CustomDispatcher2.Builder builder() { + return new CustomDispatcher2.Builder(); + } + + MockResponse response = new MockResponse().setBody("{" + + "\"ff\":{" + + "\"t\":1675095324253," + + "\"s\":-1," + + "\"d\": [{" + + "\"changeNumber\": 123," + + "\"trafficTypeName\": \"user\"," + + "\"name\": \"some_name\"," + + "\"trafficAllocation\": 100," + + "\"trafficAllocationSeed\": 123456," + + "\"seed\": 321654," + + "\"status\": \"ACTIVE\"," + + "\"killed\": false," + + "\"defaultTreatment\": \"off\"," + + "\"algo\": 2," + + "\"conditions\": [" + + "{" + + "\"partitions\": [" + + "{\"treatment\": \"on\", \"size\": 50}," + + "{\"treatment\": \"off\", \"size\": 50}" + + "]," + + "\"contitionType\": \"WHITELIST\"," + + "\"label\": \"some_label\"," + + "\"matcherGroup\": {" + + "\"matchers\": [" + + "{" + + "\"matcherType\": \"WHITELIST\"," + + "\"whitelistMatcherData\": {" + + "\"whitelist\": [\"k1\", \"k2\", \"k3\"]" + + "}," + + "\"negate\": false" + + "}" + + "]," + + "\"combiner\": \"AND\"" + + "}" + + "}," + + "{" + + "\"conditionType\": \"ROLLOUT\"," + + "\"matcherGroup\": {" + + "\"combiner\": \"AND\"," + + "\"matchers\": [" + + "{" + + "\"keySelector\": {" + + "\"trafficType\": \"user\"" + + "}," + + "\"matcherType\": \"IN_RULE_BASED_SEGMENT\"," + + "\"negate\": false," + + "\"userDefinedSegmentMatcherData\": {" + + "\"segmentName\": \"sample_rule_based_segment\"" + + "}" + + "}" + + "]" + + "}," + + "\"partitions\": [" + + "{" + + "\"treatment\": \"on\"," + + "\"size\": 100" + + "}," + + "{" + + "\"treatment\": \"off\"," + + "\"size\": 0" + + "}" + + "]," + + "\"label\": \"in rule based segment sample_rule_based_segment\"" + + "}" + + "]," + + "\"sets\": [\"set1\", \"set2\"]}]" + + "}," + + "\"rbs\": {" + + "\"t\": 1585948850111," + + "\"s\": -1," + + "\"d\": [" + + "{" + + "\"changeNumber\": 5," + + "\"name\": \"sample_rule_based_segment\"," + + "\"status\": \"ACTIVE\"," + + "\"trafficTypeName\": \"user\"," + + "\"excluded\":{" + + "\"keys\":[\"mauro@split.io\",\"gaston@split.io\"]," + + "\"segments\":[]" + + "}," + + "\"conditions\": [" + + "{" + + "\"matcherGroup\": {" + + "\"combiner\": \"AND\"," + + "\"matchers\": [" + + "{" + + "\"keySelector\": {" + + "\"trafficType\": \"user\"," + + "\"attribute\": \"email\"" + + "}," + + "\"matcherType\": \"ENDS_WITH\"," + + "\"negate\": false," + + "\"whitelistMatcherData\": {" + + "\"whitelist\": [" + + "\"@split.io\"" + + "]}}]}}]}]}}"); + MockResponse response2 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1675095324253, \"t\":1675095324253}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); + MockResponse response3 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1685095324253, \"t\":1695095324253}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); + MockResponse response4 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1695095324253, \"t\":1775095324253}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); + MockResponse response5 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1775095324253, \"t\":1775095324253}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); + + @NotNull + @Override + public MockResponse dispatch(@NotNull RecordedRequest request) { + switch (request.getPath()) { + case CustomDispatcher2.SPLIT_FETCHER_1: + return getResponse(CustomDispatcher2.SPLIT_FETCHER_1, response); + case CustomDispatcher2.SPLIT_FETCHER_2: + return getResponse(CustomDispatcher2.SPLIT_FETCHER_2, response2); + case CustomDispatcher2.SPLIT_FETCHER_3: + return getResponse(CustomDispatcher2.SPLIT_FETCHER_3, response3); + case CustomDispatcher2.SPLIT_FETCHER_4: + return getResponse(CustomDispatcher2.SPLIT_FETCHER_4, response4); + case CustomDispatcher2.SPLIT_FETCHER_5: + return getResponse(CustomDispatcher2.SPLIT_FETCHER_5, response5); + } + return new MockResponse().setResponseCode(404); + } + + private MockResponse getResponse(String target, MockResponse mockedResponse) { + Queue responses = _responses.get(target); + if(responses != null) { + MockResponse finalResponse = responses.poll(); + return finalResponse == null ? mockedResponse : finalResponse; + } + return mockedResponse; + } + + + + public static final class Builder { + private Map> _responses = new HashMap<>(); + public Builder(){}; + + /** + * Add responses to an specific path + * @param path + * @param responses + * @return + */ + public Builder path(String path, Queue responses) { + _responses.put(path, responses); + return this; + } + + public CustomDispatcher2 build() { + return new CustomDispatcher2(_responses); + } + } +} diff --git a/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java b/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java index 91be19f47..04163aedd 100644 --- a/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java +++ b/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java @@ -6,17 +6,12 @@ import io.split.client.interceptors.FlagSetsFilterImpl; import io.split.client.utils.FileInputStreamProvider; import io.split.client.utils.InputStreamProvider; -import io.split.engine.experiments.SplitChangeFetcher; -import io.split.engine.experiments.SplitFetcher; -import io.split.engine.experiments.SplitFetcherImp; -import io.split.engine.experiments.SplitParser; -import io.split.engine.experiments.SplitSynchronizationTask; +import io.split.engine.experiments.*; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTaskImp; -import io.split.storages.SegmentCacheProducer; -import io.split.storages.SplitCache; -import io.split.storages.SplitCacheProducer; +import io.split.storages.*; import io.split.storages.memory.InMemoryCacheImp; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; import io.split.telemetry.storage.NoopTelemetryStorage; import io.split.telemetry.storage.TelemetryStorage; @@ -38,15 +33,19 @@ public void testSyncAll(){ InputStreamProvider inputStreamProvider = new FileInputStreamProvider("src/test/resources/split_init.json"); SplitChangeFetcher splitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCache); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, FLAG_SETS_FILTER); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000L, null); SegmentChangeFetcher segmentChangeFetcher = new LocalhostSegmentChangeFetcher("src/test/resources/"); SegmentCacheProducer segmentCacheProducer = new SegmentCacheInMemoryImpl(); SegmentSynchronizationTaskImp segmentSynchronizationTaskImp = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1000, 1, segmentCacheProducer, - TELEMETRY_STORAGE_NOOP, splitCacheProducer, null); + TELEMETRY_STORAGE_NOOP, splitCacheProducer, null, ruleBasedSegmentCache); SplitTasks splitTasks = SplitTasks.build(splitSynchronizationTask, segmentSynchronizationTaskImp, null, null, null, null); LocalhostSynchronizer localhostSynchronizer = new LocalhostSynchronizer(splitTasks, splitFetcher, false); @@ -60,8 +59,12 @@ public void testPeriodicFetching() throws InterruptedException { SplitChangeFetcher splitChangeFetcher = Mockito.mock(JsonLocalhostSplitChangeFetcher.class); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCache); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, FLAG_SETS_FILTER); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000L, null); FetchOptions fetchOptions = new FetchOptions.Builder().build(); @@ -69,7 +72,7 @@ public void testPeriodicFetching() throws InterruptedException { SegmentCacheProducer segmentCacheProducer = new SegmentCacheInMemoryImpl(); SegmentSynchronizationTaskImp segmentSynchronizationTaskImp = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1000, 1, segmentCacheProducer, - TELEMETRY_STORAGE_NOOP, splitCacheProducer, null); + TELEMETRY_STORAGE_NOOP, splitCacheProducer, null, ruleBasedSegmentCache); SplitTasks splitTasks = SplitTasks.build(splitSynchronizationTask, segmentSynchronizationTaskImp, null, null, null, null); LocalhostSynchronizer localhostSynchronizer = new LocalhostSynchronizer(splitTasks, splitFetcher, true); @@ -78,7 +81,7 @@ public void testPeriodicFetching() throws InterruptedException { Thread.sleep(2000); - Mockito.verify(splitChangeFetcher, Mockito.times(1)).fetch(-1, fetchOptions); + Mockito.verify(splitChangeFetcher, Mockito.times(1)).fetch(-1, -1, fetchOptions); } @Test @@ -86,14 +89,17 @@ public void testRefreshSplits() { SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(FLAG_SETS_FILTER); SplitChangeFetcher splitChangeFetcher = Mockito.mock(SplitChangeFetcher.class); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, FLAG_SETS_FILTER); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCacheProducer); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000L, null); SplitTasks splitTasks = SplitTasks.build(splitSynchronizationTask, null, null, null, null, null); LocalhostSynchronizer localhostSynchronizer = new LocalhostSynchronizer(splitTasks, splitFetcher, false); - localhostSynchronizer.refreshSplits(null); + localhostSynchronizer.refreshSplits(null, null); - Mockito.verify(splitChangeFetcher, Mockito.times(1)).fetch(Mockito.anyLong(), Mockito.anyObject()); + Mockito.verify(splitChangeFetcher, Mockito.times(1)).fetch(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyObject()); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/common/SynchronizerTest.java b/client/src/test/java/io/split/engine/common/SynchronizerTest.java index b51ff8a8e..0ce439c7f 100644 --- a/client/src/test/java/io/split/engine/common/SynchronizerTest.java +++ b/client/src/test/java/io/split/engine/common/SynchronizerTest.java @@ -7,11 +7,7 @@ import io.split.client.interceptors.FlagSetsFilterImpl; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTaskImp; -import io.split.storages.SegmentCache; -import io.split.storages.SegmentCacheProducer; -import io.split.storages.SplitCache; -import io.split.storages.SplitCacheConsumer; -import io.split.storages.SplitCacheProducer; +import io.split.storages.*; import io.split.storages.memory.InMemoryCacheImp; import io.split.engine.experiments.FetchResult; import io.split.engine.experiments.SplitFetcherImp; @@ -22,9 +18,11 @@ import io.split.telemetry.synchronizer.TelemetrySyncTask; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.mockito.internal.matchers.Any; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -43,6 +41,7 @@ public class SynchronizerTest { private SegmentSynchronizationTask _segmentFetcher; private SplitFetcherImp _splitFetcher; private SplitCacheProducer _splitCacheProducer; + private RuleBasedSegmentCacheProducer _ruleBasedSegmentCacheProducer; private Synchronizer _synchronizer; private SegmentCacheProducer _segmentCacheProducer; private SplitTasks _splitTasks; @@ -56,6 +55,7 @@ public void beforeMethod() { _refreshableSplitFetcherTask = Mockito.mock(SplitSynchronizationTask.class); _segmentFetcher = Mockito.mock(SegmentSynchronizationTask.class); _splitFetcher = Mockito.mock(SplitFetcherImp.class); + _ruleBasedSegmentCacheProducer = Mockito.mock(RuleBasedSegmentCacheProducer.class); _splitCacheProducer = Mockito.mock(SplitCacheProducer.class); _segmentCacheProducer = Mockito.mock(SegmentCache.class); _telemetrySyncTask = Mockito.mock(TelemetrySyncTask.class); @@ -65,7 +65,7 @@ public void beforeMethod() { _splitTasks = SplitTasks.build(_refreshableSplitFetcherTask, _segmentFetcher, _impressionsManager, _eventsTask, _telemetrySyncTask, _uniqueKeysTracker); - _synchronizer = new SynchronizerImp(_splitTasks, _splitFetcher, _splitCacheProducer, _segmentCacheProducer, 50, 10, 5, new HashSet<>()); + _synchronizer = new SynchronizerImp(_splitTasks, _splitFetcher, _splitCacheProducer, _segmentCacheProducer, _ruleBasedSegmentCacheProducer, 50, 10, 5, new HashSet<>()); } @Test @@ -83,7 +83,7 @@ public void syncAll() throws InterruptedException { public void testSyncAllSegments() throws InterruptedException, NoSuchFieldException, IllegalAccessException { SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(Mockito.mock(SegmentChangeFetcher.class), 20L, 1, _segmentCacheProducer, Mockito.mock(TelemetryRuntimeProducer.class), - Mockito.mock(SplitCacheConsumer.class), null); + Mockito.mock(SplitCacheConsumer.class), null, Mockito.mock(RuleBasedSegmentCache.class)); Field synchronizerSegmentFetcher = SynchronizerImp.class.getDeclaredField("_segmentSynchronizationTaskImp"); synchronizerSegmentFetcher.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); @@ -120,7 +120,7 @@ public void stopPeriodicFetching() { public void streamingRetryOnSplit() { when(_splitCacheProducer.getChangeNumber()).thenReturn(0l).thenReturn(0l).thenReturn(1l); when(_splitFetcher.forceRefresh(Mockito.anyObject())).thenReturn(new FetchResult(true, false, new HashSet<>())); - _synchronizer.refreshSplits(1L); + _synchronizer.refreshSplits(1L, 0L); Mockito.verify(_splitCacheProducer, Mockito.times(3)).getChangeNumber(); } @@ -145,7 +145,7 @@ public void streamingRetryOnSplitAndSegment() { SegmentFetcher fetcher = Mockito.mock(SegmentFetcher.class); when(_segmentCacheProducer.getChangeNumber(Mockito.anyString())).thenReturn(0l).thenReturn(0l).thenReturn(1l); when(_segmentFetcher.getFetcher(Mockito.anyString())).thenReturn(fetcher); - _synchronizer.refreshSplits(1L); + _synchronizer.refreshSplits(1L, 0L); Mockito.verify(_splitCacheProducer, Mockito.times(3)).getChangeNumber(); Mockito.verify(_segmentFetcher, Mockito.times(2)).getFetcher(Mockito.anyString()); @@ -158,6 +158,7 @@ public void testCDNBypassIsRequestedAfterNFailures() { _splitFetcher, cache, _segmentCacheProducer, + _ruleBasedSegmentCacheProducer, 50, 3, 1, @@ -173,7 +174,7 @@ public void testCDNBypassIsRequestedAfterNFailures() { return new FetchResult(true, false, new HashSet<>()); }).when(_splitFetcher).forceRefresh(optionsCaptor.capture()); - imp.refreshSplits(123L); + imp.refreshSplits(123L, 0L); List options = optionsCaptor.getAllValues(); Assert.assertEquals(options.size(), 4); @@ -190,6 +191,7 @@ public void testCDNBypassRequestLimitAndBackoff() throws NoSuchFieldException, I _splitFetcher, cache, _segmentCacheProducer, + _ruleBasedSegmentCacheProducer, 50, 3, 1, @@ -214,7 +216,7 @@ public void testCDNBypassRequestLimitAndBackoff() throws NoSuchFieldException, I backoffBase.set(imp, 1); // 1ms long before = System.currentTimeMillis(); - imp.refreshSplits(1L); + imp.refreshSplits(1L, 0L); long after = System.currentTimeMillis(); List options = optionsCaptor.getAllValues(); @@ -245,6 +247,7 @@ public void testCDNBypassRequestLimitAndForSegmentsBackoff() throws NoSuchFieldE _splitFetcher, cache, _segmentCacheProducer, + _ruleBasedSegmentCacheProducer, 50, 3, 1, @@ -303,6 +306,7 @@ public void testDataRecording(){ _splitFetcher, cache, _segmentCacheProducer, + _ruleBasedSegmentCacheProducer, 50, 3, 1, @@ -321,4 +325,18 @@ public void testDataRecording(){ Mockito.verify(_uniqueKeysTracker, Mockito.times(1)).stop(); Mockito.verify(_telemetrySyncTask, Mockito.times(1)).stopScheduledTask(); } + + @Test + public void skipSyncWhenChangeNumbersAreZero() { + _synchronizer.refreshSplits(0L, 0L); + Mockito.verify(_splitFetcher, Mockito.times(0)).forceRefresh(Mockito.anyObject()); + } + + @Test + public void testSyncRuleBasedSegment() { + when(_ruleBasedSegmentCacheProducer.getChangeNumber()).thenReturn(-1l).thenReturn(-1l).thenReturn(123l); + when(_splitFetcher.forceRefresh(Mockito.anyObject())).thenReturn(new FetchResult(true, false, new HashSet<>())); + _synchronizer.refreshSplits(0L, 123L); + Mockito.verify(_splitFetcher, Mockito.times(2)).forceRefresh(Mockito.anyObject()); + } } \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java b/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java index a58a22194..5cc6d01d9 100644 --- a/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java +++ b/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java @@ -7,14 +7,19 @@ import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; import io.split.engine.experiments.ParsedCondition; +import io.split.engine.experiments.ParsedRuleBasedSegment; import io.split.engine.experiments.ParsedSplit; import io.split.engine.matchers.AttributeMatcher; import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.PrerequisitesMatcher; +import io.split.engine.matchers.RuleBasedSegmentMatcher; import io.split.engine.matchers.strings.EndsWithAnyOfMatcher; import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SegmentCache; import io.split.storages.SplitCache; import io.split.storages.memory.InMemoryCacheImp; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; import org.junit.Assert; import org.junit.Test; @@ -33,6 +38,7 @@ public class EvaluatorIntegrationTest { private static final String TEST_LABEL_VALUE_WHITELIST = "test label whitelist"; private static final String TEST_LABEL_VALUE_ROLL_OUT = "test label roll out"; private static final String ON_TREATMENT = "on"; + private static final String OFF_TREATMENT = "off"; @Test public void evaluateFeatureWithWhitelistShouldReturnOn() { @@ -152,35 +158,61 @@ public void evaluateFeaturesSplitsNull() { Map result = evaluator.evaluateFeatures("mauro@test.io", null, null, null); } + @Test + public void evaluateFeatureWithRuleBasedSegmentMatcher() { + Evaluator evaluator = buildEvaluatorAndLoadCache(false, 100); + + EvaluatorImp.TreatmentLabelAndChangeNumber result = evaluator.evaluateFeature("mauro@test.io", null, "split_5", null); + Assert.assertEquals(ON_TREATMENT, result.treatment); + + result = evaluator.evaluateFeature("admin", null, "split_5", null); + Assert.assertEquals(OFF_TREATMENT, result.treatment); + } + private Evaluator buildEvaluatorAndLoadCache(boolean killed, int trafficAllocation) { FlagSetsFilter flagSetsFilter = new FlagSetsFilterImpl(new HashSet<>()); SplitCache splitCache = new InMemoryCacheImp(flagSetsFilter); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); - Evaluator evaluator = new EvaluatorImp(splitCache, segmentCache); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + Evaluator evaluator = new EvaluatorImp(splitCache, segmentCache, ruleBasedSegmentCache); Partition partition = new Partition(); partition.treatment = ON_TREATMENT; partition.size = 100; + Partition partitionOff = new Partition(); + partitionOff.treatment = OFF_TREATMENT; + partitionOff.size = 100; + List partitions = Lists.newArrayList(partition); AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); AttributeMatcher endsWithMatcher = AttributeMatcher.vanilla(new EndsWithAnyOfMatcher(Lists.newArrayList("@test.io", "@mail.io"))); + AttributeMatcher ruleBasedSegmentMatcher = AttributeMatcher.vanilla(new RuleBasedSegmentMatcher("sample_rule_based_segment")); CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); CombiningMatcher endsWithCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(endsWithMatcher)); + CombiningMatcher ruleBasedSegmentCombinerMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ruleBasedSegmentMatcher)); ParsedCondition whitelistCondition = new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, partitions, TEST_LABEL_VALUE_WHITELIST); ParsedCondition rollOutCondition = new ParsedCondition(ConditionType.ROLLOUT, endsWithCombiningMatcher, partitions, TEST_LABEL_VALUE_ROLL_OUT); + ParsedCondition ruleBasedSegmentCondition = new ParsedCondition(ConditionType.ROLLOUT, ruleBasedSegmentCombinerMatcher, Lists.newArrayList(partitionOff), TEST_LABEL_VALUE_ROLL_OUT); List conditions = Lists.newArrayList(whitelistCondition, rollOutCondition); + List conditionsForRBS = Lists.newArrayList(ruleBasedSegmentCondition, rollOutCondition); + + ParsedSplit parsedSplit1 = new ParsedSplit("split_1", 0, false, DEFAULT_TREATMENT_VALUE, conditions, TRAFFIC_TYPE_VALUE, 223366551, 100, 0, 2, null, new HashSet<>(), true, new PrerequisitesMatcher(null)); + ParsedSplit parsedSplit2 = new ParsedSplit("split_2", 0, true, DEFAULT_TREATMENT_VALUE, conditions, TRAFFIC_TYPE_VALUE, 223366552, 100, 0, 2, null, new HashSet<>(), true, new PrerequisitesMatcher(null)); + ParsedSplit parsedSplit3 = new ParsedSplit("split_3", 0, false, DEFAULT_TREATMENT_VALUE, conditions, TRAFFIC_TYPE_VALUE, 223366554, 100, 0, 2, null, new HashSet<>(), true, new PrerequisitesMatcher(null)); + ParsedSplit parsedSplit4 = new ParsedSplit("split_test", 0, killed, DEFAULT_TREATMENT_VALUE, conditions, TRAFFIC_TYPE_VALUE, 223366555, trafficAllocation, 0, 2, null, new HashSet<>(), true, new PrerequisitesMatcher(null)); + ParsedSplit parsedSplit5 = new ParsedSplit("split_5", 0, false, DEFAULT_TREATMENT_VALUE, conditionsForRBS, TRAFFIC_TYPE_VALUE, 223366554, 100, 0, 2, null, new HashSet<>(), true, new PrerequisitesMatcher(null)); + splitCache.putMany(Stream.of(parsedSplit1, parsedSplit2, parsedSplit3, parsedSplit4, parsedSplit5).collect(Collectors.toList())); - ParsedSplit parsedSplit1 = new ParsedSplit("split_1", 0, false, DEFAULT_TREATMENT_VALUE, conditions, TRAFFIC_TYPE_VALUE, 223366551, 100, 0, 2, null, new HashSet<>(), true); - ParsedSplit parsedSplit2 = new ParsedSplit("split_2", 0, true, DEFAULT_TREATMENT_VALUE, conditions, TRAFFIC_TYPE_VALUE, 223366552, 100, 0, 2, null, new HashSet<>(), true); - ParsedSplit parsedSplit3 = new ParsedSplit("split_3", 0, false, DEFAULT_TREATMENT_VALUE, conditions, TRAFFIC_TYPE_VALUE, 223366554, 100, 0, 2, null, new HashSet<>(), true); - ParsedSplit parsedSplit4 = new ParsedSplit("split_test", 0, killed, DEFAULT_TREATMENT_VALUE, conditions, TRAFFIC_TYPE_VALUE, 223366555, trafficAllocation, 0, 2, null, new HashSet<>(), true); + ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("sample_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, TEST_LABEL_VALUE_WHITELIST)),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), Lists.newArrayList()); + ruleBasedSegmentCache.update(Lists.newArrayList(parsedRuleBasedSegment), null, 123); - splitCache.putMany(Stream.of(parsedSplit1, parsedSplit2, parsedSplit3, parsedSplit4).collect(Collectors.toList())); return evaluator; } } \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java b/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java index 5be942bf1..cf166bd2b 100644 --- a/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java +++ b/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java @@ -2,9 +2,13 @@ import io.split.client.dtos.ConditionType; import io.split.client.dtos.Partition; +import io.split.client.dtos.Prerequisites; +import io.split.client.utils.Json; import io.split.engine.experiments.ParsedCondition; import io.split.engine.experiments.ParsedSplit; import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.PrerequisitesMatcher; +import io.split.storages.RuleBasedSegmentCacheConsumer; import io.split.storages.SegmentCacheConsumer; import io.split.storages.SplitCacheConsumer; import org.junit.Assert; @@ -33,6 +37,7 @@ public class EvaluatorTest { private SplitCacheConsumer _splitCacheConsumer; private SegmentCacheConsumer _segmentCacheConsumer; + private RuleBasedSegmentCacheConsumer _ruleBasedSegmentCacheConsumer; private Evaluator _evaluator; private CombiningMatcher _matcher; private Map _configurations; @@ -44,7 +49,8 @@ public class EvaluatorTest { public void before() { _splitCacheConsumer = Mockito.mock(SplitCacheConsumer.class); _segmentCacheConsumer = Mockito.mock(SegmentCacheConsumer.class); - _evaluator = new EvaluatorImp(_splitCacheConsumer, _segmentCacheConsumer); + _ruleBasedSegmentCacheConsumer = Mockito.mock(RuleBasedSegmentCacheConsumer.class); + _evaluator = new EvaluatorImp(_splitCacheConsumer, _segmentCacheConsumer, _ruleBasedSegmentCacheConsumer); _matcher = Mockito.mock(CombiningMatcher.class); _evaluationContext = Mockito.mock(EvaluationContext.class); @@ -65,7 +71,7 @@ public void evaluateWhenSplitNameDoesNotExistReturnControl() { @Test public void evaluateWhenSplitIsKilledReturnDefaultTreatment() { - ParsedSplit split = ParsedSplit.createParsedSplitForTests(SPLIT_NAME, 0, true, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 2, new HashSet<>(), true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests(SPLIT_NAME, 0, true, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 2, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); @@ -77,7 +83,7 @@ public void evaluateWhenSplitIsKilledReturnDefaultTreatment() { @Test public void evaluateWithoutConditionsReturnDefaultTreatment() { - ParsedSplit split = ParsedSplit.createParsedSplitForTests(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 2, new HashSet<>(), true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 2, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); @@ -96,7 +102,7 @@ public void evaluateWithRollOutConditionBucketIsBiggerTrafficAllocationReturnDef ParsedCondition condition = new ParsedCondition(ConditionType.ROLLOUT, _matcher,_partitions, TEST_LABEL_VALUE); _conditions.add(condition); - ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 10, 12, 2, _configurations, new HashSet<>(), true); + ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 10, 12, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); Mockito.when(condition.matcher().match(MATCHING_KEY, BUCKETING_KEY, null, _evaluationContext)).thenReturn(true); @@ -117,7 +123,7 @@ public void evaluateWithRollOutConditionTrafficAllocationIsBiggerBucketReturnTre ParsedCondition condition = new ParsedCondition(ConditionType.ROLLOUT, _matcher, _partitions, TEST_LABEL_VALUE); _conditions.add(condition); - ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true); + ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true); @@ -138,7 +144,7 @@ public void evaluateWithWhitelistConditionReturnTreatment() { ParsedCondition condition = new ParsedCondition(ConditionType.WHITELIST, _matcher, _partitions, "test whitelist label"); _conditions.add(condition); - ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true); + ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true); @@ -152,7 +158,7 @@ public void evaluateWithWhitelistConditionReturnTreatment() { @Test public void evaluateWithSets() { - ParsedSplit split = ParsedSplit.createParsedSplitForTests(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 2, new HashSet<>(Arrays.asList("set1", "set2")), true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 2, new HashSet<>(Arrays.asList("set1", "set2")), true, new PrerequisitesMatcher(null)); List sets = new ArrayList<>(Arrays.asList("set1", "empty_set")); Map> flagSets = new HashMap<>(); flagSets.put("set1", new HashSet<>(Arrays.asList(SPLIT_NAME))); @@ -173,7 +179,7 @@ public void evaluateWithSets() { @Test public void evaluateWithSetsNotHaveFlags() { - ParsedSplit split = ParsedSplit.createParsedSplitForTests(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 2, new HashSet<>(Arrays.asList("set1", "set2")), true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 2, new HashSet<>(Arrays.asList("set1", "set2")), true, new PrerequisitesMatcher(null)); List sets = new ArrayList<>(Arrays.asList("set2")); Map> flagSets = new HashMap<>(); Mockito.when(_splitCacheConsumer.getNamesByFlagSets(sets)).thenReturn(flagSets); @@ -183,4 +189,41 @@ public void evaluateWithSetsNotHaveFlags() { Map result = _evaluator.evaluateFeaturesByFlagSets(MATCHING_KEY, BUCKETING_KEY, sets, null); Assert.assertTrue(result.isEmpty()); } + + @Test + public void evaluateWithPrerequisites() { + Partition partition = new Partition(); + partition.treatment = TREATMENT_VALUE; + partition.size = 100; + _partitions.add(partition); + ParsedCondition condition = new ParsedCondition(ConditionType.WHITELIST, _matcher, _partitions, "test whitelist label"); + _conditions.add(condition); + List prerequisites = Arrays.asList(Json.fromJson("{\"n\": \"split1\", \"ts\": [\"" + TREATMENT_VALUE + "\"]}", Prerequisites.class)); + + ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(prerequisites)); + ParsedSplit split1 = new ParsedSplit("split1", 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null)); + + Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); + Mockito.when(_splitCacheConsumer.get("split1")).thenReturn(split1); + Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true); + + EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); + assertEquals(TREATMENT_VALUE, result.treatment); + assertEquals("test whitelist label", result.label); + assertEquals(CHANGE_NUMBER, result.changeNumber); + + Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(false); + result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); + assertEquals(DEFAULT_TREATMENT_VALUE, result.treatment); + assertEquals(Labels.PREREQUISITES_NOT_MET, result.label); + assertEquals(CHANGE_NUMBER, result.changeNumber); + + // if split is killed, label should be killed. + split = new ParsedSplit(SPLIT_NAME, 0, true, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(prerequisites)); + Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split); + result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null); + assertEquals(DEFAULT_TREATMENT_VALUE, result.treatment); + assertEquals(Labels.KILLED, result.label); + assertEquals(CHANGE_NUMBER, result.changeNumber); + } } \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/experiments/AChangePerCallSplitChangeFetcher.java b/client/src/test/java/io/split/engine/experiments/AChangePerCallSplitChangeFetcher.java index 63fbf1e26..0e0f67296 100644 --- a/client/src/test/java/io/split/engine/experiments/AChangePerCallSplitChangeFetcher.java +++ b/client/src/test/java/io/split/engine/experiments/AChangePerCallSplitChangeFetcher.java @@ -32,7 +32,7 @@ public AChangePerCallSplitChangeFetcher(String segmentName) { @Override - public SplitChange fetch(long since, FetchOptions options) { + public SplitChange fetch(long since, long rbSince, FetchOptions options) { long latestChangeNumber = since + 1; Condition condition = null; @@ -67,9 +67,9 @@ public SplitChange fetch(long since, FetchOptions options) { SplitChange splitChange = new SplitChange(); - splitChange.splits = Lists.newArrayList(add, remove); - splitChange.since = since; - splitChange.till = latestChangeNumber; + splitChange.featureFlags.d = Lists.newArrayList(add, remove); + splitChange.featureFlags.s = since; + splitChange.featureFlags.t = latestChangeNumber; _lastAdded.set(latestChangeNumber); diff --git a/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java b/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java new file mode 100644 index 000000000..253636814 --- /dev/null +++ b/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java @@ -0,0 +1,88 @@ +package io.split.engine.experiments; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.ExcludedSegments; +import io.split.client.dtos.MatcherCombiner; +import io.split.client.dtos.SplitChange; +import io.split.client.utils.Json; +import io.split.client.utils.RuleBasedSegmentsToUpdate; +import io.split.engine.matchers.AttributeMatcher; +import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.UserDefinedSegmentMatcher; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static io.split.client.utils.RuleBasedSegmentProcessor.processRuleBasedSegmentChanges; + +public class ParsedRuleBasedSegmentTest { + + @Test + public void works() { + List excludedSegments = new ArrayList<>(); + excludedSegments.add(new ExcludedSegments("standard","segment1")); + excludedSegments.add(new ExcludedSegments("standard","segment2")); + + AttributeMatcher segmentMatcher = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher("employees")); + CombiningMatcher segmentCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(segmentMatcher)); + ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("another_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, segmentCombiningMatcher, null, "label")), "user", + 123, Lists.newArrayList("mauro@test.io", "gaston@test.io"), excludedSegments); + + Assert.assertEquals(Sets.newHashSet("segment2", "segment1", "employees"), parsedRuleBasedSegment.getSegmentsNames()); + Assert.assertEquals("another_rule_based_segment", parsedRuleBasedSegment.ruleBasedSegment()); + Assert.assertEquals(Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, segmentCombiningMatcher, null, "label")), + parsedRuleBasedSegment.parsedConditions()); + Assert.assertEquals(123, parsedRuleBasedSegment.changeNumber()); + } + + @Test + public void worksWithoutExcluded() { + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + String load = "{\"ff\":{\"s\":-1,\"t\":-1,\"d\":[]},\"rbs\":{\"s\":-1,\"t\":1457726098069,\"d\":[{ \"changeNumber\": 123, \"trafficTypeName\": \"user\", \"name\": \"some_name\"," + + "\"status\": \"ACTIVE\",\"conditions\": [{\"contitionType\": \"ROLLOUT\"," + + "\"label\": \"some_label\", \"matcherGroup\": { \"matchers\": [{ \"matcherType\": \"ALL_KEYS\", \"negate\": false}]," + + "\"combiner\": \"AND\"}}]}]}}"; + SplitChange change = Json.fromJson(load, SplitChange.class); + RuleBasedSegmentsToUpdate toUpdate = processRuleBasedSegmentChanges(parser, change.ruleBasedSegments.d); + Assert.assertTrue(toUpdate.getToAdd().get(0).excludedKeys().isEmpty()); + Assert.assertTrue(toUpdate.getToAdd().get(0).excludedSegments().isEmpty()); + + load = "{\"ff\":{\"s\":-1,\"t\":-1,\"d\":[]},\"rbs\":{\"s\":-1,\"t\":1457726098069,\"d\":[{ \"changeNumber\": 123, \"trafficTypeName\": \"user\", \"name\": \"some_name\"," + + "\"status\": \"ACTIVE\",\"excluded\":{\"segments\":[{\"type\": \"standard\",\"name\":\"segment1\"}]},\"conditions\": [{\"contitionType\": \"ROLLOUT\"," + + "\"label\": \"some_label\", \"matcherGroup\": { \"matchers\": [{ \"matcherType\": \"ALL_KEYS\", \"negate\": false}]," + + "\"combiner\": \"AND\"}}]}]}}"; + change = Json.fromJson(load, SplitChange.class); + toUpdate = processRuleBasedSegmentChanges(parser, change.ruleBasedSegments.d); + Assert.assertTrue(toUpdate.getToAdd().get(0).excludedKeys().isEmpty()); + + load = "{\"ff\":{\"s\":-1,\"t\":-1,\"d\":[]},\"rbs\":{\"s\":-1,\"t\":1457726098069,\"d\":[{ \"changeNumber\": 123, \"trafficTypeName\": \"user\", \"name\": \"some_name\"," + + "\"status\": \"ACTIVE\",\"excluded\":{\"segments\":[{\"type\": \"standard\",\"name\":\"segment1\"}], \"keys\":null},\"conditions\": [{\"contitionType\": \"ROLLOUT\"," + + "\"label\": \"some_label\", \"matcherGroup\": { \"matchers\": [{ \"matcherType\": \"ALL_KEYS\", \"negate\": false}]," + + "\"combiner\": \"AND\"}}]}]}}"; + change = Json.fromJson(load, SplitChange.class); + toUpdate = processRuleBasedSegmentChanges(parser, change.ruleBasedSegments.d); + Assert.assertTrue(toUpdate.getToAdd().get(0).excludedKeys().isEmpty()); + + load = "{\"ff\":{\"s\":-1,\"t\":-1,\"d\":[]},\"rbs\":{\"s\":-1,\"t\":1457726098069,\"d\":[{ \"changeNumber\": 123, \"trafficTypeName\": \"user\", \"name\": \"some_name\"," + + "\"status\": \"ACTIVE\",\"excluded\":{\"keys\":[\"key1\"]},\"conditions\": [{\"contitionType\": \"ROLLOUT\"," + + "\"label\": \"some_label\", \"matcherGroup\": { \"matchers\": [{ \"matcherType\": \"ALL_KEYS\", \"negate\": false}]," + + "\"combiner\": \"AND\"}}]}]}}"; + change = Json.fromJson(load, SplitChange.class); + toUpdate = processRuleBasedSegmentChanges(parser, change.ruleBasedSegments.d); + Assert.assertTrue(toUpdate.getToAdd().get(0).excludedSegments().isEmpty()); + + load = "{\"ff\":{\"s\":-1,\"t\":-1,\"d\":[]},\"rbs\":{\"s\":-1,\"t\":1457726098069,\"d\":[{ \"changeNumber\": 123, \"trafficTypeName\": \"user\", \"name\": \"some_name\"," + + "\"status\": \"ACTIVE\",\"excluded\":{\"segments\":null, \"keys\":[\"key1\"]},\"conditions\": [{\"contitionType\": \"ROLLOUT\"," + + "\"label\": \"some_label\", \"matcherGroup\": { \"matchers\": [{ \"matcherType\": \"ALL_KEYS\", \"negate\": false}]," + + "\"combiner\": \"AND\"}}]}]}}"; + change = Json.fromJson(load, SplitChange.class); + toUpdate = processRuleBasedSegmentChanges(parser, change.ruleBasedSegments.d); + Assert.assertTrue(toUpdate.getToAdd().get(0).excludedSegments().isEmpty()); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java b/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java new file mode 100644 index 000000000..526e44491 --- /dev/null +++ b/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java @@ -0,0 +1,556 @@ +package io.split.engine.experiments; + +import com.google.common.collect.Lists; +import io.split.client.dtos.*; +import io.split.client.dtos.Matcher; +import io.split.client.utils.Json; +import io.split.client.utils.RuleBasedSegmentsToUpdate; +import io.split.engine.ConditionsTestUtil; +import io.split.engine.evaluator.Labels; +import io.split.engine.matchers.*; +import io.split.engine.matchers.collections.ContainsAllOfSetMatcher; +import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; +import io.split.engine.matchers.collections.EqualToSetMatcher; +import io.split.engine.matchers.collections.PartOfSetMatcher; +import io.split.engine.matchers.strings.ContainsAnyOfMatcher; +import io.split.engine.matchers.strings.EndsWithAnyOfMatcher; +import io.split.engine.matchers.strings.StartsWithAnyOfMatcher; +import io.split.engine.segments.SegmentChangeFetcher; +import io.split.grammar.Treatments; +import io.split.storages.SegmentCache; +import io.split.storages.memory.SegmentCacheInMemoryImpl; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.split.TestHelper.makeRuleBasedSegment; +import static io.split.client.utils.RuleBasedSegmentProcessor.processRuleBasedSegmentChanges; + +import static org.junit.Assert.assertTrue; + +/** + * Tests for ExperimentParser + * + * @author adil + */ +public class RuleBasedSegmentParserTest { + + public static final String EMPLOYEES = "employees"; + public static final String SALES_PEOPLE = "salespeople"; + public static final int CONDITIONS_UPPER_LIMIT = 50; + + @Test + public void works() { + SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + segmentCache.updateSegment(EMPLOYEES, Stream.of("adil", "pato", "trevor").collect(Collectors.toList()), new ArrayList<>(), 1L); + segmentCache.updateSegment(SALES_PEOPLE, Stream.of("kunal").collect(Collectors.toList()), new ArrayList<>(), 1L); + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + + Matcher employeesMatcher = ConditionsTestUtil.userDefinedSegmentMatcher(EMPLOYEES, false); + Matcher notSalespeople = ConditionsTestUtil.userDefinedSegmentMatcher(SALES_PEOPLE, true); + Condition c = ConditionsTestUtil.and(employeesMatcher, notSalespeople, null); + List conditions = Lists.newArrayList(c); + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); + + AttributeMatcher employeesMatcherLogic = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher(EMPLOYEES)); + AttributeMatcher notSalesPeopleMatcherLogic = new AttributeMatcher(null, new UserDefinedSegmentMatcher(SALES_PEOPLE), true); + CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(employeesMatcherLogic, notSalesPeopleMatcherLogic)); + ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); + List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); + + ParsedRuleBasedSegment expected = ParsedRuleBasedSegment.createParsedRuleBasedSegmentForTests ("first.name", listOfMatcherAndSplits, "user", 1, + new ArrayList<>(), new ArrayList<>()); + + Assert.assertEquals(actual, expected); + assertTrue(expected.hashCode() != 0); + assertTrue(expected.equals(expected)); + } + + @Test + public void worksForTwoConditions() { + SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + segmentCache.updateSegment(EMPLOYEES, Stream.of("adil", "pato", "trevor").collect(Collectors.toList()), new ArrayList<>(), 1L); + segmentCache.updateSegment(SALES_PEOPLE, Stream.of("kunal").collect(Collectors.toList()), new ArrayList<>(), 1L); + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + + Matcher employeesMatcher = ConditionsTestUtil.userDefinedSegmentMatcher(EMPLOYEES, false); + + Matcher salespeopleMatcher = ConditionsTestUtil.userDefinedSegmentMatcher(SALES_PEOPLE, false); + + List fullyRollout = Lists.newArrayList(ConditionsTestUtil.partition("on", 100)); + List turnOff = Lists.newArrayList(ConditionsTestUtil.partition(Treatments.CONTROL, 100)); + + Condition c1 = ConditionsTestUtil.and(employeesMatcher, fullyRollout); + Condition c2 = ConditionsTestUtil.and(salespeopleMatcher, turnOff); + + List conditions = Lists.newArrayList(c1, c2); + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); + + ParsedCondition parsedCondition1 = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new UserDefinedSegmentMatcher(EMPLOYEES)), fullyRollout); + ParsedCondition parsedCondition2 = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new UserDefinedSegmentMatcher(EMPLOYEES)), turnOff); + List listOfParsedConditions = Lists.newArrayList(parsedCondition1, parsedCondition2); + + ParsedRuleBasedSegment expected = ParsedRuleBasedSegment.createParsedRuleBasedSegmentForTests ("first.name", listOfParsedConditions, "user", 1, + new ArrayList<>(), new ArrayList<>()); + + Assert.assertEquals(actual, expected); + } + + @Test + public void successForLongConditions() { + SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + segmentCache.updateSegment(EMPLOYEES, Stream.of("adil", "pato", "trevor").collect(Collectors.toList()), new ArrayList<>(), 1L); + segmentCache.updateSegment(SALES_PEOPLE, Stream.of("kunal").collect(Collectors.toList()), new ArrayList<>(), 1L); + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee); + + Matcher employeesMatcher = ConditionsTestUtil.userDefinedSegmentMatcher(EMPLOYEES, false); + + List conditions = Lists.newArrayList(); + List p1 = Lists.newArrayList(ConditionsTestUtil.partition("on", 100)); + for (int i = 0 ; i < CONDITIONS_UPPER_LIMIT+1 ; i++) { + Condition c = ConditionsTestUtil.and(employeesMatcher, p1); + conditions.add(c); + } + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + + Assert.assertNotNull(parser.parse(ruleBasedSegment)); + } + + @Test + public void worksWithAttributes() { + SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + segmentCache.updateSegment(EMPLOYEES, Stream.of("adil", "pato", "trevor").collect(Collectors.toList()), new ArrayList<>(), 1L); + segmentCache.updateSegment(SALES_PEOPLE, Stream.of("kunal").collect(Collectors.toList()), new ArrayList<>(), 1L); + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + + Matcher employeesMatcher = ConditionsTestUtil.userDefinedSegmentMatcher("user", "name", EMPLOYEES, false); + + Matcher creationDateNotOlderThanAPoint = ConditionsTestUtil.numericMatcher("user", "creation_date", + MatcherType.GREATER_THAN_OR_EQUAL_TO, + DataType.DATETIME, + 1457386741L, + true); + + Condition c = ConditionsTestUtil.and(employeesMatcher, creationDateNotOlderThanAPoint, null); + + List conditions = Lists.newArrayList(c); + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); + + AttributeMatcher employeesMatcherLogic = new AttributeMatcher("name", new UserDefinedSegmentMatcher(EMPLOYEES), false); + AttributeMatcher creationDateNotOlderThanAPointLogic = new AttributeMatcher("creation_date", new GreaterThanOrEqualToMatcher(1457386741L, DataType.DATETIME), true); + CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(employeesMatcherLogic, creationDateNotOlderThanAPointLogic)); + ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); + List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); + + ParsedRuleBasedSegment expected = ParsedRuleBasedSegment.createParsedRuleBasedSegmentForTests ("first.name", listOfMatcherAndSplits, "user", 1, + new ArrayList<>(), new ArrayList<>()); + + Assert.assertEquals(actual, expected); + } + + @Test + public void lessThanOrEqualTo() { + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + + Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.LESS_THAN_OR_EQUAL_TO, DataType.NUMBER, 10L, false); + Condition c = ConditionsTestUtil.and(ageLessThan10, null); + + List conditions = Lists.newArrayList(c); + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); + + AttributeMatcher ageLessThan10Logic = new AttributeMatcher("age", new LessThanOrEqualToMatcher(10, DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageLessThan10Logic)); + ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); + List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); + + ParsedRuleBasedSegment expected = ParsedRuleBasedSegment.createParsedRuleBasedSegmentForTests ("first.name", listOfMatcherAndSplits, "user", 1, + new ArrayList<>(), new ArrayList<>()); + + Assert.assertEquals(actual, expected); + } + + @Test + public void equalTo() { + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + + Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, DataType.NUMBER, 10L, true); + Condition c = ConditionsTestUtil.and(ageLessThan10, null); + List conditions = Lists.newArrayList(c); + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); + + AttributeMatcher equalToMatcher = new AttributeMatcher("age", new EqualToMatcher(10, DataType.NUMBER), true); + CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(equalToMatcher)); + ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); + List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); + + ParsedRuleBasedSegment expected = ParsedRuleBasedSegment.createParsedRuleBasedSegmentForTests ("first.name", listOfMatcherAndSplits, "user", 1, + new ArrayList<>(), new ArrayList<>()); + + Assert.assertEquals(actual, expected); + } + + @Test + public void equalToNegativeNumber() { + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + + Matcher equalToNegative10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, DataType.NUMBER, -10L, false); + Condition c = ConditionsTestUtil.and(equalToNegative10, null); + List conditions = Lists.newArrayList(c); + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); + + AttributeMatcher ageEqualTo10Logic = new AttributeMatcher("age", new EqualToMatcher(-10, DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageEqualTo10Logic)); + ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); + List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); + + ParsedRuleBasedSegment expected = ParsedRuleBasedSegment.createParsedRuleBasedSegmentForTests ("first.name", listOfMatcherAndSplits, "user", 1, + new ArrayList<>(), new ArrayList<>()); + + Assert.assertEquals(actual, expected); + } + + @Test + public void between() { + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + + Matcher ageBetween10And11 = ConditionsTestUtil.betweenMatcher("user", + "age", + DataType.NUMBER, + 10, + 12, + false); + + Condition c = ConditionsTestUtil.and(ageBetween10And11, null); + List conditions = Lists.newArrayList(c); + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); + + AttributeMatcher ageBetween10And11Logic = new AttributeMatcher("age", new BetweenMatcher(10, 12, DataType.NUMBER), false); + CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ageBetween10And11Logic)); + ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); + List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); + + ParsedRuleBasedSegment expected = ParsedRuleBasedSegment.createParsedRuleBasedSegmentForTests ("first.name", listOfMatcherAndSplits, "user", 1, + new ArrayList<>(), new ArrayList<>()); + + Assert.assertEquals(actual, expected); + } + + @Test + public void containsAnyOfSet() { + ArrayList set = Lists.newArrayList("sms", "voice"); + + Condition c = ConditionsTestUtil.containsAnyOfSet("user", + "products", + set, + false, + null + ); + + ContainsAnyOfSetMatcher m = new ContainsAnyOfSetMatcher(set); + setMatcherTest(c, m); + } + + @Test + public void containsAllOfSet() { + ArrayList set = Lists.newArrayList("sms", "voice"); + Condition c = ConditionsTestUtil.containsAllOfSet("user", + "products", + set, + false, + null + ); + + ContainsAllOfSetMatcher m = new ContainsAllOfSetMatcher(set); + setMatcherTest(c, m); + } + + @Test + public void equalToSet() { + ArrayList set = Lists.newArrayList("sms", "voice"); + Condition c = ConditionsTestUtil.equalToSet("user", + "products", + set, + false, + null + ); + + EqualToSetMatcher m = new EqualToSetMatcher(set); + setMatcherTest(c, m); + } + + @Test + public void isPartOfSet() { + ArrayList set = Lists.newArrayList("sms", "voice"); + Condition c = ConditionsTestUtil.isPartOfSet("user", + "products", + set, + false, + null + ); + + PartOfSetMatcher m = new PartOfSetMatcher(set); + setMatcherTest(c, m); + } + + @Test + public void startsWithString() { + ArrayList set = Lists.newArrayList("sms", "voice"); + Condition c = ConditionsTestUtil.startsWithString("user", + "products", + set, + false, + null + ); + + StartsWithAnyOfMatcher m = new StartsWithAnyOfMatcher(set); + setMatcherTest(c, m); + } + + @Test + public void endsWithString() { + ArrayList set = Lists.newArrayList("sms", "voice"); + Condition c = ConditionsTestUtil.endsWithString("user", + "products", + set, + false, + null + ); + + EndsWithAnyOfMatcher m = new EndsWithAnyOfMatcher(set); + setMatcherTest(c, m); + } + + + @Test + public void containsString() { + ArrayList set = Lists.newArrayList("sms", "voice"); + Condition c = ConditionsTestUtil.containsString("user", + "products", + set, + false, + null + ); + + ContainsAnyOfMatcher m = new ContainsAnyOfMatcher(set); + setMatcherTest(c, m); + } + + @Test + public void UnsupportedMatcher() { + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + String splitWithUndefinedMatcher = "{\"ff\":{\"s\":-1,\"t\":-1,\"d\":[]},\"rbs\":{\"s\":-1,\"t\":1457726098069,\"d\":[{ \"changeNumber\": 123, \"trafficTypeName\": \"user\", \"name\": \"some_name\"," + + "\"status\": \"ACTIVE\",\"conditions\": [{\"contitionType\": \"ROLLOUT\"," + + "\"label\": \"some_label\", \"matcherGroup\": { \"matchers\": [{ \"matcherType\": \"UNKNOWN\", \"negate\": false}]," + + "\"combiner\": \"AND\"}}],\"excluded\":{\"keys\":[],\"segments\":[]}}]}}"; + SplitChange change = Json.fromJson(splitWithUndefinedMatcher, SplitChange.class); + for (RuleBasedSegment ruleBasedSegment : change.ruleBasedSegments.d) { + // should not cause exception + ParsedRuleBasedSegment parsedRuleBasedSegment = parser.parse(ruleBasedSegment); + for (ParsedCondition parsedCondition : parsedRuleBasedSegment.parsedConditions()) { + assertTrue(parsedCondition.label() == Labels.UNSUPPORTED_MATCHER); + for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { + // Check the matcher is ALL_KEYS + assertTrue(matcher.matcher().toString().equals(" in segment all")); + } + } + } + } + + @Test + public void EqualToSemverMatcher() throws IOException { + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (RuleBasedSegment ruleBasedSegment : change.ruleBasedSegments.d) { + // should not cause exception + ParsedRuleBasedSegment parsedRuleBasedSegment = parser.parse(ruleBasedSegment); + if (ruleBasedSegment.name.equals("rbs_semver_equalto")) { + for (ParsedCondition parsedCondition : parsedRuleBasedSegment.parsedConditions()) { + assertTrue(parsedCondition.label().equals("equal to semver")); + for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { + // Check the matcher is ALL_KEYS + assertTrue(matcher.matcher().toString().equals(" == semver 1\\.22\\.9")); + return; + } + } + } + } + assertTrue(false); + } + + @Test + public void GreaterThanOrEqualSemverMatcher() throws IOException { + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (RuleBasedSegment ruleBasedSegment : change.ruleBasedSegments.d) { + // should not cause exception + ParsedRuleBasedSegment parsedRuleBasedSegment = parser.parse(ruleBasedSegment); + if (ruleBasedSegment.name.equals("rbs_semver_greater_or_equalto")) { + for (ParsedCondition parsedCondition : parsedRuleBasedSegment.parsedConditions()) { + assertTrue(parsedCondition.label().equals("greater than or equal to semver")); + for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { + // Check the matcher is ALL_KEYS + assertTrue(matcher.matcher().toString().equals(" >= semver 1\\.22\\.9")); + return; + } + } + } + } + assertTrue(false); + } + + @Test + public void LessThanOrEqualSemverMatcher() throws IOException { + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (RuleBasedSegment ruleBasedSegment : change.ruleBasedSegments.d) { + // should not cause exception + ParsedRuleBasedSegment parsedRuleBasedSegment = parser.parse(ruleBasedSegment); + if (ruleBasedSegment.name.equals("rbs_semver_less_or_equalto")) { + for (ParsedCondition parsedCondition : parsedRuleBasedSegment.parsedConditions()) { + assertTrue(parsedCondition.label().equals("less than or equal to semver")); + for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { + // Check the matcher is ALL_KEYS + assertTrue(matcher.matcher().toString().equals(" <= semver 1\\.22\\.9")); + return; + } + } + } + } + assertTrue(false); + } + + @Test + public void BetweenSemverMatcher() throws IOException { + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + RuleBasedSegmentsToUpdate ruleBasedSegmentsToUpdate = processRuleBasedSegmentChanges(parser, change.ruleBasedSegments.d); + for (ParsedRuleBasedSegment parsedRuleBasedSegment : ruleBasedSegmentsToUpdate.getToAdd()) { + // should not cause exception + if (parsedRuleBasedSegment.ruleBasedSegment().equals("rbs_semver_between")) { + for (ParsedCondition parsedCondition : parsedRuleBasedSegment.parsedConditions()) { + assertTrue(parsedCondition.label().equals("between semver")); + for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { + // Check the matcher is ALL_KEYS + assertTrue(matcher.matcher().toString().equals(" between semver 1\\.22\\.9 and 2\\.1\\.0")); + return; + } + } + } + } + assertTrue(false); + } + + @Test + public void InListSemverMatcher() throws IOException { + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (RuleBasedSegment ruleBasedSegment : change.ruleBasedSegments.d) { + // should not cause exception + ParsedRuleBasedSegment parsedRuleBasedSegment = parser.parse(ruleBasedSegment); + if (ruleBasedSegment.name.equals("rbs_semver_inlist")) { + for (ParsedCondition parsedCondition : parsedRuleBasedSegment.parsedConditions()) { + assertTrue(parsedCondition.label().equals("in list semver")); + for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) { + // Check the matcher is ALL_KEYS + assertTrue(matcher.matcher().toString().startsWith(" in semver list")); + return; + } + } + } + } + assertTrue(false); + } + + public void setMatcherTest(Condition c, io.split.engine.matchers.Matcher m) { + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); + SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + + ArrayList set = Lists.newArrayList("sms", "voice"); + List conditions = Lists.newArrayList(c); + + RuleBasedSegmentParser parser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("first.name", conditions, 1); + ParsedRuleBasedSegment actual = parser.parse(ruleBasedSegment); + + AttributeMatcher attrMatcher = new AttributeMatcher("products", m, false); + CombiningMatcher combiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(attrMatcher)); + ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, null); + List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); + + ParsedRuleBasedSegment expected = ParsedRuleBasedSegment.createParsedRuleBasedSegmentForTests ("first.name", listOfMatcherAndSplits, "user", 1, + new ArrayList<>(), new ArrayList<>()); + + Assert.assertEquals(actual, expected); + } + + private SegmentChange getSegmentChange(long since, long till, String segmentName){ + SegmentChange segmentChange = new SegmentChange(); + segmentChange.name = segmentName; + segmentChange.since = since; + segmentChange.till = till; + segmentChange.added = new ArrayList<>(); + segmentChange.removed = new ArrayList<>(); + return segmentChange; + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/experiments/SplitFetcherImpTest.java b/client/src/test/java/io/split/engine/experiments/SplitFetcherImpTest.java index 8ddab8dad..78b6eac66 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitFetcherImpTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitFetcherImpTest.java @@ -1,16 +1,33 @@ package io.split.engine.experiments; -import io.split.client.JsonLocalhostSplitChangeFetcher; +import io.split.SplitMockServer; +import io.split.client.*; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; -import io.split.client.utils.FileInputStreamProvider; -import io.split.client.utils.InputStreamProvider; +import io.split.client.interceptors.GzipDecoderResponseInterceptor; +import io.split.client.interceptors.GzipEncoderRequestInterceptor; +import io.split.client.utils.*; import io.split.engine.common.FetchOptions; +import io.split.service.SplitHttpClient; +import io.split.service.SplitHttpClientImpl; +import io.split.storages.RuleBasedSegmentCache; +import io.split.storages.RuleBasedSegmentCacheProducer; +import io.split.storages.SplitCache; import io.split.storages.SplitCacheProducer; import io.split.storages.memory.InMemoryCacheImp; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; +import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.NoopTelemetryStorage; import io.split.telemetry.storage.TelemetryStorage; +import io.split.telemetry.storage.TelemetryStorageProducer; +import okhttp3.mockwebserver.MockResponse; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.cookie.StandardCookieSpec; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.util.Timeout; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -18,8 +35,8 @@ import java.io.File; import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; +import java.net.URI; +import java.util.*; public class SplitFetcherImpTest { @@ -27,22 +44,153 @@ public class SplitFetcherImpTest { public TemporaryFolder folder = new TemporaryFolder(); private static final TelemetryStorage TELEMETRY_STORAGE_NOOP = Mockito.mock(NoopTelemetryStorage.class); - private static final String TEST_FLAG_SETS = "{\"splits\":[{\"trafficTypeName\":\"client\",\"name\":\"workm\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set_1\",\"set_2\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"client\",\"name\":\"workm_set_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set_3\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]}],\"since\":-1,\"till\":1602796638344}"; + private static final String TEST_FLAG_SETS = "{\"ff\":{\"d\":[{\"trafficTypeName\":\"client\",\"name\":\"workm\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set_1\",\"set_2\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"client\",\"name\":\"workm_set_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":147392224,\"seed\":524417105,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"on\",\"changeNumber\":1602796638344,\"algo\":2,\"configurations\":{},\"sets\":[\"set_3\"],\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_segment\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":100},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"in segment new_segment\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"client\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0},{\"treatment\":\"free\",\"size\":0},{\"treatment\":\"conta\",\"size\":0}],\"label\":\"default rule\"}]}],\"s\":-1,\"t\":1602796638344},\"rbs\":{\"d\":[],\"t\":-1,\"s\":-1}}"; + + @Test + public void testFetchingSplitsAndRuleBasedSegments() throws Exception { + MockResponse response = new MockResponse().setBody("{" + + "\"ff\":{" + + "\"t\":1675095324253," + + "\"s\":-1," + + "\"d\": [{" + + "\"changeNumber\": 123," + + "\"trafficTypeName\": \"user\"," + + "\"name\": \"some_name\"," + + "\"trafficAllocation\": 100," + + "\"trafficAllocationSeed\": 123456," + + "\"seed\": 321654," + + "\"status\": \"ACTIVE\"," + + "\"killed\": false," + + "\"defaultTreatment\": \"off\"," + + "\"algo\": 2," + + "\"conditions\": [{" + + "\"partitions\": [{\"treatment\": \"on\", \"size\": 50},{\"treatment\": \"off\", \"size\": 50}]," + + "\"contitionType\": \"WHITELIST\"," + + "\"label\": \"some_label\"," + + "\"matcherGroup\": {" + + "\"matchers\": [{\"matcherType\": \"WHITELIST\",\"whitelistMatcherData\": {\"whitelist\": [\"k1\", \"k2\", \"k3\"]},\"negate\": false}]," + + "\"combiner\": \"AND\"}" + + "},{" + + "\"conditionType\": \"ROLLOUT\"," + + "\"matcherGroup\": {\"combiner\": \"AND\"," + + "\"matchers\": [{\"keySelector\": {\"trafficType\": \"user\"},\"matcherType\": \"IN_RULE_BASED_SEGMENT\",\"negate\": false,\"userDefinedSegmentMatcherData\": {\"segmentName\": \"sample_rule_based_segment\"}}]" + + "}," + + "\"partitions\": [{\"treatment\": \"on\",\"size\": 100},{\"treatment\": \"off\",\"size\": 0}]," + + "\"label\": \"in rule based segment sample_rule_based_segment\"" + + "}]," + + "\"sets\": [\"set1\", \"set2\"]}]" + + "}," + + "\"rbs\": {" + + "\"t\": 1585948850111," + + "\"s\": -1," + + "\"d\": [" + + "{" + + "\"changeNumber\": 5," + + "\"name\": \"sample_rule_based_segment\"," + + "\"status\": \"ACTIVE\"," + + "\"trafficTypeName\": \"user\"," + + "\"excluded\":{" + + "\"keys\":[\"mauro@split.io\",\"gaston@split.io\"]," + + "\"segments\":[]" + + "}," + + "\"conditions\": [" + + "{" + + "\"matcherGroup\": {" + + "\"combiner\": \"AND\"," + + "\"matchers\": [" + + "{" + + "\"keySelector\": {" + + "\"trafficType\": \"user\"," + + "\"attribute\": \"email\"" + + "}," + + "\"matcherType\": \"ENDS_WITH\"," + + "\"negate\": false," + + "\"whitelistMatcherData\": {" + + "\"whitelist\": [" + + "\"@split.io\"" + + "]}}]}}]}]}}"); + MockResponse response2 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1675095324253, \"t\":1685095324253}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); + MockResponse response3 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1685095324253, \"t\":1695095324253}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); + MockResponse response4 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1695095324253, \"t\":1775095324253}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); + MockResponse response5 = new MockResponse().setBody("{\"ff\":{\"d\": [], \"s\":1775095324253, \"t\":1775095324253}, \"rbs\":{\"d\":[],\"s\":1585948850111,\"t\":1585948850111}}"); + Queue responses = new LinkedList<>(); + responses.add(response); + Queue responses2 = new LinkedList<>(); + responses2.add(response2); + Queue responses3 = new LinkedList<>(); + responses3.add(response3); + Queue responses4 = new LinkedList<>(); + responses4.add(response4); + Queue responses5 = new LinkedList<>(); + responses5.add(response5); + SplitMockServer splitServer = new SplitMockServer(CustomDispatcher2.builder() + .path(CustomDispatcher2.SPLIT_FETCHER_1, responses) + .path(CustomDispatcher2.SPLIT_FETCHER_2, responses2) + .path(CustomDispatcher2.SPLIT_FETCHER_3, responses3) + .path(CustomDispatcher2.SPLIT_FETCHER_4, responses4) + .path(CustomDispatcher2.SPLIT_FETCHER_5, responses5) + .build()); + splitServer.start(); + + SplitClientConfig config = SplitClientConfig.builder() + .setBlockUntilReadyTimeout(10000) + .endpoint(splitServer.getUrl(), splitServer.getUrl()) + .featuresRefreshRate(20) + .segmentsRefreshRate(30) + .streamingEnabled(false) + .build(); + + SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + FlagSetsFilter flagSetsFilter = new FlagSetsFilterImpl(config.getSetsFilter()); + SplitCache splitCache = new InMemoryCacheImp(flagSetsFilter); + RequestDecorator _requestDecorator = new RequestDecorator(config.customHeaderDecorator()); + SDKMetadata _sdkMetadata = new SDKMetadata("1.1.1", "ip", "machineName"); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(Timeout.ofMilliseconds(config.connectionTimeout())) + .setCookieSpec(StandardCookieSpec.STRICT) + .build(); + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); + TelemetryStorageProducer _telemetryStorageProducer = telemetryStorage; + + HttpClientBuilder httpClientbuilder = HttpClients.custom() + .setDefaultRequestConfig(requestConfig) + .addRequestInterceptorLast(new GzipEncoderRequestInterceptor()) + .addResponseInterceptorLast((new GzipDecoderResponseInterceptor())); + SplitHttpClient _splitHttpClient = SplitHttpClientImpl.create(httpClientbuilder.build(), + _requestDecorator, + "apiToken", + _sdkMetadata); + URI _rootTarget = URI.create(config.endpoint()); + SplitChangeFetcher splitChangeFetcher = HttpSplitChangeFetcher.create(_splitHttpClient, _rootTarget, + _telemetryStorageProducer, config.isSdkEndpointOverridden()); + SplitFetcherImp splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCache, _telemetryStorageProducer, + flagSetsFilter, ruleBasedSegmentParser, ruleBasedSegmentCache); + + splitFetcher.forceRefresh(new FetchOptions.Builder().cacheControlHeaders(false).build()); + splitServer.stop(); + Assert.assertEquals("some_name", splitCache.get("some_name").feature()); + Assert.assertEquals("sample_rule_based_segment", ruleBasedSegmentCache.get("sample_rule_based_segment").ruleBasedSegment()); + } @Test public void testLocalHost() { FlagSetsFilter flagSetsFilter = new FlagSetsFilterImpl(new HashSet<>()); SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(flagSetsFilter); + RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); InputStreamProvider inputStreamProvider = new FileInputStreamProvider("src/test/resources/split_init.json"); SplitChangeFetcher splitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); SplitParser splitParser = new SplitParser(); FetchOptions fetchOptions = new FetchOptions.Builder().build(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter, + ruleBasedSegmentParser, ruleBasedSegmentCacheProducer); FetchResult fetchResult = splitFetcher.forceRefresh(fetchOptions); - Assert.assertEquals(1, fetchResult.getSegments().size()); + Assert.assertEquals(2, fetchResult.getSegments().size()); } @Test @@ -55,11 +203,14 @@ public void testLocalHostFlagSets() throws IOException { InputStreamProvider inputStreamProvider = new FileInputStreamProvider(file.getAbsolutePath()); FlagSetsFilter flagSetsFilter = new FlagSetsFilterImpl(new HashSet<>(Arrays.asList("set_1"))); SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(flagSetsFilter); + RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SplitChangeFetcher splitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); SplitParser splitParser = new SplitParser(); FetchOptions fetchOptions = new FetchOptions.Builder().build(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter, + ruleBasedSegmentParser, ruleBasedSegmentCacheProducer); FetchResult fetchResult = splitFetcher.forceRefresh(fetchOptions); @@ -76,11 +227,14 @@ public void testLocalHostFlagSetsNotIntersect() throws IOException { InputStreamProvider inputStreamProvider = new FileInputStreamProvider(file.getAbsolutePath()); FlagSetsFilter flagSetsFilter = new FlagSetsFilterImpl(new HashSet<>(Arrays.asList("set_4"))); SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(flagSetsFilter); + RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SplitChangeFetcher splitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); SplitParser splitParser = new SplitParser(); FetchOptions fetchOptions = new FetchOptions.Builder().build(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter, + ruleBasedSegmentParser, ruleBasedSegmentCacheProducer); FetchResult fetchResult = splitFetcher.forceRefresh(fetchOptions); diff --git a/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java b/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java index 00078bb9b..a6c2468ab 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java @@ -3,10 +3,10 @@ import com.google.common.collect.Lists; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; +import io.split.storages.*; import io.split.storages.memory.InMemoryCacheImp; -import io.split.storages.SegmentCache; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; -import io.split.storages.SplitCache; import io.split.client.dtos.*; import io.split.engine.ConditionsTestUtil; import io.split.engine.common.FetchOptions; @@ -31,8 +31,6 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -71,7 +69,11 @@ public void worksWhenWeStartWithAnyState() throws InterruptedException { private void works(long startingChangeNumber) throws InterruptedException { AChangePerCallSplitChangeFetcher splitChangeFetcher = new AChangePerCallSplitChangeFetcher(); SplitCache cache = new InMemoryCacheImp(startingChangeNumber, FLAG_SETS_FILTER); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER); + RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCacheProducer); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 3, TimeUnit.SECONDS); @@ -86,7 +88,7 @@ private void works(long startingChangeNumber) throws InterruptedException { ParsedCondition expectedParsedCondition = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(ConditionsTestUtil.partition("on", 10))); List expectedListOfMatcherAndSplits = Lists.newArrayList(expectedParsedCondition); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("" + cache.getChangeNumber(), (int) cache.getChangeNumber(), false, Treatments.OFF, expectedListOfMatcherAndSplits, null, cache.getChangeNumber(), 1, new HashSet<>(), true); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("" + cache.getChangeNumber(), (int) cache.getChangeNumber(), false, Treatments.OFF, expectedListOfMatcherAndSplits, null, cache.getChangeNumber(), 1, new HashSet<>(), true, null); ParsedSplit actual = cache.get("" + cache.getChangeNumber()); Thread.sleep(1000); @@ -103,9 +105,14 @@ public void whenParserFailsWeRemoveTheExperiment() throws InterruptedException { validSplit.name = "-1"; SplitChange validReturn = new SplitChange(); - validReturn.splits = Lists.newArrayList(validSplit); - validReturn.since = -1L; - validReturn.till = 0L; + validReturn.featureFlags = new ChangeDto<>(); + validReturn.featureFlags.d = Lists.newArrayList(validSplit); + validReturn.featureFlags.s = -1L; + validReturn.featureFlags.t = 0L; + validReturn.ruleBasedSegments = new ChangeDto<>(); + validReturn.ruleBasedSegments.t = -1; + validReturn.ruleBasedSegments.s = -1; + validReturn.ruleBasedSegments.d = new ArrayList<>(); MatcherGroup invalidMatcherGroup = new MatcherGroup(); invalidMatcherGroup.matchers = Lists.newArrayList(); @@ -122,27 +129,41 @@ public void whenParserFailsWeRemoveTheExperiment() throws InterruptedException { invalidSplit.name = "-1"; SplitChange invalidReturn = new SplitChange(); - invalidReturn.splits = Lists.newArrayList(invalidSplit); - invalidReturn.since = 0L; - invalidReturn.till = 1L; + invalidReturn.featureFlags = new ChangeDto<>(); + invalidReturn.featureFlags.d = Lists.newArrayList(invalidSplit); + invalidReturn.featureFlags.s = 0L; + invalidReturn.featureFlags.t = 1L; + invalidReturn.ruleBasedSegments = new ChangeDto<>(); + invalidReturn.ruleBasedSegments.t = -1; + invalidReturn.ruleBasedSegments.s = -1; + invalidReturn.ruleBasedSegments.d = new ArrayList<>(); SplitChange noReturn = new SplitChange(); - noReturn.splits = Lists.newArrayList(); - noReturn.since = 1L; - noReturn.till = 1L; + noReturn.featureFlags = new ChangeDto<>(); + noReturn.featureFlags.d = Lists.newArrayList(); + noReturn.featureFlags.s = 1L; + noReturn.featureFlags.t = 1L; + noReturn.ruleBasedSegments = new ChangeDto<>(); + noReturn.ruleBasedSegments.t = -1; + noReturn.ruleBasedSegments.s = -1; + noReturn.ruleBasedSegments.d = new ArrayList<>(); SplitChangeFetcher splitChangeFetcher = mock(SplitChangeFetcher.class); - when(splitChangeFetcher.fetch(Mockito.eq(-1L), Mockito.any())).thenReturn(validReturn); - when(splitChangeFetcher.fetch(Mockito.eq(0L), Mockito.any())).thenReturn(invalidReturn); - when(splitChangeFetcher.fetch(Mockito.eq(1L), Mockito.any())).thenReturn(noReturn); + when(splitChangeFetcher.fetch(Mockito.eq(-1L), Mockito.eq(-1L), Mockito.any())).thenReturn(validReturn); + when(splitChangeFetcher.fetch(Mockito.eq(0L), Mockito.eq(-1L), Mockito.any())).thenReturn(invalidReturn); + when(splitChangeFetcher.fetch(Mockito.eq(1L), Mockito.eq(-1L), Mockito.any())).thenReturn(noReturn); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); SplitCache cache = new InMemoryCacheImp(-1, FLAG_SETS_FILTER); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, TELEMETRY_STORAGE, cache, null); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, TELEMETRY_STORAGE, cache, null, ruleBasedSegmentCache); segmentSynchronizationTask.start(); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCache); + // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -157,13 +178,16 @@ public void ifThereIsAProblemTalkingToSplitChangeCountDownLatchIsNotDecremented( SplitCache cache = new InMemoryCacheImp(-1, FLAG_SETS_FILTER); SplitChangeFetcher splitChangeFetcher = mock(SplitChangeFetcher.class); - when(splitChangeFetcher.fetch(-1L, new FetchOptions.Builder().build())).thenThrow(new RuntimeException()); + when(splitChangeFetcher.fetch(-1L, -1, new FetchOptions.Builder().build())).thenThrow(new RuntimeException()); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, TELEMETRY_STORAGE, cache, null); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, TELEMETRY_STORAGE, cache, null, ruleBasedSegmentCache); segmentSynchronizationTask.start(); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCache); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -186,15 +210,23 @@ public void addFeatureFlags() throws InterruptedException { featureFlag1.trafficAllocationSeed = 147392224; SplitChange validReturn = new SplitChange(); - validReturn.splits = Lists.newArrayList(featureFlag1); - validReturn.since = -1L; - validReturn.till = 0L; + validReturn.featureFlags = new ChangeDto<>(); + validReturn.featureFlags.d = Lists.newArrayList(featureFlag1); + validReturn.featureFlags.s = -1L; + validReturn.featureFlags.t = 0L; + validReturn.ruleBasedSegments = new ChangeDto<>(); + validReturn.ruleBasedSegments.t = -1; + validReturn.ruleBasedSegments.s = -1; + validReturn.ruleBasedSegments.d = new ArrayList<>(); SplitChangeFetcher splitChangeFetcher = mock(SplitChangeFetcher.class); - when(splitChangeFetcher.fetch(Mockito.eq(-1L), Mockito.any())).thenReturn(validReturn); + when(splitChangeFetcher.fetch(Mockito.eq(-1L), Mockito.eq(-1L), Mockito.any())).thenReturn(validReturn); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); FlagSetsFilter flagSetsFilter = new FlagSetsFilterImpl(new HashSet<>(Arrays.asList("set_1", "set_2"))); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, flagSetsFilter); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, flagSetsFilter, + ruleBasedSegmentParser, ruleBasedSegmentCache); executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -204,11 +236,16 @@ public void addFeatureFlags() throws InterruptedException { featureFlag1.sets.remove("set_2"); validReturn = new SplitChange(); - validReturn.splits = Lists.newArrayList(featureFlag1); - validReturn.since = 0L; - validReturn.till = 1L; + validReturn.featureFlags = new ChangeDto<>(); + validReturn.featureFlags.d = Lists.newArrayList(featureFlag1); + validReturn.featureFlags.s = 0L; + validReturn.featureFlags.t = 1L; + validReturn.ruleBasedSegments = new ChangeDto<>(); + validReturn.ruleBasedSegments.t = -1; + validReturn.ruleBasedSegments.s = -1; + validReturn.ruleBasedSegments.d = new ArrayList<>(); - when(splitChangeFetcher.fetch(Mockito.eq(0L), Mockito.any())).thenReturn(validReturn); + when(splitChangeFetcher.fetch(Mockito.eq(0L), Mockito.eq(-1L), Mockito.any())).thenReturn(validReturn); executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -243,13 +280,16 @@ public void worksWithUserDefinedSegments() throws Exception { AChangePerCallSplitChangeFetcher experimentChangeFetcher = new AChangePerCallSplitChangeFetcher(segmentName); SplitCache cache = new InMemoryCacheImp(startingChangeNumber, FLAG_SETS_FILTER); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); SegmentChange segmentChange = getSegmentChange(0L, 0L, segmentName); when(segmentChangeFetcher.fetch(anyString(), anyLong(), any())).thenReturn(segmentChange); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, Mockito.mock(TelemetryStorage.class), cache, null); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, Mockito.mock(TelemetryStorage.class), cache, null, ruleBasedSegmentCache); segmentSynchronizationTask.start(); - SplitFetcherImp fetcher = new SplitFetcherImp(experimentChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER); + SplitFetcherImp fetcher = new SplitFetcherImp(experimentChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE, FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCache); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -269,25 +309,37 @@ public void testBypassCdnClearedAfterFirstHit() { SplitChangeFetcher mockFetcher = Mockito.mock(SplitChangeFetcher.class); SplitParser mockParser = new SplitParser(); SplitCache mockCache = new InMemoryCacheImp(FLAG_SETS_FILTER); - SplitFetcherImp fetcher = new SplitFetcherImp(mockFetcher, mockParser, mockCache, Mockito.mock(TelemetryRuntimeProducer.class), FLAG_SETS_FILTER); - + RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + SplitFetcherImp fetcher = new SplitFetcherImp(mockFetcher, mockParser, mockCache, Mockito.mock(TelemetryRuntimeProducer.class), FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCacheProducer); SplitChange response1 = new SplitChange(); - response1.splits = new ArrayList<>(); - response1.since = -1; - response1.till = 1; + response1.featureFlags = new ChangeDto<>(); + response1.featureFlags.d = new ArrayList<>(); + response1.featureFlags.s = -1; + response1.featureFlags.t = 1; + response1.ruleBasedSegments = new ChangeDto<>(); + response1.ruleBasedSegments.t = -1; + response1.ruleBasedSegments.s = -1; + response1.ruleBasedSegments.d = new ArrayList<>(); SplitChange response2 = new SplitChange(); - response2.splits = new ArrayList<>(); - response2.since = 1; - response2.till = 1; - + response2.featureFlags = new ChangeDto<>(); + response2.featureFlags.d = new ArrayList<>(); + response2.featureFlags.s = 1; + response2.featureFlags.t = 1; + response2.ruleBasedSegments = new ChangeDto<>(); + response2.ruleBasedSegments.t = -1; + response2.ruleBasedSegments.s = -1; + response2.ruleBasedSegments.d = new ArrayList<>(); ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class); ArgumentCaptor cnCaptor = ArgumentCaptor.forClass(Long.class); - when(mockFetcher.fetch(cnCaptor.capture(), optionsCaptor.capture())).thenReturn(response1, response2); + ArgumentCaptor rbsCnCaptor = ArgumentCaptor.forClass(Long.class); + when(mockFetcher.fetch(cnCaptor.capture(), rbsCnCaptor.capture(), optionsCaptor.capture())).thenReturn(response1, response2); - FetchOptions originalOptions = new FetchOptions.Builder().targetChangeNumber(123).build(); + FetchOptions originalOptions = new FetchOptions.Builder().targetChangeNumber(123).targetChangeNumberRBS(-1).build(); fetcher.forceRefresh(originalOptions); List capturedCNs = cnCaptor.getAllValues(); List capturedOptions = optionsCaptor.getAllValues(); diff --git a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java index 7c9b9cbab..4676a8c3b 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java @@ -11,11 +11,7 @@ import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; import io.split.client.dtos.Status; -import io.split.storages.SegmentCache; -import io.split.storages.memory.SegmentCacheInMemoryImpl; -import io.split.client.utils.Json; -import io.split.engine.evaluator.Labels; -import io.split.engine.ConditionsTestUtil; +import io.split.engine.matchers.PrerequisitesMatcher; import io.split.engine.matchers.AttributeMatcher; import io.split.engine.matchers.BetweenMatcher; import io.split.engine.matchers.CombiningMatcher; @@ -23,6 +19,11 @@ import io.split.engine.matchers.GreaterThanOrEqualToMatcher; import io.split.engine.matchers.LessThanOrEqualToMatcher; import io.split.engine.matchers.UserDefinedSegmentMatcher; +import io.split.storages.SegmentCache; +import io.split.storages.memory.SegmentCacheInMemoryImpl; +import io.split.client.utils.Json; +import io.split.engine.evaluator.Labels; +import io.split.engine.ConditionsTestUtil; import io.split.engine.matchers.collections.ContainsAllOfSetMatcher; import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; import io.split.engine.matchers.collections.EqualToSetMatcher; @@ -36,7 +37,6 @@ import org.junit.Test; import org.mockito.Mockito; -import javax.validation.constraints.AssertTrue; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -95,11 +95,12 @@ public void works() { ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, new HashSet<>(), false); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, null, false, new PrerequisitesMatcher(null)); - Assert.assertEquals(actual, expected); + compareParsed(actual, expected); assertTrue(expected.hashCode() != 0); assertTrue(expected.equals(expected)); + Assert.assertEquals(expected.toString(), actual.toString()); } @Test @@ -138,9 +139,23 @@ public void worksWithConfig() { List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, - listOfMatcherAndSplits, "user", 1, 1, configurations, new HashSet<>(), false); + listOfMatcherAndSplits, "user", 1, 1, configurations, new HashSet<>(), false, new PrerequisitesMatcher(null)); + + Assert.assertEquals(actual.parsedConditions(), expected.parsedConditions()); + Assert.assertEquals(actual.feature(), expected.feature()); + Assert.assertEquals(actual.changeNumber(), expected.changeNumber()); + Assert.assertEquals(actual.defaultTreatment(), expected.defaultTreatment()); + Assert.assertEquals(actual.killed(), expected.killed()); + Assert.assertEquals(actual.impressionsDisabled(), expected.impressionsDisabled()); + Assert.assertEquals(null, actual.flagSets()); + Assert.assertEquals(actual.algo(), expected.algo()); + Assert.assertEquals(actual.seed(), expected.seed()); + Assert.assertEquals(actual.trafficAllocation(), expected.trafficAllocation()); + Assert.assertEquals(actual.trafficAllocationSeed(), expected.trafficAllocationSeed()); + Assert.assertEquals(actual.getSegmentsNames(), expected.getSegmentsNames()); + Assert.assertEquals(actual.getRuleBasedSegmentsNames(), expected.getRuleBasedSegmentsNames()); + Assert.assertEquals(actual.prerequisitesMatcher().toString(), expected.prerequisitesMatcher().toString()); - Assert.assertEquals(actual, expected); Assert.assertEquals(actual.configurations().get("on"), configurations.get("on")); } @@ -174,12 +189,12 @@ public void worksForTwoConditions() { ParsedSplit actual = parser.parse(split); ParsedCondition parsedCondition1 = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new UserDefinedSegmentMatcher(EMPLOYEES)), fullyRollout); - ParsedCondition parsedCondition2 = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new UserDefinedSegmentMatcher(EMPLOYEES)), turnOff); + ParsedCondition parsedCondition2 = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new UserDefinedSegmentMatcher(SALES_PEOPLE)), turnOff); List listOfParsedConditions = Lists.newArrayList(parsedCondition1, parsedCondition2); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfParsedConditions, "user", 1, 1, new HashSet<>(), false); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfParsedConditions, "user", 1, 1, null, false, new PrerequisitesMatcher(null)); - Assert.assertEquals(actual, expected); + compareParsed(actual, expected); } @Test @@ -246,9 +261,9 @@ public void worksWithAttributes() { ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, new HashSet<>(), false); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, null, false, new PrerequisitesMatcher(null)); - Assert.assertEquals(actual, expected); + compareParsed(actual, expected); } @Test @@ -279,9 +294,9 @@ public void lessThanOrEqualTo() { ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, new HashSet<>(), false); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, null, false, new PrerequisitesMatcher(null)); - Assert.assertEquals(actual, expected); + compareParsed(actual, expected); } @Test @@ -311,9 +326,9 @@ public void equalTo() { ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, new HashSet<>(), false); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, null, false, new PrerequisitesMatcher(null)); - Assert.assertEquals(actual, expected); + compareParsed(actual, expected); } @Test @@ -342,9 +357,9 @@ public void equalToNegativeNumber() { ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, new HashSet<>(), false); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, null, false, new PrerequisitesMatcher(null)); - Assert.assertEquals(actual, expected); + compareParsed(actual, expected); } @Test @@ -378,9 +393,9 @@ public void between() { ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, new HashSet<>(), false); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("first.name", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, null, false, new PrerequisitesMatcher(null)); - Assert.assertEquals(actual, expected); + compareParsed(actual, expected); } @Test @@ -513,14 +528,14 @@ public void containsString() { @Test public void UnsupportedMatcher() { SplitParser parser = new SplitParser(); - String splitWithUndefinedMatcher = "{\"since\":-1,\"till\": 1457726098069,\"splits\": [{ \"changeNumber\": 123, \"trafficTypeName\": \"user\", \"name\": \"some_name\"," + String splitWithUndefinedMatcher = "{\"ff\":{\"s\":-1,\"t\": 1457726098069,\"d\": [{ \"changeNumber\": 123, \"trafficTypeName\": \"user\", \"name\": \"some_name\"," + "\"trafficAllocation\": 100, \"trafficAllocationSeed\": 123456, \"seed\": 321654, \"status\": \"ACTIVE\"," + "\"killed\": false, \"defaultTreatment\": \"off\", \"algo\": 2,\"conditions\": [{ \"partitions\": [" + "{\"treatment\": \"on\", \"size\": 50}, {\"treatment\": \"off\", \"size\": 50}], \"contitionType\": \"ROLLOUT\"," + "\"label\": \"some_label\", \"matcherGroup\": { \"matchers\": [{ \"matcherType\": \"UNKNOWN\", \"negate\": false}]," - + "\"combiner\": \"AND\"}}], \"sets\": [\"set1\"]}]}"; + + "\"combiner\": \"AND\"}}], \"sets\": [\"set1\"]}]}, \"rbs\":{\"s\":-1,\"t\":-1,\"d\":[]}}"; SplitChange change = Json.fromJson(splitWithUndefinedMatcher, SplitChange.class); - for (Split split : change.splits) { + for (Split split : change.featureFlags.d) { // should not cause exception ParsedSplit parsedSplit = parser.parse(split); for (ParsedCondition parsedCondition : parsedSplit.parsedConditions()) { @@ -536,9 +551,9 @@ public void UnsupportedMatcher() { @Test public void EqualToSemverMatcher() throws IOException { SplitParser parser = new SplitParser(); - String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); - SplitChange change = Json.fromJson(splits, SplitChange.class); - for (Split split : change.splits) { + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (Split split : change.featureFlags.d) { // should not cause exception ParsedSplit parsedSplit = parser.parse(split); if (split.name.equals("semver_equalto")) { @@ -558,9 +573,9 @@ public void EqualToSemverMatcher() throws IOException { @Test public void GreaterThanOrEqualSemverMatcher() throws IOException { SplitParser parser = new SplitParser(); - String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); - SplitChange change = Json.fromJson(splits, SplitChange.class); - for (Split split : change.splits) { + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (Split split : change.featureFlags.d) { // should not cause exception ParsedSplit parsedSplit = parser.parse(split); if (split.name.equals("semver_greater_or_equalto")) { @@ -580,9 +595,9 @@ public void GreaterThanOrEqualSemverMatcher() throws IOException { @Test public void LessThanOrEqualSemverMatcher() throws IOException { SplitParser parser = new SplitParser(); - String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); - SplitChange change = Json.fromJson(splits, SplitChange.class); - for (Split split : change.splits) { + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (Split split : change.featureFlags.d) { // should not cause exception ParsedSplit parsedSplit = parser.parse(split); if (split.name.equals("semver_less_or_equalto")) { @@ -602,9 +617,9 @@ public void LessThanOrEqualSemverMatcher() throws IOException { @Test public void BetweenSemverMatcher() throws IOException { SplitParser parser = new SplitParser(); - String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); - SplitChange change = Json.fromJson(splits, SplitChange.class); - for (Split split : change.splits) { + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (Split split : change.featureFlags.d) { // should not cause exception ParsedSplit parsedSplit = parser.parse(split); if (split.name.equals("semver_between")) { @@ -624,9 +639,9 @@ public void BetweenSemverMatcher() throws IOException { @Test public void InListSemverMatcher() throws IOException { SplitParser parser = new SplitParser(); - String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); - SplitChange change = Json.fromJson(splits, SplitChange.class); - for (Split split : change.splits) { + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/semver/semver-splits.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); + for (Split split : change.featureFlags.d) { // should not cause exception ParsedSplit parsedSplit = parser.parse(split); if (split.name.equals("semver_inlist")) { @@ -646,10 +661,10 @@ public void InListSemverMatcher() throws IOException { @Test public void ImpressionToggleParseTest() throws IOException { SplitParser parser = new SplitParser(); - String splits = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_imp_toggle.json")), StandardCharsets.UTF_8); - SplitChange change = Json.fromJson(splits, SplitChange.class); + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/splits_imp_toggle.json")), StandardCharsets.UTF_8); + SplitChange change = Json.fromJson(load, SplitChange.class); boolean check1 = false, check2 = false, check3 = false; - for (Split split : change.splits) { + for (Split split : change.featureFlags.d) { ParsedSplit parsedSplit = parser.parse(split); if (split.name.equals("without_impression_toggle")) { assertFalse(parsedSplit.impressionsDisabled()); @@ -694,9 +709,27 @@ public void setMatcherTest(Condition c, io.split.engine.matchers.Matcher m) { ParsedCondition parsedCondition = ParsedCondition.createParsedConditionForTests(combiningMatcher, partitions); List listOfMatcherAndSplits = Lists.newArrayList(parsedCondition); - ParsedSplit expected = ParsedSplit.createParsedSplitForTests("splitName", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, new HashSet<>(), false); + ParsedSplit expected = ParsedSplit.createParsedSplitForTests("splitName", 123, false, Treatments.OFF, listOfMatcherAndSplits, "user", 1, 1, null, false, new PrerequisitesMatcher(null)); + + compareParsed(actual, expected); + } - Assert.assertEquals(actual, expected); + private void compareParsed(ParsedSplit actual, ParsedSplit expected) { + Assert.assertEquals(actual.getRuleBasedSegmentsNames(), expected.getRuleBasedSegmentsNames()); + Assert.assertEquals(actual.seed(), expected.seed()); + Assert.assertEquals(actual.algo(), expected.algo()); + Assert.assertEquals(actual.trafficAllocationSeed(), expected.trafficAllocationSeed()); + Assert.assertEquals(actual.flagSets(), expected.flagSets()); + Assert.assertEquals(actual.parsedConditions(), expected.parsedConditions()); + Assert.assertEquals(actual.trafficAllocation(), expected.trafficAllocation()); + Assert.assertEquals(actual.getSegmentsNames(), expected.getSegmentsNames()); + Assert.assertEquals(actual.impressionsDisabled(), expected.impressionsDisabled()); + Assert.assertEquals(actual.killed(), expected.killed()); + Assert.assertEquals(actual.defaultTreatment(), expected.defaultTreatment()); + Assert.assertEquals(actual.changeNumber(), expected.changeNumber()); + Assert.assertEquals(actual.feature(), expected.feature()); + Assert.assertEquals(actual.configurations(), expected.configurations()); + Assert.assertEquals(actual.prerequisitesMatcher().toString(), expected.prerequisitesMatcher().toString()); } private Split makeSplit(String name, int seed, List conditions, long changeNumber) { @@ -716,6 +749,7 @@ private Split makeSplit(String name, int seed, List conditions, long split.changeNumber = changeNumber; split.algo = 1; split.configurations = configurations; + split.prerequisites = new ArrayList<>(); return split; } diff --git a/client/src/test/java/io/split/engine/experiments/SplitSynchronizationTaskTest.java b/client/src/test/java/io/split/engine/experiments/SplitSynchronizationTaskTest.java index ca7ceab77..ce04294cb 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitSynchronizationTaskTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitSynchronizationTaskTest.java @@ -4,8 +4,10 @@ import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; import io.split.engine.common.FetchOptions; +import io.split.storages.RuleBasedSegmentCacheProducer; import io.split.storages.SplitCacheProducer; import io.split.storages.memory.InMemoryCacheImp; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.telemetry.storage.NoopTelemetryStorage; import io.split.telemetry.storage.TelemetryStorage; import org.junit.Test; @@ -25,7 +27,10 @@ public void testLocalhost() throws InterruptedException { SplitChangeFetcher splitChangeFetcher = Mockito.mock(JsonLocalhostSplitChangeFetcher.class); SplitParser splitParser = new SplitParser(); FetchOptions fetchOptions = new FetchOptions.Builder().build(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, FLAG_SETS_FILTER); + RuleBasedSegmentCacheProducer ruleBasedSegmentCacheProducer = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, FLAG_SETS_FILTER, + ruleBasedSegmentParser, ruleBasedSegmentCacheProducer); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000, null); @@ -33,7 +38,7 @@ public void testLocalhost() throws InterruptedException { Thread.sleep(2000); - Mockito.verify(splitChangeFetcher, Mockito.times(1)).fetch(-1, fetchOptions); + Mockito.verify(splitChangeFetcher, Mockito.times(1)).fetch(-1, -1, fetchOptions); } @Test diff --git a/client/src/test/java/io/split/engine/matchers/NegatableMatcherTest.java b/client/src/test/java/io/split/engine/matchers/NegatableMatcherTest.java index 97e4aebe2..f80f38739 100644 --- a/client/src/test/java/io/split/engine/matchers/NegatableMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/NegatableMatcherTest.java @@ -4,7 +4,9 @@ import io.split.engine.evaluator.EvaluationContext; import io.split.engine.evaluator.Evaluator; import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SegmentCache; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; import org.junit.Assert; import org.junit.Test; @@ -26,19 +28,20 @@ public void worksAllKeys() { AllKeysMatcher delegate = new AllKeysMatcher(); AttributeMatcher.NegatableMatcher matcher = new AttributeMatcher.NegatableMatcher(delegate, true); - test(matcher, "foo", false, Mockito.mock(SegmentCache.class)); + test(matcher, "foo", false, Mockito.mock(SegmentCache.class), Mockito.mock(RuleBasedSegmentCache.class)); } @Test public void worksSegment() { SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); segmentCache.updateSegment("foo", Stream.of("a","b").collect(Collectors.toList()), new ArrayList<>(), 1L); UserDefinedSegmentMatcher delegate = new UserDefinedSegmentMatcher("foo"); AttributeMatcher.NegatableMatcher matcher = new AttributeMatcher.NegatableMatcher(delegate, true); - test(matcher, "a", false, segmentCache); - test(matcher, "b", false, segmentCache); - test(matcher, "c", true, segmentCache); + test(matcher, "a", false, segmentCache, ruleBasedSegmentCache); + test(matcher, "b", false, segmentCache, ruleBasedSegmentCache); + test(matcher, "c", true, segmentCache, ruleBasedSegmentCache); } @Test @@ -46,14 +49,14 @@ public void worksWhitelist() { WhitelistMatcher delegate = new WhitelistMatcher(Lists.newArrayList("a", "b")); AttributeMatcher.NegatableMatcher matcher = new AttributeMatcher.NegatableMatcher(delegate, true); - test(matcher, "a", false, Mockito.mock(SegmentCache.class)); - test(matcher, "b", false, Mockito.mock(SegmentCache.class)); - test(matcher, "c", true, Mockito.mock(SegmentCache.class)); + test(matcher, "a", false, Mockito.mock(SegmentCache.class), Mockito.mock(RuleBasedSegmentCache.class)); + test(matcher, "b", false, Mockito.mock(SegmentCache.class), Mockito.mock(RuleBasedSegmentCache.class)); + test(matcher, "c", true, Mockito.mock(SegmentCache.class), Mockito.mock(RuleBasedSegmentCache.class)); } - private void test(AttributeMatcher.NegatableMatcher negationMatcher, String key, boolean expected, SegmentCache segmentCache) { - Assert.assertEquals(expected, negationMatcher.match(key, null, null, new EvaluationContext(Mockito.mock(Evaluator.class), segmentCache))); - Assert.assertNotEquals(expected, negationMatcher.delegate().match(key, null, null, new EvaluationContext(Mockito.mock(Evaluator.class), segmentCache))); + private void test(AttributeMatcher.NegatableMatcher negationMatcher, String key, boolean expected, SegmentCache segmentCache, RuleBasedSegmentCache ruleBasedSegmentCache) { + Assert.assertEquals(expected, negationMatcher.match(key, null, null, new EvaluationContext(Mockito.mock(Evaluator.class), segmentCache, ruleBasedSegmentCache))); + Assert.assertNotEquals(expected, negationMatcher.delegate().match(key, null, null, new EvaluationContext(Mockito.mock(Evaluator.class), segmentCache, ruleBasedSegmentCache))); } diff --git a/client/src/test/java/io/split/engine/matchers/PrerequisitesMatcherTest.java b/client/src/test/java/io/split/engine/matchers/PrerequisitesMatcherTest.java new file mode 100644 index 000000000..4fe92d045 --- /dev/null +++ b/client/src/test/java/io/split/engine/matchers/PrerequisitesMatcherTest.java @@ -0,0 +1,55 @@ +package io.split.engine.matchers; + +import io.split.client.dtos.Prerequisites; +import io.split.client.utils.Json; +import io.split.engine.evaluator.EvaluationContext; +import io.split.engine.evaluator.Evaluator; +import io.split.engine.evaluator.EvaluatorImp; +import io.split.storages.RuleBasedSegmentCache; +import io.split.storages.SegmentCache; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.List; + +/** + * Tests for Prerequisites matcher + */ +public class PrerequisitesMatcherTest { + + @Test + public void works() { + Evaluator evaluator = Mockito.mock(Evaluator.class); + EvaluationContext evaluationContext = new EvaluationContext(evaluator, Mockito.mock(SegmentCache.class), Mockito.mock(RuleBasedSegmentCache.class)); + List prerequisites = Arrays.asList(Json.fromJson("{\"n\": \"split1\", \"ts\": [\"on\"]}", Prerequisites.class), Json.fromJson("{\"n\": \"split2\", \"ts\": [\"off\"]}", Prerequisites.class)); + PrerequisitesMatcher matcher = new PrerequisitesMatcher(prerequisites); + Assert.assertEquals("prerequisites: split1 [on], split2 [off]", matcher.toString()); + PrerequisitesMatcher matcher2 = new PrerequisitesMatcher(prerequisites); + Assert.assertTrue(matcher.equals(matcher2)); + Assert.assertTrue(matcher.hashCode() != 0); + + Mockito.when(evaluator.evaluateFeature("user", "user", "split1", null)).thenReturn(new EvaluatorImp.TreatmentLabelAndChangeNumber("on", "")); + Mockito.when(evaluator.evaluateFeature("user", "user", "split2", null)).thenReturn(new EvaluatorImp.TreatmentLabelAndChangeNumber("off", "")); + Assert.assertTrue(matcher.match("user", "user", null, evaluationContext)); + + Mockito.when(evaluator.evaluateFeature("user", "user", "split2", null)).thenReturn(new EvaluatorImp.TreatmentLabelAndChangeNumber("on", "")); + Assert.assertFalse(matcher.match("user", "user", null, evaluationContext)); + } + + @Test + public void invalidParams() { + Evaluator evaluator = Mockito.mock(Evaluator.class); + EvaluationContext evaluationContext = new EvaluationContext(evaluator, Mockito.mock(SegmentCache.class), Mockito.mock(RuleBasedSegmentCache.class)); + + List prerequisites = Arrays.asList(Json.fromJson("{\"n\": \"split1\", \"ts\": [\"on\"]}", Prerequisites.class), Json.fromJson("{\"n\": \"split2\", \"ts\": [\"off\"]}", Prerequisites.class)); + PrerequisitesMatcher matcher = new PrerequisitesMatcher(prerequisites); + Mockito.when(evaluator.evaluateFeature("user", "user", "split1", null)).thenReturn(new EvaluatorImp.TreatmentLabelAndChangeNumber("on", "")); + Assert.assertFalse(matcher.match(null, null, null, evaluationContext)); + Assert.assertFalse(matcher.match(123, null, null, evaluationContext)); + + matcher = new PrerequisitesMatcher(null); + Assert.assertFalse(matcher.match(123, null, null, evaluationContext)); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/matchers/RuleBasedSegmentMatcherTest.java b/client/src/test/java/io/split/engine/matchers/RuleBasedSegmentMatcherTest.java new file mode 100644 index 000000000..7d5d0c48b --- /dev/null +++ b/client/src/test/java/io/split/engine/matchers/RuleBasedSegmentMatcherTest.java @@ -0,0 +1,137 @@ +package io.split.engine.matchers; + +import com.google.common.collect.Lists; +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.MatcherCombiner; +import io.split.client.dtos.SplitChange; +import io.split.client.utils.Json; +import io.split.client.utils.RuleBasedSegmentsToUpdate; +import io.split.engine.evaluator.EvaluationContext; +import io.split.engine.evaluator.Evaluator; +import io.split.engine.experiments.ParsedCondition; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.engine.experiments.RuleBasedSegmentParser; +import io.split.engine.matchers.strings.WhitelistMatcher; +import io.split.storages.RuleBasedSegmentCache; +import io.split.storages.SegmentCache; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; +import io.split.storages.memory.SegmentCacheInMemoryImpl; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import static io.split.client.utils.RuleBasedSegmentProcessor.processRuleBasedSegmentChanges; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class RuleBasedSegmentMatcherTest { + @Test + public void works() { + Evaluator evaluator = Mockito.mock(Evaluator.class); + SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + EvaluationContext evaluationContext = new EvaluationContext(evaluator, segmentCache, ruleBasedSegmentCache); + AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); + + AttributeMatcher ruleBasedSegmentMatcher = AttributeMatcher.vanilla(new RuleBasedSegmentMatcher("sample_rule_based_segment")); + CombiningMatcher ruleBasedSegmentCombinerMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(ruleBasedSegmentMatcher)); + ParsedCondition ruleBasedSegmentCondition = new ParsedCondition(ConditionType.ROLLOUT, ruleBasedSegmentCombinerMatcher, null, "test rbs rule"); + ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("sample_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, "whitelist label")),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), Lists.newArrayList()); + ruleBasedSegmentCache.update(Lists.newArrayList(parsedRuleBasedSegment), null, 123); + + RuleBasedSegmentMatcher matcher = new RuleBasedSegmentMatcher("sample_rule_based_segment"); + + assertThat(matcher.match("mauro@test.io", null, null, evaluationContext), is(false)); + assertThat(matcher.match("admin", null, null, evaluationContext), is(true)); + + assertThat(matcher.match("foo", null, null, evaluationContext), is(false)); + assertThat(matcher.match(null, null, null, evaluationContext), is(false)); + } + + @Test + public void usingRbsInConditionTest() throws IOException { + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/rule_base_segments.json")), StandardCharsets.UTF_8); + Evaluator evaluator = Mockito.mock(Evaluator.class); + SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + EvaluationContext evaluationContext = new EvaluationContext(evaluator, segmentCache, ruleBasedSegmentCache); + + SplitChange change = Json.fromJson(load, SplitChange.class); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + RuleBasedSegmentsToUpdate ruleBasedSegmentsToUpdate = processRuleBasedSegmentChanges(ruleBasedSegmentParser, + change.ruleBasedSegments.d); + ruleBasedSegmentCache.update(ruleBasedSegmentsToUpdate.getToAdd(), null, 123); + RuleBasedSegmentMatcher matcher = new RuleBasedSegmentMatcher("dependent_rbs"); + HashMap attrib1 = new HashMap() {{ + put("email", "mauro@@split.io"); + }}; + HashMap attrib2 = new HashMap() {{ + put("email", "bilal@@split.io"); + }}; + assertThat(matcher.match("mauro@split.io", null, attrib1, evaluationContext), is(false)); + assertThat(matcher.match("bilal@split.io", null, attrib2, evaluationContext), is(true)); + } + + @Test + public void usingSegmentInExcludedTest() throws IOException { + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/rule_base_segments3.json")), StandardCharsets.UTF_8); + Evaluator evaluator = Mockito.mock(Evaluator.class); + SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + segmentCache.updateSegment("segment1", Arrays.asList("bilal@split.io"), new ArrayList<>(), 123); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + EvaluationContext evaluationContext = new EvaluationContext(evaluator, segmentCache, ruleBasedSegmentCache); + + SplitChange change = Json.fromJson(load, SplitChange.class); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + RuleBasedSegmentsToUpdate ruleBasedSegmentsToUpdate = processRuleBasedSegmentChanges(ruleBasedSegmentParser, + change.ruleBasedSegments.d); + ruleBasedSegmentCache.update(ruleBasedSegmentsToUpdate.getToAdd(), null, 123); + RuleBasedSegmentMatcher matcher = new RuleBasedSegmentMatcher("sample_rule_based_segment"); + HashMap attrib1 = new HashMap() {{ + put("email", "mauro@split.io"); + }}; + HashMap attrib2 = new HashMap() {{ + put("email", "bilal@split.io"); + }}; + HashMap attrib3 = new HashMap() {{ + put("email", "pato@split.io"); + }}; + assertThat(matcher.match("mauro@split.io", null, attrib1, evaluationContext), is(false)); + assertThat(matcher.match("bilal@split.io", null, attrib2, evaluationContext), is(false)); + assertThat(matcher.match("pato@split.io", null, attrib3, evaluationContext), is(true)); + } + + @Test + public void usingRbsInExcludedTest() throws IOException { + String load = new String(Files.readAllBytes(Paths.get("src/test/resources/rule_base_segments2.json")), StandardCharsets.UTF_8); + Evaluator evaluator = Mockito.mock(Evaluator.class); + SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + EvaluationContext evaluationContext = new EvaluationContext(evaluator, segmentCache, ruleBasedSegmentCache); + + SplitChange change = Json.fromJson(load, SplitChange.class); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + RuleBasedSegmentsToUpdate ruleBasedSegmentsToUpdate = processRuleBasedSegmentChanges(ruleBasedSegmentParser, + change.ruleBasedSegments.d); + ruleBasedSegmentCache.update(ruleBasedSegmentsToUpdate.getToAdd(), null, 123); + RuleBasedSegmentMatcher matcher = new RuleBasedSegmentMatcher("sample_rule_based_segment"); + HashMap attrib1 = new HashMap() {{ + put("email", "mauro@split.io"); + }}; + HashMap attrib2 = new HashMap() {{ + put("email", "bilal@harness.io"); + }}; + assertThat(matcher.match("mauro", null, attrib1, evaluationContext), is(false)); + assertThat(matcher.match("bilal", null, attrib2, evaluationContext), is(true)); + } +} diff --git a/client/src/test/java/io/split/engine/matchers/UserDefinedSegmentMatcherTest.java b/client/src/test/java/io/split/engine/matchers/UserDefinedSegmentMatcherTest.java index 0d59e3c54..b957f73d0 100644 --- a/client/src/test/java/io/split/engine/matchers/UserDefinedSegmentMatcherTest.java +++ b/client/src/test/java/io/split/engine/matchers/UserDefinedSegmentMatcherTest.java @@ -3,7 +3,10 @@ import com.google.common.collect.Sets; import io.split.engine.evaluator.EvaluationContext; import io.split.engine.evaluator.Evaluator; +import io.split.storages.RuleBasedSegmentCacheConsumer; +import io.split.storages.RuleBasedSegmentCacheProducer; import io.split.storages.SegmentCache; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; import org.junit.Test; import org.mockito.Mockito; @@ -27,7 +30,8 @@ public void works() { Set keys = Sets.newHashSet("a", "b"); Evaluator evaluator = Mockito.mock(Evaluator.class); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); - EvaluationContext evaluationContext = new EvaluationContext(evaluator, segmentCache); + RuleBasedSegmentCacheConsumer ruleBasedSegmentCacheConsumer = new RuleBasedSegmentCacheInMemoryImp(); + EvaluationContext evaluationContext = new EvaluationContext(evaluator, segmentCache, ruleBasedSegmentCacheConsumer); segmentCache.updateSegment("foo", Stream.of("a","b").collect(Collectors.toList()), new ArrayList<>(), 1L); UserDefinedSegmentMatcher matcher = new UserDefinedSegmentMatcher("foo"); diff --git a/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java b/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java index 027d09f5f..f6f7f04f4 100644 --- a/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java +++ b/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java @@ -1,6 +1,7 @@ package io.split.engine.segments; import com.google.common.collect.Maps; +import io.split.Spec; import io.split.client.LocalhostSegmentChangeFetcher; import io.split.client.JsonLocalhostSplitChangeFetcher; import io.split.client.interceptors.FlagSetsFilter; @@ -8,21 +9,17 @@ import io.split.client.utils.InputStreamProvider; import io.split.client.utils.StaticContentInputStreamProvider; import io.split.engine.common.FetchOptions; -import io.split.engine.experiments.SplitChangeFetcher; -import io.split.engine.experiments.SplitFetcher; -import io.split.engine.experiments.SplitFetcherImp; -import io.split.engine.experiments.SplitParser; -import io.split.engine.experiments.SplitSynchronizationTask; -import io.split.storages.SegmentCacheProducer; -import io.split.storages.SplitCache; -import io.split.storages.SplitCacheConsumer; +import io.split.engine.experiments.*; +import io.split.storages.*; import io.split.storages.memory.InMemoryCacheImp; +import io.split.storages.memory.RuleBasedSegmentCacheInMemoryImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.NoopTelemetryStorage; import io.split.telemetry.storage.TelemetryStorage; import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; import org.slf4j.Logger; @@ -68,7 +65,7 @@ public void works() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, segmentCacheProducer, - TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null); + TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null, Mockito.mock(RuleBasedSegmentCache.class)); // create two tasks that will separately call segment and make sure @@ -113,7 +110,7 @@ public void testFetchAllAsynchronousAndGetFalse() throws NoSuchFieldException, I SegmentFetcherImp segmentFetcher = Mockito.mock(SegmentFetcherImp.class); _segmentFetchers.put("SF", segmentFetcher); final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, - segmentCacheProducer, TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null); + segmentCacheProducer, TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null, Mockito.mock(RuleBasedSegmentCache.class)); Mockito.when(segmentFetcher.runWhitCacheHeader()).thenReturn(false); Mockito.when(segmentFetcher.fetch(Mockito.anyObject())).thenReturn(false); @@ -137,7 +134,7 @@ public void testFetchAllAsynchronousAndGetTrue() throws NoSuchFieldException, Il SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentFetcherImp segmentFetcher = Mockito.mock(SegmentFetcherImp.class); final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, segmentCacheProducer, - TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null); + TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null, Mockito.mock(RuleBasedSegmentCache.class)); // Before executing, we'll update the map of segmentFecthers via reflection. Field segmentFetchersForced = SegmentSynchronizationTaskImp.class.getDeclaredField("_segmentFetchers"); @@ -162,7 +159,11 @@ public void testLocalhostSegmentChangeFetcher() throws InterruptedException, Fil SplitChangeFetcher splitChangeFetcher = new JsonLocalhostSplitChangeFetcher(inputStreamProvider); SplitParser splitParser = new SplitParser(); FetchOptions fetchOptions = new FetchOptions.Builder().build(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter); + RuleBasedSegmentCache ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP, flagSetsFilter, + ruleBasedSegmentParser, ruleBasedSegmentCache); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000, null); @@ -174,12 +175,13 @@ public void testLocalhostSegmentChangeFetcher() throws InterruptedException, Fil SegmentCacheProducer segmentCacheProducer = new SegmentCacheInMemoryImpl(); SegmentSynchronizationTaskImp segmentSynchronizationTaskImp = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1000, 1, segmentCacheProducer, - TELEMETRY_STORAGE_NOOP, splitCacheProducer, null); + TELEMETRY_STORAGE_NOOP, splitCacheProducer, null, ruleBasedSegmentCache); segmentSynchronizationTaskImp.start(); Thread.sleep(2000); Mockito.verify(segmentChangeFetcher, Mockito.times(1)).fetch("segment_1",-1, fetchOptions); + Mockito.verify(segmentChangeFetcher, Mockito.times(1)).fetch("segment_2",-1, fetchOptions); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/CommonChangeNotificationTest.java b/client/src/test/java/io/split/engine/sse/CommonChangeNotificationTest.java new file mode 100644 index 000000000..f536870c1 --- /dev/null +++ b/client/src/test/java/io/split/engine/sse/CommonChangeNotificationTest.java @@ -0,0 +1,46 @@ +package io.split.engine.sse; + +import io.split.client.dtos.RuleBasedSegment; +import io.split.client.dtos.Split; +import io.split.client.dtos.Status; +import io.split.client.utils.Json; +import io.split.engine.sse.dtos.CommonChangeNotification; +import io.split.engine.sse.dtos.GenericNotificationData; +import io.split.engine.sse.dtos.IncomingNotification; +import io.split.engine.sse.dtos.RawMessageNotification; +import io.split.engine.sse.enums.CompressType; +import org.junit.Assert; +import org.junit.Test; + +public class CommonChangeNotificationTest { + + @Test + public void testFeatureFlagNotification() { + String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":2,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; + RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); + GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); + + CommonChangeNotification featureFlagChangeNotification = new CommonChangeNotification(genericNotificationData, Split.class); + Assert.assertEquals(IncomingNotification.Type.SPLIT_UPDATE, featureFlagChangeNotification.getType()); + Assert.assertEquals(1684265694505L, featureFlagChangeNotification.getChangeNumber()); + Assert.assertEquals(CompressType.ZLIB, featureFlagChangeNotification.getCompressType()); + Assert.assertEquals(0L, featureFlagChangeNotification.getPreviousChangeNumber()); + Assert.assertEquals("mauro_java", featureFlagChangeNotification.getDefinition().name); + Assert.assertEquals(-1769377604, featureFlagChangeNotification.getDefinition().seed); + } + + @Test + public void testRuleBasedSegmentNotification() { + String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"RB_SEGMENT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":0,\\\"d\\\":\\\"eyJjaGFuZ2VOdW1iZXIiOiA1LCAibmFtZSI6ICJzYW1wbGVfcnVsZV9iYXNlZF9zZWdtZW50IiwgInN0YXR1cyI6ICJBQ1RJVkUiLCAidHJhZmZpY1R5cGVOYW1lIjogInVzZXIiLCAiZXhjbHVkZWQiOiB7ImtleXMiOiBbIm1hdXJvQHNwbGl0LmlvIiwgImdhc3RvbkBzcGxpdC5pbyJdLCAic2VnbWVudHMiOiBbXX0sICJjb25kaXRpb25zIjogW3sibWF0Y2hlckdyb3VwIjogeyJjb21iaW5lciI6ICJBTkQiLCAibWF0Y2hlcnMiOiBbeyJrZXlTZWxlY3RvciI6IHsidHJhZmZpY1R5cGUiOiAidXNlciIsICJhdHRyaWJ1dGUiOiAiZW1haWwifSwgIm1hdGNoZXJUeXBlIjogIkVORFNfV0lUSCIsICJuZWdhdGUiOiBmYWxzZSwgIndoaXRlbGlzdE1hdGNoZXJEYXRhIjogeyJ3aGl0ZWxpc3QiOiBbIkBzcGxpdC5pbyJdfX1dfX1dfQ==\\\"}\"}"; + RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); + GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); + + CommonChangeNotification ruleBasedSegmentCommonChangeNotification = new CommonChangeNotification(genericNotificationData, RuleBasedSegment.class); + Assert.assertEquals(IncomingNotification.Type.RB_SEGMENT_UPDATE, ruleBasedSegmentCommonChangeNotification.getType()); + Assert.assertEquals(1684265694505L, ruleBasedSegmentCommonChangeNotification.getChangeNumber()); + Assert.assertEquals(CompressType.NOT_COMPRESSED, ruleBasedSegmentCommonChangeNotification.getCompressType()); + Assert.assertEquals(0L, ruleBasedSegmentCommonChangeNotification.getPreviousChangeNumber()); + Assert.assertEquals("sample_rule_based_segment", ruleBasedSegmentCommonChangeNotification.getDefinition().name); + Assert.assertEquals(Status.ACTIVE, ruleBasedSegmentCommonChangeNotification.getDefinition().status); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java b/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java index 604b6371f..e3a5050e1 100644 --- a/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java +++ b/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java @@ -3,8 +3,8 @@ import io.split.SSEMockServer; import io.split.client.RequestDecorator; import io.split.engine.sse.client.SSEClient; +import io.split.engine.sse.dtos.CommonChangeNotification; import io.split.engine.sse.dtos.ErrorNotification; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.config.RequestConfig; @@ -96,7 +96,7 @@ public void startAndReceiveNotification() throws IOException { Awaitility.await() .atMost(50L, TimeUnit.SECONDS) - .untilAsserted(() -> Mockito.verify(_notificationProcessor, Mockito.times(1)).process(Mockito.any(FeatureFlagChangeNotification.class))); + .untilAsserted(() -> Mockito.verify(_notificationProcessor, Mockito.times(1)).process(Mockito.any(CommonChangeNotification.class))); OutboundSseEvent sseEventError = new OutboundEvent .Builder() diff --git a/client/src/test/java/io/split/engine/sse/NotificationParserImpTest.java b/client/src/test/java/io/split/engine/sse/NotificationParserImpTest.java index cd57e56ac..b8df61320 100644 --- a/client/src/test/java/io/split/engine/sse/NotificationParserImpTest.java +++ b/client/src/test/java/io/split/engine/sse/NotificationParserImpTest.java @@ -1,6 +1,7 @@ package io.split.engine.sse; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.client.dtos.Split; +import io.split.engine.sse.dtos.CommonChangeNotification; import io.split.engine.sse.enums.CompressType; import io.split.engine.sse.exceptions.EventParsingException; @@ -14,9 +15,10 @@ public void validateZlibCompressType() throws EventParsingException { String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":2,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; NotificationParserImp notificationParserImp = new NotificationParserImp(); - FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); - Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().name, "mauro_java"); - Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().changeNumber, 1684265694505L); + CommonChangeNotification incomingNotification = (CommonChangeNotification) notificationParserImp.parseMessage(payload); + Split split = (Split) incomingNotification.getDefinition(); + Assert.assertEquals("mauro_java", split.name); + Assert.assertEquals(1684265694505L, split.changeNumber); Assert.assertEquals(CompressType.ZLIB, incomingNotification.getCompressType()); Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); } @@ -26,9 +28,10 @@ public void validateGzipCompressType() throws EventParsingException { String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":1,\\\"d\\\":\\\"H4sIAAAAAAAA/8yT327aTBDFXyU612vJxoTgvUMfKB8qcaSapqoihAZ7DNusvWi9TpUiv3tl/pdQVb1qL+cwc3bOj/EGzlKeq3T6tuaYCoZEXbGFgMogkXXDIM0y31v4C/aCgMnrU9/3gl7Pp4yilMMIAuVusqDamvlXeiWIg/FAa5OSU6aEDHz/ip4wZ5Be1AmjoBsFAtVOCO56UXh31/O7ApUjV1eQGPw3HT+NIPCitG7bctIVC2ScU63d1DK5gksHCZPnEEhXVC45rosFW8ig1++GYej3g85tJEB6aSA7Aqkpc7Ws7XahCnLTbLVM7evnzalsUUHi8//j6WgyTqYQKMilK7b31tRryLa3WKiyfRCDeHhq2Dntiys+JS/J8THUt5VyrFXlHnYTQ3LU2h91yGdQVqhy+0RtTeuhUoNZ08wagTVZdxbBndF5vYVApb7z9m9pZgKaFqwhT+6coRHvg398nEweP/157Bd+S1hz6oxtm88O73B0jbhgM47nyej+YRRfgdNODDlXJWcJL9tUF5SqnRqfbtPr4LdcTHnk4rfp3buLOkG7+Pmp++vRM9w/wVblzX7Pm8OGfxf5YDKZfxh9SS6B/2Pc9t/7ja01o5k1PwIAAP//uTipVskEAAA=\\\"}\"}"; NotificationParserImp notificationParserImp = new NotificationParserImp(); - FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); - Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().name, "mauro_java"); - Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().changeNumber, 1684333081259L); + CommonChangeNotification incomingNotification = (CommonChangeNotification) notificationParserImp.parseMessage(payload); + Split split = (Split) incomingNotification.getDefinition(); + Assert.assertEquals("mauro_java", split.name); + Assert.assertEquals(1684333081259L, split.changeNumber); Assert.assertEquals(CompressType.GZIP, incomingNotification.getCompressType()); Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); } @@ -38,9 +41,10 @@ public void validateNotCompressType() throws EventParsingException { String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684329854385,\\\"pcn\\\":0,\\\"c\\\":0,\\\"d\\\":\\\"eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0=\\\"}\"}"; NotificationParserImp notificationParserImp = new NotificationParserImp(); - FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); - Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().name, "mauro_java"); - Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().changeNumber, 1684329854385L); + CommonChangeNotification incomingNotification = (CommonChangeNotification) notificationParserImp.parseMessage(payload); + Split split = (Split) incomingNotification.getDefinition(); + Assert.assertEquals("mauro_java", split.name); + Assert.assertEquals(1684329854385L, split.changeNumber); Assert.assertEquals(CompressType.NOT_COMPRESSED, incomingNotification.getCompressType()); Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); } @@ -50,7 +54,7 @@ public void validateCompressTypeIncorrect() throws EventParsingException { String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":3,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; NotificationParserImp notificationParserImp = new NotificationParserImp(); - FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); + CommonChangeNotification incomingNotification = (CommonChangeNotification) notificationParserImp.parseMessage(payload); Assert.assertNull(incomingNotification.getCompressType()); Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); } @@ -60,7 +64,7 @@ public void validateCompressTypeNull() throws EventParsingException { String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; NotificationParserImp notificationParserImp = new NotificationParserImp(); - FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); + CommonChangeNotification incomingNotification = (CommonChangeNotification) notificationParserImp.parseMessage(payload); Assert.assertNull(incomingNotification.getCompressType()); Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); } diff --git a/client/src/test/java/io/split/engine/sse/NotificationParserTest.java b/client/src/test/java/io/split/engine/sse/NotificationParserTest.java index 26f9e1997..ad0b4075e 100644 --- a/client/src/test/java/io/split/engine/sse/NotificationParserTest.java +++ b/client/src/test/java/io/split/engine/sse/NotificationParserTest.java @@ -1,13 +1,6 @@ package io.split.engine.sse; -import io.split.engine.sse.dtos.ControlNotification; -import io.split.engine.sse.dtos.ControlType; -import io.split.engine.sse.dtos.ErrorNotification; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; -import io.split.engine.sse.dtos.IncomingNotification; -import io.split.engine.sse.dtos.OccupancyNotification; -import io.split.engine.sse.dtos.SegmentChangeNotification; -import io.split.engine.sse.dtos.SplitKillNotification; +import io.split.engine.sse.dtos.*; import io.split.engine.sse.exceptions.EventParsingException; import org.junit.Before; import org.junit.Test; @@ -29,7 +22,7 @@ public void parseSplitUpdateShouldReturnParsedEvent() throws EventParsingExcepti IncomingNotification result = notificationParser.parseMessage(payload); assertEquals(IncomingNotification.Type.SPLIT_UPDATE, result.getType()); assertEquals("xxxx_xxxx_splits", result.getChannel()); - assertEquals(1592590435115L, ((FeatureFlagChangeNotification) result).getChangeNumber()); + assertEquals(1592590435115L, ((CommonChangeNotification) result).getChangeNumber()); } @Test diff --git a/client/src/test/java/io/split/engine/sse/NotificationProcessorTest.java b/client/src/test/java/io/split/engine/sse/NotificationProcessorTest.java index a56f05dd1..ea75ecb1d 100644 --- a/client/src/test/java/io/split/engine/sse/NotificationProcessorTest.java +++ b/client/src/test/java/io/split/engine/sse/NotificationProcessorTest.java @@ -1,12 +1,8 @@ package io.split.engine.sse; -import io.split.engine.sse.dtos.ControlNotification; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; -import io.split.engine.sse.dtos.GenericNotificationData; -import io.split.engine.sse.dtos.OccupancyNotification; -import io.split.engine.sse.dtos.SegmentChangeNotification; -import io.split.engine.sse.dtos.SegmentQueueDto; -import io.split.engine.sse.dtos.SplitKillNotification; +import io.split.client.dtos.RuleBasedSegment; +import io.split.client.dtos.Split; +import io.split.engine.sse.dtos.*; import io.split.engine.sse.workers.SegmentsWorkerImp; import io.split.engine.sse.workers.FeatureFlagsWorker; import io.split.engine.sse.workers.Worker; @@ -37,13 +33,29 @@ public void processSplitUpdateAddToQueueInWorker() { .changeNumber(changeNumber) .channel(channel) .build(); - FeatureFlagChangeNotification splitChangeNotification = new FeatureFlagChangeNotification(genericNotificationData); + CommonChangeNotification splitChangeNotification = new CommonChangeNotification(genericNotificationData, Split.class); _notificationProcessor.process(splitChangeNotification); Mockito.verify(_featureFlagsWorker, Mockito.times(1)).addToQueue(Mockito.anyObject()); } + @Test + public void processRuleBasedSegmentUpdateAddToQueueInWorker() { + long changeNumber = 1585867723838L; + String channel = "splits"; + GenericNotificationData genericNotificationData = GenericNotificationData.builder() + .changeNumber(changeNumber) + .channel(channel) + .type(IncomingNotification.Type.RB_SEGMENT_UPDATE) + .build(); + CommonChangeNotification ruleBasedSegmentChangeNotification = new CommonChangeNotification(genericNotificationData, RuleBasedSegment.class); + + _notificationProcessor.process(ruleBasedSegmentChangeNotification); + + Mockito.verify(_featureFlagsWorker, Mockito.times(1)).addToQueue(Mockito.anyObject()); + } + @Test public void processSplitKillAndAddToQueueInWorker() { long changeNumber = 1585867723838L; diff --git a/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java b/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java index 9be19b487..1f7c9a8c7 100644 --- a/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java +++ b/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java @@ -1,14 +1,24 @@ package io.split.engine.sse.workers; +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.Split; +import io.split.client.dtos.RuleBasedSegment; +import io.split.client.dtos.MatcherCombiner; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; import io.split.client.utils.Json; import io.split.engine.common.Synchronizer; import io.split.engine.common.SynchronizerImp; +import io.split.engine.experiments.ParsedCondition; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.engine.experiments.RuleBasedSegmentParser; import io.split.engine.experiments.SplitParser; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; -import io.split.engine.sse.dtos.GenericNotificationData; +import io.split.engine.matchers.AttributeMatcher; +import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.sse.dtos.CommonChangeNotification; import io.split.engine.sse.dtos.RawMessageNotification; +import io.split.engine.sse.dtos.GenericNotificationData; +import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SplitCacheProducer; import io.split.storages.memory.InMemoryCacheImp; import io.split.telemetry.domain.UpdatesFromSSE; @@ -18,8 +28,12 @@ import org.junit.Test; import org.mockito.Mockito; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; +import static org.mockito.Mockito.when; + public class FeatureFlagWorkerImpTest { private static final FlagSetsFilter FLAG_SETS_FILTER = new FlagSetsFilterImpl(new HashSet<>()); @@ -27,54 +41,114 @@ public class FeatureFlagWorkerImpTest { @Test public void testRefreshSplitsWithCorrectFF() { SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); Synchronizer synchronizer = Mockito.mock(SynchronizerImp.class); SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); TelemetryStorage telemetryRuntimeProducer = new InMemoryTelemetryStorage(); - FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, splitCacheProducer, telemetryRuntimeProducer, FLAG_SETS_FILTER); + FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER); String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":2,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); - FeatureFlagChangeNotification featureFlagChangeNotification = new FeatureFlagChangeNotification(genericNotificationData); + + CommonChangeNotification featureFlagChangeNotification = new CommonChangeNotification(genericNotificationData, Split.class); featureFlagsWorker.executeRefresh(featureFlagChangeNotification); UpdatesFromSSE updatesFromSSE = telemetryRuntimeProducer.popUpdatesFromSSE(); Assert.assertEquals(1, updatesFromSSE.getSplits()); - Mockito.verify(synchronizer, Mockito.times(0)).refreshSplits(1684265694505L); + Mockito.verify(synchronizer, Mockito.times(0)).refreshSplits(1684265694505L, 0L); Mockito.verify(synchronizer, Mockito.times(1)).forceRefreshSegment(Mockito.anyString()); } @Test public void testRefreshSplitsWithEmptyData() { SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); Synchronizer synchronizer = Mockito.mock(SynchronizerImp.class); SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); TelemetryStorage telemetryRuntimeProducer = new InMemoryTelemetryStorage(); - FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, splitCacheProducer, telemetryRuntimeProducer, FLAG_SETS_FILTER); + FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER); String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505}\"}"; RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); - FeatureFlagChangeNotification featureFlagChangeNotification = new FeatureFlagChangeNotification(genericNotificationData); + + CommonChangeNotification featureFlagChangeNotification = new CommonChangeNotification(genericNotificationData, Split.class); featureFlagsWorker.executeRefresh(featureFlagChangeNotification); UpdatesFromSSE updatesFromSSE = telemetryRuntimeProducer.popUpdatesFromSSE(); Assert.assertEquals(0, updatesFromSSE.getSplits()); - Mockito.verify(synchronizer, Mockito.times(1)).refreshSplits(1684265694505L); + Mockito.verify(synchronizer, Mockito.times(1)).refreshSplits(1684265694505L, 0L); Mockito.verify(synchronizer, Mockito.times(0)).forceRefreshSegment(Mockito.anyString()); } @Test public void testRefreshSplitsArchiveFF() { SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); Synchronizer synchronizer = Mockito.mock(SynchronizerImp.class); SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(1686165614090L, FLAG_SETS_FILTER); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); TelemetryStorage telemetryRuntimeProducer = new InMemoryTelemetryStorage(); - FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, splitCacheProducer, telemetryRuntimeProducer, FLAG_SETS_FILTER); + FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER); String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1686165617166,\\\"pcn\\\":1686165614090,\\\"c\\\":2,\\\"d\\\":\\\"eJxsUdFu4jAQ/JVqnx3JDjTh/JZCrj2JBh0EqtOBIuNswKqTIMeuxKH8+ykhiKrqiyXvzM7O7lzAGlEUSqbnEyaiRODgGjRAQOXAIQ/puPB96tHHIPQYQ/QmFNErxEgG44DKnI2AQHXtTOI0my6WcXZAmxoUtsTKvil7nNZVoQ5RYdFERh7VBwK5TY60rqWwqq6AM0q/qa8Qc+As/EHZ5HHMCDR9wQ/9kIajcEygscK6BjhEy+nLr008AwLvSuuOVgjdIIEcC+H03RZw2Hg/n88JEJBHUR0wceUeDXAWTAIWPAYsZEFAQOhDDdwnIPslnOk9NcAvNwEOly3IWtdmC3wLe+1wCy0Q2Hh/zNvTV9xg3sFtr5irQe3v5f7twgAOy8V8vlinQKAUVh7RPJvanbrBsi73qurMQpTM7oSrzjueV6hR2tp05E8J39MV1hq1d7YrWWxsZ2cQGYjzeLXK0pcoyRbLLP69juZZuuiyxoPo2oa7ukqYc+JKNEq+XgVmwopucC6sGMSS9etTvAQCH0I7BO7Ttt21BE7C2E8XsN+l06h/CJy25CveH/eGM0rbHQEt9qiHnR62jtKR7N/8wafQ7tr/AQAA//8S4fPB\\\"}\"}"; RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); - FeatureFlagChangeNotification featureFlagChangeNotification = new FeatureFlagChangeNotification(genericNotificationData); + + CommonChangeNotification featureFlagChangeNotification = new CommonChangeNotification(genericNotificationData, Split.class); featureFlagsWorker.executeRefresh(featureFlagChangeNotification); UpdatesFromSSE updatesFromSSE = telemetryRuntimeProducer.popUpdatesFromSSE(); Assert.assertEquals(1, updatesFromSSE.getSplits()); - Mockito.verify(synchronizer, Mockito.times(0)).refreshSplits(1686165617166L); + Mockito.verify(synchronizer, Mockito.times(0)).refreshSplits(1686165617166L, 0L); Mockito.verify(synchronizer, Mockito.times(0)).forceRefreshSegment(Mockito.anyString()); } + + @Test + public void testUpdateRuleBasedSegmentsWithCorrectFF() { + io.split.engine.matchers.Matcher matcher = (matchValue, bucketingKey, attributes, evaluationContext) -> false; + ParsedCondition parsedCondition = new ParsedCondition(ConditionType.ROLLOUT, + new CombiningMatcher(MatcherCombiner.AND, Arrays.asList(new AttributeMatcher("email", matcher, false))), + null, + "my label"); + ParsedRuleBasedSegment parsedRBS = new ParsedRuleBasedSegment("sample_rule_based_segment", + Arrays.asList(parsedCondition), + "user", + 5, + Arrays.asList("mauro@split.io","gaston@split.io"), + new ArrayList<>()); + + SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + Synchronizer synchronizer = Mockito.mock(SynchronizerImp.class); + SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); + TelemetryStorage telemetryRuntimeProducer = new InMemoryTelemetryStorage(); + FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER); + String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"RB_SEGMENT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":0,\\\"d\\\":\\\"eyJjaGFuZ2VOdW1iZXIiOiA1LCAibmFtZSI6ICJzYW1wbGVfcnVsZV9iYXNlZF9zZWdtZW50IiwgInN0YXR1cyI6ICJBQ1RJVkUiLCAidHJhZmZpY1R5cGVOYW1lIjogInVzZXIiLCAiZXhjbHVkZWQiOiB7ImtleXMiOiBbIm1hdXJvQHNwbGl0LmlvIiwgImdhc3RvbkBzcGxpdC5pbyJdLCAic2VnbWVudHMiOiBbXX0sICJjb25kaXRpb25zIjogW3sibWF0Y2hlckdyb3VwIjogeyJjb21iaW5lciI6ICJBTkQiLCAibWF0Y2hlcnMiOiBbeyJrZXlTZWxlY3RvciI6IHsidHJhZmZpY1R5cGUiOiAidXNlciIsICJhdHRyaWJ1dGUiOiAiZW1haWwifSwgIm1hdGNoZXJUeXBlIjogIkVORFNfV0lUSCIsICJuZWdhdGUiOiBmYWxzZSwgIndoaXRlbGlzdE1hdGNoZXJEYXRhIjogeyJ3aGl0ZWxpc3QiOiBbIkBzcGxpdC5pbyJdfX1dfX1dfQ==\\\"}\"}"; + RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); + GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); + + CommonChangeNotification ruleBasedSegmentChangeNotification = new CommonChangeNotification(genericNotificationData, RuleBasedSegment.class); + featureFlagsWorker.executeRefresh(ruleBasedSegmentChangeNotification); + Mockito.verify(ruleBasedSegmentCache, Mockito.times(1)).update(Arrays.asList(parsedRBS), new ArrayList<>(), 1684265694505L); + } + + @Test + public void testRefreshRuleBasedSegmentWithCorrectFF() { + SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + Synchronizer synchronizer = Mockito.mock(SynchronizerImp.class); + SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); + HashSet rbs = new HashSet<>(); + rbs.add("sample_rule_based_segment"); + when(ruleBasedSegmentCache.contains(rbs)).thenReturn(false); + TelemetryStorage telemetryRuntimeProducer = new InMemoryTelemetryStorage(); + FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER); + String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":0,\\\"d\\\":\\\"eyJjaGFuZ2VOdW1iZXIiOiAxMCwgInRyYWZmaWNUeXBlTmFtZSI6ICJ1c2VyIiwgIm5hbWUiOiAicmJzX2ZsYWciLCAidHJhZmZpY0FsbG9jYXRpb24iOiAxMDAsICJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOiAxODI4Mzc3MzgwLCAic2VlZCI6IC0yODY2MTc5MjEsICJzdGF0dXMiOiAiQUNUSVZFIiwgImtpbGxlZCI6IGZhbHNlLCAiZGVmYXVsdFRyZWF0bWVudCI6ICJvZmYiLCAiYWxnbyI6IDIsICJjb25kaXRpb25zIjogW3siY29uZGl0aW9uVHlwZSI6ICJST0xMT1VUIiwgIm1hdGNoZXJHcm91cCI6IHsiY29tYmluZXIiOiAiQU5EIiwgIm1hdGNoZXJzIjogW3sia2V5U2VsZWN0b3IiOiB7InRyYWZmaWNUeXBlIjogInVzZXIifSwgIm1hdGNoZXJUeXBlIjogIklOX1JVTEVfQkFTRURfU0VHTUVOVCIsICJuZWdhdGUiOiBmYWxzZSwgInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjogeyJzZWdtZW50TmFtZSI6ICJzYW1wbGVfcnVsZV9iYXNlZF9zZWdtZW50In19XX0sICJwYXJ0aXRpb25zIjogW3sidHJlYXRtZW50IjogIm9uIiwgInNpemUiOiAxMDB9LCB7InRyZWF0bWVudCI6ICJvZmYiLCAic2l6ZSI6IDB9XSwgImxhYmVsIjogImluIHJ1bGUgYmFzZWQgc2VnbWVudCBzYW1wbGVfcnVsZV9iYXNlZF9zZWdtZW50In0sIHsiY29uZGl0aW9uVHlwZSI6ICJST0xMT1VUIiwgIm1hdGNoZXJHcm91cCI6IHsiY29tYmluZXIiOiAiQU5EIiwgIm1hdGNoZXJzIjogW3sia2V5U2VsZWN0b3IiOiB7InRyYWZmaWNUeXBlIjogInVzZXIifSwgIm1hdGNoZXJUeXBlIjogIkFMTF9LRVlTIiwgIm5lZ2F0ZSI6IGZhbHNlfV19LCAicGFydGl0aW9ucyI6IFt7InRyZWF0bWVudCI6ICJvbiIsICJzaXplIjogMH0sIHsidHJlYXRtZW50IjogIm9mZiIsICJzaXplIjogMTAwfV0sICJsYWJlbCI6ICJkZWZhdWx0IHJ1bGUifV0sICJjb25maWd1cmF0aW9ucyI6IHt9LCAic2V0cyI6IFtdLCAiaW1wcmVzc2lvbnNEaXNhYmxlZCI6IGZhbHNlfQ==\\\"}\"}"; + RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); + GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); + CommonChangeNotification featureFlagChangeNotification = new CommonChangeNotification(genericNotificationData, Split.class); + + featureFlagsWorker.executeRefresh(featureFlagChangeNotification); + Mockito.verify(synchronizer, Mockito.times(0)).refreshSplits(0L, 1684265694505L); + } } \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/workers/SplitsWorkerTest.java b/client/src/test/java/io/split/engine/sse/workers/SplitsWorkerTest.java index 2ece6c550..7e63fa554 100644 --- a/client/src/test/java/io/split/engine/sse/workers/SplitsWorkerTest.java +++ b/client/src/test/java/io/split/engine/sse/workers/SplitsWorkerTest.java @@ -1,12 +1,16 @@ package io.split.engine.sse.workers; +import io.split.client.dtos.Split; import io.split.client.interceptors.FlagSetsFilter; import io.split.client.interceptors.FlagSetsFilterImpl; import io.split.engine.common.Synchronizer; +import io.split.engine.experiments.RuleBasedSegmentParser; import io.split.engine.experiments.SplitParser; -import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.CommonChangeNotification; import io.split.engine.sse.dtos.GenericNotificationData; +import io.split.engine.sse.dtos.IncomingNotification; import io.split.engine.sse.dtos.SplitKillNotification; +import io.split.storages.RuleBasedSegmentCache; import io.split.storages.SplitCacheProducer; import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.TelemetryRuntimeProducer; @@ -28,14 +32,16 @@ public class SplitsWorkerTest { public void addToQueueWithoutElementsWShouldNotTriggerFetch() throws InterruptedException { Synchronizer splitFetcherMock = Mockito.mock(Synchronizer.class); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); - FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(splitFetcherMock, splitParser, splitCacheProducer, telemetryRuntimeProducer, FLAG_SETS_FILTER); + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(splitFetcherMock, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER); featureFlagsWorker.start(); Thread.sleep(500); - Mockito.verify(splitFetcherMock, Mockito.never()).refreshSplits(Mockito.anyObject()); + Mockito.verify(splitFetcherMock, Mockito.never()).refreshSplits(Mockito.anyObject(), Mockito.anyObject()); featureFlagsWorker.stop(); } @@ -43,29 +49,36 @@ public void addToQueueWithoutElementsWShouldNotTriggerFetch() throws Interrupted public void addToQueueWithElementsWShouldTriggerFetch() throws InterruptedException { Synchronizer syncMock = Mockito.mock(Synchronizer.class); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); - FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, splitCacheProducer, telemetryRuntimeProducer, FLAG_SETS_FILTER); + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER); featureFlagsWorker.start(); ArgumentCaptor cnCaptor = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor cnCaptor2 = ArgumentCaptor.forClass(Long.class); - featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + featureFlagsWorker.addToQueue(new CommonChangeNotification(GenericNotificationData.builder() .changeNumber(1585956698457L) - .build())); - featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .type(IncomingNotification.Type.SPLIT_UPDATE) + .build(), Split.class)); + featureFlagsWorker.addToQueue(new CommonChangeNotification(GenericNotificationData.builder() .changeNumber(1585956698467L) - .build())); - featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .type(IncomingNotification.Type.SPLIT_UPDATE) + .build(), Split.class)); + featureFlagsWorker.addToQueue(new CommonChangeNotification(GenericNotificationData.builder() .changeNumber(1585956698477L) - .build())); - featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .type(IncomingNotification.Type.SPLIT_UPDATE) + .build(), Split.class)); + featureFlagsWorker.addToQueue(new CommonChangeNotification(GenericNotificationData.builder() .changeNumber(1585956698476L) - .build())); + .type(IncomingNotification.Type.SPLIT_UPDATE) + .build(), Split.class)); Thread.sleep(1000); - Mockito.verify(syncMock, Mockito.times(4)).refreshSplits(cnCaptor.capture()); + Mockito.verify(syncMock, Mockito.times(4)).refreshSplits(cnCaptor.capture(), cnCaptor2.capture()); List captured = cnCaptor.getAllValues(); assertThat(captured, contains(1585956698457L, 1585956698467L, 1585956698477L, 1585956698476L)); featureFlagsWorker.stop(); @@ -79,9 +92,11 @@ public void killShouldTriggerFetch() { Synchronizer syncMock = Mockito.mock(Synchronizer.class); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); - FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, splitCacheProducer, telemetryRuntimeProducer, FLAG_SETS_FILTER) { + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER) { }; featureFlagsWorker.start(); SplitKillNotification splitKillNotification = new SplitKillNotification(GenericNotificationData.builder() @@ -99,31 +114,36 @@ public void killShouldTriggerFetch() { public void messagesNotProcessedWhenWorkerStopped() throws InterruptedException { Synchronizer syncMock = Mockito.mock(Synchronizer.class); SplitParser splitParser = new SplitParser(); + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + RuleBasedSegmentCache ruleBasedSegmentCache = Mockito.mock(RuleBasedSegmentCache.class); TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); - FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, splitCacheProducer, telemetryRuntimeProducer, FLAG_SETS_FILTER); + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, ruleBasedSegmentParser, splitCacheProducer, ruleBasedSegmentCache, telemetryRuntimeProducer, FLAG_SETS_FILTER); featureFlagsWorker.start(); - featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + featureFlagsWorker.addToQueue(new CommonChangeNotification(GenericNotificationData.builder() .changeNumber(1585956698457L) - .build())); + .type(IncomingNotification.Type.SPLIT_UPDATE) + .build(), Split.class)); Thread.sleep(500); featureFlagsWorker.stop(); Thread.sleep(500); - featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + featureFlagsWorker.addToQueue(new CommonChangeNotification(GenericNotificationData.builder() .changeNumber(1585956698467L) - .build())); - Mockito.verify(syncMock, Mockito.times(1)).refreshSplits(Mockito.anyObject()); // Previous one! + .type(IncomingNotification.Type.SPLIT_UPDATE) + .build(), Split.class)); + Mockito.verify(syncMock, Mockito.times(1)).refreshSplits(Mockito.anyObject(), Mockito.anyObject()); // Previous one! Mockito.reset(syncMock); featureFlagsWorker.start(); - featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + featureFlagsWorker.addToQueue(new CommonChangeNotification(GenericNotificationData.builder() .changeNumber(1585956698477L) - .build())); + .type(IncomingNotification.Type.SPLIT_UPDATE) + .build(), Split.class)); Thread.sleep(500); - Mockito.verify(syncMock, Mockito.times(1)).refreshSplits(Mockito.anyObject()); + Mockito.verify(syncMock, Mockito.times(1)).refreshSplits(Mockito.anyObject(), Mockito.anyObject()); featureFlagsWorker.stop(); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/service/HttpSplitClientTest.java b/client/src/test/java/io/split/service/HttpSplitClientTest.java index ec8cd5e52..746ae5c01 100644 --- a/client/src/test/java/io/split/service/HttpSplitClientTest.java +++ b/client/src/test/java/io/split/service/HttpSplitClientTest.java @@ -8,15 +8,11 @@ import io.split.client.impressions.Impression; import io.split.client.utils.Json; import io.split.client.utils.SDKMetadata; -import io.split.client.utils.Utils; import io.split.engine.common.FetchOptions; -import io.split.service.SplitHttpClient; -import io.split.service.SplitHttpClientImpl; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpUriRequest; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.core5.http.HttpStatus; -//import org.apache.hc.core5.http.Header; import org.junit.Assert; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -39,7 +35,7 @@ public class HttpSplitClientTest { @Test public void testGetWithSpecialCharacters() throws URISyntaxException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException { - URI rootTarget = URI.create("https://api.split.io/splitChanges?since=1234567"); + URI rootTarget = URI.create("https://api.split.io/splitChanges?since=1234567&rbSince=-1"); CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("split-change-special-characters.json", HttpStatus.SC_OK); RequestDecorator decorator = new RequestDecorator(null); @@ -61,10 +57,10 @@ public void testGetWithSpecialCharacters() throws URISyntaxException, Invocation assertThat(headers[0].getName(), is(equalTo("Via"))); assertThat(headers[0].getValues().get(0), is(equalTo("HTTP/1.1 m_proxy_rio1"))); Assert.assertNotNull(change); - Assert.assertEquals(1, change.splits.size()); - Assert.assertNotNull(change.splits.get(0)); + Assert.assertEquals(1, change.featureFlags.d.size()); + Assert.assertNotNull(change.featureFlags.d.get(0)); - Split split = change.splits.get(0); + Split split = change.featureFlags.d.get(0); Map configs = split.configurations; Assert.assertEquals(2, configs.size()); Assert.assertEquals("{\"test\": \"blue\",\"grüne Straße\": 13}", configs.get("on")); diff --git a/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java b/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java index d2079f1bb..5589d71da 100644 --- a/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java +++ b/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java @@ -139,10 +139,10 @@ public void getMany() { @Test public void trafficTypesExist() { - ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, null, true); - ParsedSplit split2 = ParsedSplit.createParsedSplitForTests("splitName_2", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, null, true); - ParsedSplit split3 = ParsedSplit.createParsedSplitForTests("splitName_3", 0, false, "default_treatment", new ArrayList<>(), "tt_2", 123, 2, null, true); - ParsedSplit split4 = ParsedSplit.createParsedSplitForTests("splitName_4", 0, false, "default_treatment", new ArrayList<>(), "tt_3", 123, 2, null, true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, null, true, null); + ParsedSplit split2 = ParsedSplit.createParsedSplitForTests("splitName_2", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, null, true, null); + ParsedSplit split3 = ParsedSplit.createParsedSplitForTests("splitName_3", 0, false, "default_treatment", new ArrayList<>(), "tt_2", 123, 2, null, true, null); + ParsedSplit split4 = ParsedSplit.createParsedSplitForTests("splitName_4", 0, false, "default_treatment", new ArrayList<>(), "tt_3", 123, 2, null, true, null); _cache.putMany(Stream.of(split, split2, split3, split4).collect(Collectors.toList())); assertTrue(_cache.trafficTypeExists("tt_2")); @@ -163,10 +163,10 @@ public void testSegmentNames() { ParsedCondition parsedCondition1 = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new UserDefinedSegmentMatcher(EMPLOYEES)), fullyRollout); ParsedCondition parsedCondition2 = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new UserDefinedSegmentMatcher(EMPLOYEES+"2")), turnOff); - ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", Stream.of(parsedCondition1).collect(Collectors.toList()), "tt", 123, 2, null, true); - ParsedSplit split2 = ParsedSplit.createParsedSplitForTests("splitName_2", 0, false, "default_treatment", Stream.of(parsedCondition2).collect(Collectors.toList()), "tt", 123, 2, null, true); - ParsedSplit split3 = ParsedSplit.createParsedSplitForTests("splitName_3", 0, false, "default_treatment", Stream.of(parsedCondition1).collect(Collectors.toList()), "tt_2", 123, 2, null, true); - ParsedSplit split4 = ParsedSplit.createParsedSplitForTests("splitName_4", 0, false, "default_treatment", Stream.of(parsedCondition2).collect(Collectors.toList()), "tt_3", 123, 2, null, true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", Stream.of(parsedCondition1).collect(Collectors.toList()), "tt", 123, 2, null, true, null); + ParsedSplit split2 = ParsedSplit.createParsedSplitForTests("splitName_2", 0, false, "default_treatment", Stream.of(parsedCondition2).collect(Collectors.toList()), "tt", 123, 2, null, true, null); + ParsedSplit split3 = ParsedSplit.createParsedSplitForTests("splitName_3", 0, false, "default_treatment", Stream.of(parsedCondition1).collect(Collectors.toList()), "tt_2", 123, 2, null, true, null); + ParsedSplit split4 = ParsedSplit.createParsedSplitForTests("splitName_4", 0, false, "default_treatment", Stream.of(parsedCondition2).collect(Collectors.toList()), "tt_3", 123, 2, null, true, null); _cache.putMany(Stream.of(split, split2, split3, split4).collect(Collectors.toList())); @@ -178,19 +178,19 @@ public void testSegmentNames() { } private ParsedSplit getParsedSplitWithFlagSetsSameStorage(String splitName) { - return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(Arrays.asList("set1", "set2")), true); + return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(Arrays.asList("set1", "set2")), true, null); } private ParsedSplit getParsedSplitWithFlagSetsNotSameStorage(String splitName) { - return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(Arrays.asList("set3")), true); + return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(Arrays.asList("set3")), true, null); } private ParsedSplit getParsedSplitFlagSetsNull(String splitName) { - return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, null, true); + return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, null, true, null); } private ParsedSplit getParsedSplitFlagSetsEmpty(String splitName) { - return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(), true); + return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(), true, null); } @Test @@ -204,7 +204,7 @@ public void testPutMany() { @Test public void testIncreaseTrafficType() { - ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(), true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(), true, null); _cache.putMany(Stream.of(split).collect(Collectors.toList())); _cache.increaseTrafficType("tt_2"); assertTrue(_cache.trafficTypeExists("tt_2")); @@ -212,7 +212,7 @@ public void testIncreaseTrafficType() { @Test public void testDecreaseTrafficType() { - ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(), true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(), true, null); _cache.putMany(Stream.of(split).collect(Collectors.toList())); _cache.decreaseTrafficType("tt"); assertFalse(_cache.trafficTypeExists("tt_2")); @@ -220,10 +220,10 @@ public void testDecreaseTrafficType() { @Test public void testGetNamesByFlagSets() { - ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(Arrays.asList("set1", "set2", "set3")), true); - ParsedSplit split2 = ParsedSplit.createParsedSplitForTests("splitName_2", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(Arrays.asList("set1")), true); - ParsedSplit split3 = ParsedSplit.createParsedSplitForTests("splitName_3", 0, false, "default_treatment", new ArrayList<>(), "tt_2", 123, 2, new HashSet<>(Arrays.asList("set4")), true); - ParsedSplit split4 = ParsedSplit.createParsedSplitForTests("splitName_4", 0, false, "default_treatment", new ArrayList<>(), "tt_3", 123, 2, new HashSet<>(Arrays.asList("set2")), true); + ParsedSplit split = ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(Arrays.asList("set1", "set2", "set3")), true, null); + ParsedSplit split2 = ParsedSplit.createParsedSplitForTests("splitName_2", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2, new HashSet<>(Arrays.asList("set1")), true, null); + ParsedSplit split3 = ParsedSplit.createParsedSplitForTests("splitName_3", 0, false, "default_treatment", new ArrayList<>(), "tt_2", 123, 2, new HashSet<>(Arrays.asList("set4")), true, null); + ParsedSplit split4 = ParsedSplit.createParsedSplitForTests("splitName_4", 0, false, "default_treatment", new ArrayList<>(), "tt_3", 123, 2, new HashSet<>(Arrays.asList("set2")), true, null); _cache.putMany(Stream.of(split, split2, split3, split4).collect(Collectors.toList())); Map> namesByFlagSets = _cache.getNamesByFlagSets(new ArrayList<>(Arrays.asList("set1", "set2", "set3"))); diff --git a/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java b/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java new file mode 100644 index 000000000..32487bf51 --- /dev/null +++ b/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java @@ -0,0 +1,66 @@ +package io.split.storages.memory; + +import com.google.common.collect.Sets; +import io.split.client.dtos.ExcludedSegments; +import io.split.client.dtos.MatcherCombiner; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.engine.experiments.ParsedCondition; +import io.split.client.dtos.ConditionType; + +import io.split.engine.matchers.AttributeMatcher; +import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.UserDefinedSegmentMatcher; +import io.split.engine.matchers.strings.WhitelistMatcher; +import junit.framework.TestCase; +import org.junit.Test; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.List; + +public class RuleBasedSegmentCacheInMemoryImplTest extends TestCase { + + @Test + public void testAddAndDeleteSegment(){ + RuleBasedSegmentCacheInMemoryImp ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); + ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("sample_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, "label")),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), Lists.newArrayList()); + ruleBasedSegmentCache.update(Lists.newArrayList(parsedRuleBasedSegment), null, 123); + assertEquals(123, ruleBasedSegmentCache.getChangeNumber()); + assertEquals(parsedRuleBasedSegment, ruleBasedSegmentCache.get("sample_rule_based_segment")); + + ruleBasedSegmentCache.update(null, Lists.newArrayList("sample_rule_based_segment"), 124); + assertEquals(124, ruleBasedSegmentCache.getChangeNumber()); + assertEquals(null, ruleBasedSegmentCache.get("sample_rule_based_segment")); + } + + @Test + public void testMultipleSegment(){ + List excludedSegments = new ArrayList<>(); + excludedSegments.add(new ExcludedSegments("standard","segment1")); + excludedSegments.add(new ExcludedSegments("standard","segment3")); + + RuleBasedSegmentCacheInMemoryImp ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); + ParsedRuleBasedSegment parsedRuleBasedSegment1 = new ParsedRuleBasedSegment("sample_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, "label")),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), excludedSegments); + + excludedSegments.clear(); + excludedSegments.add(new ExcludedSegments("standard","segment1")); + excludedSegments.add(new ExcludedSegments("standard","segment2")); + AttributeMatcher segmentMatcher = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher("employees")); + CombiningMatcher segmentCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(segmentMatcher)); + ParsedRuleBasedSegment parsedRuleBasedSegment2 = new ParsedRuleBasedSegment("another_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, segmentCombiningMatcher, null, "label")),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), excludedSegments); + + ruleBasedSegmentCache.update(Lists.newArrayList(parsedRuleBasedSegment1, parsedRuleBasedSegment2), null, 123); + assertEquals(Lists.newArrayList("another_rule_based_segment", "sample_rule_based_segment"), ruleBasedSegmentCache.ruleBasedSegmentNames()); + assertEquals(Sets.newHashSet("segment2", "segment1", "employees"), ruleBasedSegmentCache.getSegments()); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/storages/pluggable/CustomStorageWrapperImp.java b/client/src/test/java/io/split/storages/pluggable/CustomStorageWrapperImp.java index 65ca18b06..728ffec78 100644 --- a/client/src/test/java/io/split/storages/pluggable/CustomStorageWrapperImp.java +++ b/client/src/test/java/io/split/storages/pluggable/CustomStorageWrapperImp.java @@ -10,6 +10,7 @@ import io.split.client.dtos.ConditionType; import io.split.client.dtos.Split; import io.split.client.dtos.Status; +import io.split.client.dtos.RuleBasedSegment; import io.split.client.utils.Json; import io.split.engine.ConditionsTestUtil; import io.split.engine.segments.SegmentImp; @@ -41,6 +42,8 @@ public class CustomStorageWrapperImp implements CustomStorageWrapper { private static final String TELEMETRY_INIT = "SPLITIO.telemetry.init"; private static final String LATENCIES = "SPLITIO.telemetry.latencies"; private static final String SPLIT = "SPLITIO.split."; + private static final String RULE_BASED_SEGMENT = "SPLITIO.rbsegment"; + private static final String RULE_BASED_SEGMENTS = "SPLITIO.rbsegments"; private static final String SPLITS = "SPLITIO.splits.*"; private static final String SEGMENT = "SPLITIO.segment."; private static final String IMPRESSIONS = "SPLITIO.impressions"; @@ -48,6 +51,7 @@ public class CustomStorageWrapperImp implements CustomStorageWrapper { private static final String COUNTS = "SPLITIO.impressions.counts"; private static final String FLAG_SET = "SPLITIO.flagSet"; private Map splitsStorage = new HashMap<>(); + private Map ruleBasedSegmentStorage = new HashMap<>(); private Map segmentStorage = new HashMap<>(); private final ConcurrentMap _methodLatencies = Maps.newConcurrentMap(); private final ConcurrentMap _latencies = Maps.newConcurrentMap(); @@ -81,6 +85,9 @@ public String get(String key) throws Exception { if(value.equals(SPLIT)){ return _json.toJson(splitsStorage.get(key)); } + if(value.equals(RULE_BASED_SEGMENT)){ + return _json.toJson(ruleBasedSegmentStorage.get(key)); + } return ""; } @@ -238,6 +245,10 @@ private String getStorage(String key) { return SPLITS; else if(key.startsWith(SPLIT)) return SPLIT; + else if(key.startsWith(RULE_BASED_SEGMENT)) + return RULE_BASED_SEGMENT; + else if(key.startsWith(RULE_BASED_SEGMENTS)) + return RULE_BASED_SEGMENTS; else if (key.startsWith(LATENCIES)) return LATENCIES; else if (key.startsWith(TELEMETRY_INIT)) @@ -262,6 +273,8 @@ private void updateCache(){ segmentStorage.put(PrefixAdapter.buildSegment("segmentName"), new SegmentImp(9874654L, "segmentName", Lists.newArrayList("key", "key2"))); splitsStorage.put(PrefixAdapter.buildSplitKey("first.name"), makeSplit("first.name", 123, Lists.newArrayList(condition), 456478976L)); splitsStorage.put(PrefixAdapter.buildSplitKey("second.name"), makeSplit("second.name", 321, Lists.newArrayList(), 568613L)); + splitsStorage.put(PrefixAdapter.buildSplitKey("rbs_flag"), Json.fromJson("{\"changeNumber\": 10, \"trafficTypeName\": \"user\", \"name\": \"rbs_flag\", \"trafficAllocation\": 100, \"trafficAllocationSeed\": 1828377380, \"seed\": -286617921, \"status\": \"ACTIVE\", \"killed\": false, \"defaultTreatment\": \"off\", \"algo\": 2, \"conditions\": [{\"conditionType\": \"ROLLOUT\", \"matcherGroup\": {\"combiner\": \"AND\", \"matchers\": [{\"keySelector\": {\"trafficType\": \"user\"},\"matcherType\": \"IN_RULE_BASED_SEGMENT\", \"negate\": false, \"userDefinedSegmentMatcherData\": {\"segmentName\": \"sample_rule_based_segment\"}}]},\"partitions\": [{\"treatment\": \"on\", \"size\": 100},{\"treatment\": \"off\", \"size\": 0}],\"label\": \"in rule based segment sample_rule_based_segment\"},{\"conditionType\": \"ROLLOUT\", \"matcherGroup\": {\"combiner\": \"AND\", \"matchers\": [{\"keySelector\": {\"trafficType\": \"user\"},\"matcherType\": \"ALL_KEYS\", \"negate\": false}]},\"partitions\": [{\"treatment\": \"on\", \"size\": 0},{\"treatment\": \"off\", \"size\": 100}],\"label\": \"default rule\"}],\"configurations\": {},\"sets\": [],\"impressionsDisabled\": false}", Split.class)); + ruleBasedSegmentStorage.put(PrefixAdapter.buildRuleBasedSegmentKey("sample_rule_based_segment"), Json.fromJson( "{\"changeNumber\":5,\"name\":\"sample_rule_based_segment\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[\"mauro@split.io\"],\"segments\":[]},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}}]}", RuleBasedSegment.class)); _flagSets.put("SPLITIO.flagSet.set1", new HashSet<>(new ArrayList<>(Arrays.asList("flag1", "flag2")))); } diff --git a/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomRuleBasedSegmentAdapterConsumerTest.java b/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomRuleBasedSegmentAdapterConsumerTest.java new file mode 100644 index 000000000..f1f39a730 --- /dev/null +++ b/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomRuleBasedSegmentAdapterConsumerTest.java @@ -0,0 +1,177 @@ +package io.split.storages.pluggable.adapters; + +import com.google.common.collect.Lists; +import io.split.client.dtos.*; +import io.split.client.utils.Json; +import io.split.engine.ConditionsTestUtil; +import io.split.engine.experiments.*; +import io.split.storages.pluggable.domain.PrefixAdapter; +import io.split.storages.pluggable.domain.UserStorageWrapper; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import pluggable.CustomStorageWrapper; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.split.TestHelper.makeRuleBasedSegment; + +public class UserCustomRuleBasedSegmentAdapterConsumerTest { + + private static final String RULE_BASED_SEGMENT_NAME = "RuleBasedSegmentName"; + private CustomStorageWrapper _customStorageWrapper; + private UserStorageWrapper _userStorageWrapper; + private UserCustomRuleBasedSegmentAdapterConsumer _userCustomRuleBasedSegmentAdapterConsumer; + + @Before + public void setUp() throws NoSuchFieldException, IllegalAccessException { + _customStorageWrapper = Mockito.mock(CustomStorageWrapper.class); + _userStorageWrapper = Mockito.mock(UserStorageWrapper.class); + _userCustomRuleBasedSegmentAdapterConsumer = new UserCustomRuleBasedSegmentAdapterConsumer(_customStorageWrapper); + Field userCustomRuleBasedSegmentAdapterConsumer = UserCustomRuleBasedSegmentAdapterConsumer.class.getDeclaredField("_userStorageWrapper"); + userCustomRuleBasedSegmentAdapterConsumer.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(userCustomRuleBasedSegmentAdapterConsumer, userCustomRuleBasedSegmentAdapterConsumer.getModifiers() & ~Modifier.FINAL); + userCustomRuleBasedSegmentAdapterConsumer.set(_userCustomRuleBasedSegmentAdapterConsumer, _userStorageWrapper); + } + + @Test + public void testGetChangeNumber() { + Mockito.when(_userStorageWrapper.get(PrefixAdapter.buildRuleBasedSegmentChangeNumber())).thenReturn(getLongAsJson(120L)); + Assert.assertEquals(120L, _userCustomRuleBasedSegmentAdapterConsumer.getChangeNumber()); + Mockito.verify(_userStorageWrapper, Mockito.times(1)).get(Mockito.anyString()); + } + + @Test + public void testGetChangeNumberWithWrapperFailing() { + Mockito.when(_userStorageWrapper.get(PrefixAdapter.buildRuleBasedSegmentChangeNumber())).thenReturn(null); + Assert.assertEquals(-1L, _userCustomRuleBasedSegmentAdapterConsumer.getChangeNumber()); + Mockito.verify(_userStorageWrapper, Mockito.times(1)).get(Mockito.anyString()); + } + + @Test + public void testGetChangeNumberWithGsonFailing() { + Mockito.when(_userStorageWrapper.get(PrefixAdapter.buildRuleBasedSegmentChangeNumber())).thenReturn("a"); + Assert.assertEquals(-1L, _userCustomRuleBasedSegmentAdapterConsumer.getChangeNumber()); + Mockito.verify(_userStorageWrapper, Mockito.times(1)).get(Mockito.anyString()); + } + + @Test + public void testGetRuleBasedSegment() { + RuleBasedSegmentParser ruleBasedSegmentParser = new RuleBasedSegmentParser(); + RuleBasedSegment ruleBasedSegment = getRuleBasedSegment(RULE_BASED_SEGMENT_NAME); + Mockito.when(_userStorageWrapper.get(PrefixAdapter.buildRuleBasedSegmentKey(RULE_BASED_SEGMENT_NAME))).thenReturn(getRuleBasedSegmentAsJson(ruleBasedSegment)); + ParsedRuleBasedSegment result = _userCustomRuleBasedSegmentAdapterConsumer.get(RULE_BASED_SEGMENT_NAME); + ParsedRuleBasedSegment expected = ruleBasedSegmentParser.parse(ruleBasedSegment); + Assert.assertEquals(expected, result); + } + + @Test + public void testGetRuleBasedSegmentNotFound() { + Mockito.when(_userStorageWrapper.get(PrefixAdapter.buildRuleBasedSegmentKey(RULE_BASED_SEGMENT_NAME))).thenReturn(null); + Mockito.when(_userStorageWrapper.get(PrefixAdapter.buildRuleBasedSegmentKey(RULE_BASED_SEGMENT_NAME))).thenReturn(null); + ParsedRuleBasedSegment result = _userCustomRuleBasedSegmentAdapterConsumer.get(RULE_BASED_SEGMENT_NAME); + Assert.assertNull(result); + } + + @Test + public void testGetAll() { + RuleBasedSegment ruleBasedSegment = getRuleBasedSegment(RULE_BASED_SEGMENT_NAME); + RuleBasedSegment ruleBasedSegment2 = getRuleBasedSegment(RULE_BASED_SEGMENT_NAME+"2"); + List listResultExpected = Stream.of(ruleBasedSegment, ruleBasedSegment2).collect(Collectors.toList()); + Set keysResult = Stream.of(RULE_BASED_SEGMENT_NAME, RULE_BASED_SEGMENT_NAME+"2").collect(Collectors.toSet()); + Mockito.when(_userStorageWrapper.getKeysByPrefix(Mockito.anyObject())). + thenReturn(keysResult); + List getManyExpected = Stream.of(Json.toJson(ruleBasedSegment), Json.toJson(ruleBasedSegment2)).collect(Collectors.toList()); + Mockito.when(_userStorageWrapper.getMany(Mockito.anyObject())). + thenReturn(getManyExpected); + List ruleBasedSegmentsResult = (List) _userCustomRuleBasedSegmentAdapterConsumer.getAll(); + Assert.assertNotNull(ruleBasedSegmentsResult); + Assert.assertEquals(listResultExpected.size(), ruleBasedSegmentsResult.size()); + Mockito.verify(_userStorageWrapper, Mockito.times(1)).getKeysByPrefix(Mockito.anyString()); + Mockito.verify(_userStorageWrapper, Mockito.times(1)).getMany(Mockito.anyObject()); + } + + @Test + public void testGetAllWithWrapperFailing() { + Mockito.when(_userStorageWrapper.get(PrefixAdapter.buildGetAllSplit())). + thenReturn(null); + List ruleBasedSegmentsResult = (List) _userCustomRuleBasedSegmentAdapterConsumer.getAll(); + Assert.assertNotNull(ruleBasedSegmentsResult); + Assert.assertEquals(0, ruleBasedSegmentsResult.size()); + } + + @Test + public void testGetAllNullOnWrappers() { + Mockito.when(_userStorageWrapper.getKeysByPrefix(PrefixAdapter.buildGetAllRuleBasedSegment())). + thenReturn(null); + List ruleBasedSegmentsResult = (List) _userCustomRuleBasedSegmentAdapterConsumer.getAll(); + Assert.assertEquals(0, ruleBasedSegmentsResult.size()); + } + + @Test + public void testGetAllNullOnGetMany() { + Set keysResult = Stream.of(RULE_BASED_SEGMENT_NAME, RULE_BASED_SEGMENT_NAME+"2").collect(Collectors.toSet()); + Mockito.when(_userStorageWrapper.getKeysByPrefix(Mockito.anyObject())). + thenReturn(keysResult); + Mockito.when(_userStorageWrapper.getMany(Mockito.anyObject())). + thenReturn(null); + List ruleBasedSegmentsResult = (List) _userCustomRuleBasedSegmentAdapterConsumer.getAll(); + Assert.assertEquals(0, ruleBasedSegmentsResult.size()); + } + + @Test + public void testGetSegments() { + Condition condition = ConditionsTestUtil.makeUserDefinedSegmentCondition(ConditionType.WHITELIST, "employee", + null, false); + RuleBasedSegment ruleBasedSegment = makeRuleBasedSegment("rbs", Arrays.asList(condition), 1); + List getManyExpected = Stream.of(Json.toJson(ruleBasedSegment)).collect(Collectors.toList()); + Mockito.when(_userStorageWrapper.getMany(Mockito.anyObject())). + thenReturn(getManyExpected); + HashSet segmentResult = (HashSet) _userCustomRuleBasedSegmentAdapterConsumer.getSegments(); + Assert.assertTrue(segmentResult.contains("employee")); + } + + @Test + public void testGetruleBasedSegmentNames() { + RuleBasedSegment ruleBasedSegment = getRuleBasedSegment(RULE_BASED_SEGMENT_NAME); + RuleBasedSegment ruleBasedSegment2 = getRuleBasedSegment(RULE_BASED_SEGMENT_NAME+"2"); + Set keysResult = Stream.of(RULE_BASED_SEGMENT_NAME, RULE_BASED_SEGMENT_NAME+"2").collect(Collectors.toSet()); + Mockito.when(_userStorageWrapper.getKeysByPrefix(Mockito.anyObject())). + thenReturn(keysResult); + List getManyExpected = Stream.of(Json.toJson(ruleBasedSegment), Json.toJson(ruleBasedSegment2)).collect(Collectors.toList()); + Mockito.when(_userStorageWrapper.getMany(Mockito.anyObject())). + thenReturn(getManyExpected); + List ruleBasedSegmentsResult = _userCustomRuleBasedSegmentAdapterConsumer.ruleBasedSegmentNames(); + Assert.assertNotNull(ruleBasedSegmentsResult); + Assert.assertEquals(keysResult.size(), ruleBasedSegmentsResult.size()); + Assert.assertEquals(keysResult, new HashSet<>(ruleBasedSegmentsResult)); + } + + public static String getLongAsJson(long value) { + return Json.toJson(value); + } + + public static String getRuleBasedSegmentAsJson(RuleBasedSegment ruleBasedSegment) { + return Json.toJson(ruleBasedSegment); + } + + private RuleBasedSegment getRuleBasedSegment(String name) { + ArrayList set = Lists.newArrayList("sms", "voice"); + Condition c = ConditionsTestUtil.containsString("user", + "products", + set, + false, + null + ); + + List conditions = Lists.newArrayList(c); + return makeRuleBasedSegment(name, conditions, 1); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterConsumerTest.java b/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterConsumerTest.java index 261147113..d12badc0c 100644 --- a/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterConsumerTest.java +++ b/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterConsumerTest.java @@ -76,9 +76,23 @@ public void testGetSplit() { SplitParser splitParser = new SplitParser(); Split split = getSplit(SPLIT_NAME); Mockito.when(_userStorageWrapper.get(PrefixAdapter.buildSplitKey(SPLIT_NAME))).thenReturn(getSplitAsJson(split)); - ParsedSplit result = _userCustomSplitAdapterConsumer.get(SPLIT_NAME); + ParsedSplit actual = _userCustomSplitAdapterConsumer.get(SPLIT_NAME); ParsedSplit expected = splitParser.parse(split); - Assert.assertEquals(expected, result); + Assert.assertEquals(actual.getRuleBasedSegmentsNames(), expected.getRuleBasedSegmentsNames()); + Assert.assertEquals(actual.seed(), expected.seed()); + Assert.assertEquals(actual.algo(), expected.algo()); + Assert.assertEquals(actual.trafficAllocationSeed(), expected.trafficAllocationSeed()); + Assert.assertEquals(actual.flagSets(), expected.flagSets()); + Assert.assertEquals(actual.parsedConditions(), expected.parsedConditions()); + Assert.assertEquals(actual.trafficAllocation(), expected.trafficAllocation()); + Assert.assertEquals(actual.getSegmentsNames(), expected.getSegmentsNames()); + Assert.assertEquals(actual.impressionsDisabled(), expected.impressionsDisabled()); + Assert.assertEquals(actual.killed(), expected.killed()); + Assert.assertEquals(actual.defaultTreatment(), expected.defaultTreatment()); + Assert.assertEquals(actual.changeNumber(), expected.changeNumber()); + Assert.assertEquals(actual.feature(), expected.feature()); + Assert.assertEquals(actual.configurations(), expected.configurations()); + Assert.assertEquals(actual.prerequisitesMatcher().toString(), expected.prerequisitesMatcher().toString()); } @Test diff --git a/client/src/test/resources/rule_base_segments.json b/client/src/test/resources/rule_base_segments.json new file mode 100644 index 000000000..65cd9a5d8 --- /dev/null +++ b/client/src/test/resources/rule_base_segments.json @@ -0,0 +1,61 @@ +{"ff": {"d": [], "t": -1, "s": -1}, +"rbs": {"t": -1, "s": -1, "d": + [{ + "changeNumber": 5, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{"keys":["mauro@split.io","gaston@split.io"],"segments":[]}, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + } + ]}, + { + "changeNumber": 5, + "name": "dependent_rbs", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded": { + "keys": [], + "segments": [] + }, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "IN_RULE_BASED_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "sample_rule_based_segment" + } + } + ] + } + } + ] + }] +}} diff --git a/client/src/test/resources/rule_base_segments2.json b/client/src/test/resources/rule_base_segments2.json new file mode 100644 index 000000000..991fa81ba --- /dev/null +++ b/client/src/test/resources/rule_base_segments2.json @@ -0,0 +1,63 @@ +{"ff": {"d": [], "t": -1, "s": -1}, +"rbs": {"t": -1, "s": -1, "d": [ + { + "changeNumber": 5, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[{"type":"rule-based", "name":"no_excludes"}] + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@harness.io" + ] + } + } + ] + } + } + ] + }, + { + "changeNumber": 5, + "name": "no_excludes", + "status": "ACTIVE", + "trafficTypeName": "user", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + } + ] + } +]}} diff --git a/client/src/test/resources/rule_base_segments3.json b/client/src/test/resources/rule_base_segments3.json new file mode 100644 index 000000000..f738f3f77 --- /dev/null +++ b/client/src/test/resources/rule_base_segments3.json @@ -0,0 +1,35 @@ +{"ff": {"d": [], "t": -1, "s": -1}, +"rbs": {"t": -1, "s": -1, "d": [ + { + "changeNumber": 5, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[{"type":"standard", "name":"segment1"}] + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + } + ] + } +]}} diff --git a/client/src/test/resources/sanitizer/splitChangeSplitsToSanitize.json b/client/src/test/resources/sanitizer/splitChangeSplitsToSanitize.json index 49e779066..de35084ed 100644 --- a/client/src/test/resources/sanitizer/splitChangeSplitsToSanitize.json +++ b/client/src/test/resources/sanitizer/splitChangeSplitsToSanitize.json @@ -1,5 +1,5 @@ -{ - "splits": [ +{"ff": { + "d": [ { "name": "test1", "trafficAllocation": 101, @@ -96,6 +96,37 @@ ] } ], - "since": -1, - "till": 1660326991072 + "s": -1, + "t": 1660326991072 + }, +"rbs":{ + "d": [ + {"changeNumber":5, + "name":"sample_rule_based_segment", + "status":"ACTIVE", + "trafficTypeName":"user", + "excluded":{"keys":["mauro@split.io"]} + }, + {"changeNumber":5, + "status":"ACTIVE", + "trafficTypeName":"user", + "excluded":{"keys":["mauro@split.io"],"segments":[]}, + "conditions":[ + {"conditionType":"ROLLOUT", + "matcherGroup":{"combiner":"AND", + "matchers":[ + {"keySelector":{"trafficType":"user","attribute":"email"}, + "matcherType":"ENDS_WITH", + "negate":false, + "whitelistMatcherData":{"whitelist":["@split.io"]} + } + ] + } + } + ] + } + + ], + "s": -1, + "t": -1} } \ No newline at end of file diff --git a/client/src/test/resources/sanitizer/splitChangeTillSanitization.json b/client/src/test/resources/sanitizer/splitChangeTillSanitization.json index 018d2ecb6..5a1f806fc 100644 --- a/client/src/test/resources/sanitizer/splitChangeTillSanitization.json +++ b/client/src/test/resources/sanitizer/splitChangeTillSanitization.json @@ -1,5 +1,5 @@ -{ - "splits": [ +{"ff": { + "d": [ { "trafficTypeName": "user", "name": "test1", @@ -50,6 +50,6 @@ ] } ], - "since": 398, - "till": 0 -} \ No newline at end of file + "s": 398, + "t": 0 +}, "rbs":{"d": [], "s": -1, "t": 0}} \ No newline at end of file diff --git a/client/src/test/resources/sanitizer/splitChangeWithoutSplits.json b/client/src/test/resources/sanitizer/splitChangeWithoutSplits.json index 89fdca288..29463bffb 100644 --- a/client/src/test/resources/sanitizer/splitChangeWithoutSplits.json +++ b/client/src/test/resources/sanitizer/splitChangeWithoutSplits.json @@ -1,4 +1,4 @@ -{ - "since": -1, - "till": 2434234234 -} \ No newline at end of file +{"ff": { + "s": -1, + "t": 2434234234 +}, "rbs":{"s": -1, "t": -1}} \ No newline at end of file diff --git a/client/src/test/resources/sanitizer/splitChangerMatchersNull.json b/client/src/test/resources/sanitizer/splitChangerMatchersNull.json index 2e790d65c..282f3b548 100644 --- a/client/src/test/resources/sanitizer/splitChangerMatchersNull.json +++ b/client/src/test/resources/sanitizer/splitChangerMatchersNull.json @@ -1,5 +1,5 @@ -{ - "splits": [ +{"ff": { + "d": [ { "name": "test1", "trafficTypeName": "user", @@ -72,6 +72,6 @@ ] } ], - "since": -1, - "till": 1660326991072 -} \ No newline at end of file + "s": -1, + "t": 1660326991072 +}, "rbs":{"d": [], "s": -1, "t": -1}} \ No newline at end of file diff --git a/client/src/test/resources/segment_2.json b/client/src/test/resources/segment_2.json new file mode 100644 index 000000000..7e8c81e79 --- /dev/null +++ b/client/src/test/resources/segment_2.json @@ -0,0 +1,10 @@ +{ + "name": "segment_2", + "added": [ + "user1", + "user4" + ], + "removed": [], + "since": -1, + "till": 1585948850110 +} \ No newline at end of file diff --git a/client/src/test/resources/segment_test.json b/client/src/test/resources/segment_test.json new file mode 100644 index 000000000..a1458fc42 --- /dev/null +++ b/client/src/test/resources/segment_test.json @@ -0,0 +1,12 @@ +{ + "name": "segment_test", + "added": [ + "user1", + "user2", + "user3", + "user4" + ], + "removed": [], + "since": -1, + "till": 1585948850110 +} \ No newline at end of file diff --git a/client/src/test/resources/semver/semver-splits.json b/client/src/test/resources/semver/semver-splits.json index a7e58689e..a266c5676 100644 --- a/client/src/test/resources/semver/semver-splits.json +++ b/client/src/test/resources/semver/semver-splits.json @@ -1,5 +1,5 @@ -{ - "splits":[ +{ "ff": { + "d":[ { "trafficTypeName":"user", "name":"semver_between", @@ -426,6 +426,315 @@ ] } ], - "since":-1, - "till":1675259356568 + "s":-1, + "t":1675259356568}, + "rbs": { + "t": 1675259356568, + "s": -1, + "d": [ + { + "trafficTypeName":"user", + "name":"rbs_semver_between", + "status":"ACTIVE", + "changeNumber":1675259356568, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":"version" + }, + "matcherType":"BETWEEN_SEMVER", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "dependencyMatcherData":null, + "booleanMatcherData":null, + "stringMatcherData":null, + "betweenStringMatcherData":{ + "start":"1.22.9", + "end":"2.1.0" + } + } + ] + }, + "label":"between semver" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "label":"default rule" + } + ], + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + } + }, + { + "name":"rbs_semver_equalto", + "status":"ACTIVE", + "changeNumber":1675259356568, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":"version" + }, + "matcherType":"EQUAL_TO_SEMVER", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "dependencyMatcherData":null, + "booleanMatcherData":null, + "stringMatcherData":"1.22.9" + } + ] + }, + "label":"equal to semver" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "label":"default rule" + } + ], + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + } + }, + { + "name":"rbs_semver_greater_or_equalto", + "status":"ACTIVE", + "defaultTreatment":"off", + "changeNumber":1675259356568, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[{ + "keySelector":{ + "trafficType":"user", + "attribute":"version" + }, + "matcherType":"GREATER_THAN_OR_EQUAL_TO_SEMVER", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "dependencyMatcherData":null, + "booleanMatcherData":null, + "stringMatcherData":"1.22.9" + }]}, + "label":"greater than or equal to semver" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "label":"default rule" + } + ], + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + } + }, + { + "trafficTypeName":"user", + "name":"rbs_semver_inlist", + "status":"ACTIVE", + "changeNumber":1675259356568, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":"version" + }, + "matcherType":"IN_LIST_SEMVER", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "1.22.9", + "2.1.0" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "dependencyMatcherData":null, + "booleanMatcherData":null, + "stringMatcherData":null, + "betweenStringMatcherData":null + }]}, + "label":"in list semver" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ]}, + "label":"default rule" + }], + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + } + }, + { + "trafficTypeName":"user", + "name":"rbs_semver_less_or_equalto", + "trafficAllocation":100, + "trafficAllocationSeed":1068038034, + "seed":-1053389887, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1675259356568, + "algo":2, + "configurations":null, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":"version" + }, + "matcherType":"LESS_THAN_OR_EQUAL_TO_SEMVER", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "dependencyMatcherData":null, + "booleanMatcherData":null, + "stringMatcherData":"1.22.9" + }]}, + "label":"less than or equal to semver" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"user", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + }]}, + "label":"default rule" + }], + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + } + }] + } } \ No newline at end of file diff --git a/client/src/test/resources/split-change-special-characters.json b/client/src/test/resources/split-change-special-characters.json index 9fd55904e..ae99d7a7f 100644 --- a/client/src/test/resources/split-change-special-characters.json +++ b/client/src/test/resources/split-change-special-characters.json @@ -1,5 +1,5 @@ -{ - "splits": [ +{ "ff": { + "d": [ { "trafficTypeName": "user", "name": "DEMO_MURMUR2", @@ -10,7 +10,10 @@ "killed": false, "defaultTreatment": "of", "changeNumber": 1491244291288, - "sets": [ "set1", "set2" ], + "sets": [ + "set1", + "set2" + ], "algo": 2, "configurations": { "on": "{\"test\": \"blue\",\"grüne Straße\": 13}", @@ -51,6 +54,7 @@ ] } ], - "since": 1491244291288, - "till": 1491244291288 + "s": 1491244291288, + "t": 1491244291288}, + "rbs": {"d": [], "s": -1, "t": -1} } diff --git a/client/src/test/resources/splitFetcher/test_0.json b/client/src/test/resources/splitFetcher/test_0.json index 1edfecaec..82e6bbad5 100644 --- a/client/src/test/resources/splitFetcher/test_0.json +++ b/client/src/test/resources/splitFetcher/test_0.json @@ -1 +1,4 @@ -{"splits":[{"trafficTypeName":"user","name":"SPLIT_1","trafficAllocation":100,"trafficAllocationSeed":-1780071202,"seed":-1442762199,"status":"ACTIVE","killed":false,"defaultTreatment":"off","changeNumber":1675443537882,"algo":2,"configurations":{},"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"default rule"}]},{"trafficTypeName":"user","name":"SPLIT_2","trafficAllocation":100,"trafficAllocationSeed":-1780071202,"seed":-1442762199,"status":"ACTIVE","killed":false,"defaultTreatment":"off","changeNumber":1675443537882,"algo":2,"configurations":{},"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"default rule"}]}],"since":-1,"till":-1} \ No newline at end of file +{"ff": {"d": +[{"trafficTypeName":"user","name":"SPLIT_1","trafficAllocation":100,"trafficAllocationSeed":-1780071202,"seed":-1442762199,"status":"ACTIVE","killed":false,"defaultTreatment":"off","changeNumber":1675443537882,"algo":2,"configurations":{},"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"default rule"}]},{"trafficTypeName":"user","name":"SPLIT_2","trafficAllocation":100,"trafficAllocationSeed":-1780071202,"seed":-1442762199,"status":"ACTIVE","killed":false,"defaultTreatment":"off","changeNumber":1675443537882,"algo":2,"configurations":{},"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"default rule"}]}], + "since":-1,"till":-1 +}, "rbs":{"d": [], "s": -1, "t": -1}} \ No newline at end of file diff --git a/client/src/test/resources/split_init.json b/client/src/test/resources/split_init.json index 4a210976c..01d57975a 100644 --- a/client/src/test/resources/split_init.json +++ b/client/src/test/resources/split_init.json @@ -1,5 +1,5 @@ -{ - "splits": [ +{"ff": { + "d": [ { "trafficTypeName": "user", "name": "split_1", @@ -562,6 +562,44 @@ ] } ], - "since": -1, - "till": 1660326991072 -} \ No newline at end of file + "s": -1, + "t": 1660326991072 +}, "rbs":{"d": [ + { + "changeNumber": 5, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + }, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment_2" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + } + } + ] + } +], "s": -1, "t": -1}} \ No newline at end of file diff --git a/client/src/test/resources/split_old_spec.json b/client/src/test/resources/split_old_spec.json new file mode 100644 index 000000000..66a05ca89 --- /dev/null +++ b/client/src/test/resources/split_old_spec.json @@ -0,0 +1,566 @@ +{"splits": [ + { + "trafficTypeName": "user", + "name": "split_1", + "trafficAllocation": 100, + "trafficAllocationSeed": -1364119282, + "seed": -605938843, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1660326991072, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 50 + }, + { + "treatment": "off", + "size": 50 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "split_2", + "trafficAllocation": 100, + "trafficAllocationSeed": -92391491, + "seed": -1769377604, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1651003069855, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "admin", + "user_1", + "user_2" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "v5", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment_1" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "V4", + "size": 0 + }, + { + "treatment": "v5", + "size": 0 + } + ], + "label": "in segment segment_1" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "V4", + "size": 0 + }, + { + "treatment": "v5", + "size": 0 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "split_3", + "trafficAllocation": 100, + "trafficAllocationSeed": -670005248, + "seed": -1297078412, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1650919058695, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "admin" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "V5", + "size": 0 + }, + { + "treatment": "v8", + "size": 0 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "split_4", + "trafficAllocation": 50, + "trafficAllocationSeed": -1520910077, + "seed": -1785086567, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1647274074042, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "split_5", + "trafficAllocation": 100, + "trafficAllocationSeed": -3629915, + "seed": 816031817, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1622494310037, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "seba", + "tincho" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "user_3" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "split_6", + "trafficAllocation": 100, + "trafficAllocationSeed": -970151859, + "seed": -1258287669, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1605020019151, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "admin" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "split_7", + "trafficAllocation": 100, + "trafficAllocationSeed": 291807630, + "seed": -134149800, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1603461301902, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1660326991072 +} \ No newline at end of file diff --git a/client/src/test/resources/splits.json b/client/src/test/resources/splits.json index de9696b4e..da2654f1b 100644 --- a/client/src/test/resources/splits.json +++ b/client/src/test/resources/splits.json @@ -1,5 +1,5 @@ -{ - "splits": [ +{"ff": { + "d": [ { "trafficTypeName": "user", "name": "push_test", @@ -224,8 +224,113 @@ "label": "default label" } ] + }, + { + "changeNumber": 10, + "trafficTypeName": "user", + "name": "rbs_flag", + "trafficAllocation": 100, + "trafficAllocationSeed": 1828377380, + "seed": -286617921, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "IN_RULE_BASED_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "sample_rule_based_segment" + } + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "in rule based segment sample_rule_based_segment" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "ALL_KEYS", + "negate": false + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ], + "configurations": {}, + "sets": [], + "impressionsDisabled": false } ], - "since": -1, - "till": 1585948850109 + "s": -1, + "t": 1585948850109 +}, "rbs":{"d": [ + { + "changeNumber": 5, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + } + ] + }], "s": -1, "t": 1585948850109} } diff --git a/client/src/test/resources/splits2.json b/client/src/test/resources/splits2.json index a01787d4a..afbc92992 100644 --- a/client/src/test/resources/splits2.json +++ b/client/src/test/resources/splits2.json @@ -1,5 +1,5 @@ -{ - "splits": [ +{"ff": { + "d": [ { "trafficTypeName": "user", "name": "push_test", @@ -115,6 +115,6 @@ ] } ], - "since": 1585948850110, - "till": 1585948850111 -} \ No newline at end of file + "s": 1585948850110, + "t": 1585948850111 +}, "rbs":{"d": [], "s": 1585948850110, "t": 1585948850111}} \ No newline at end of file diff --git a/client/src/test/resources/splits_imp_toggle.json b/client/src/test/resources/splits_imp_toggle.json index 5295f239b..18c5b0aaa 100644 --- a/client/src/test/resources/splits_imp_toggle.json +++ b/client/src/test/resources/splits_imp_toggle.json @@ -1,155 +1,156 @@ { - "splits": [ - { - "trafficTypeName": "user", - "name": "without_impression_toggle", - "trafficAllocation": 24, - "trafficAllocationSeed": -172559061, - "seed": -906334215, - "status": "ACTIVE", - "killed": true, - "defaultTreatment": "off", - "changeNumber": 1585948717645, - "algo": 2, - "configurations": {}, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "ff": { + "d": [ + { + "trafficTypeName": "user", + "name": "without_impression_toggle", + "trafficAllocation": 24, + "trafficAllocationSeed": -172559061, + "seed": -906334215, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1585948717645, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "off", + "size": 0 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "impression_toggle_on", + "trafficAllocation": 24, + "trafficAllocationSeed": -172559061, + "seed": -906334215, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1585948717645, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "impression_toggle_on", - "trafficAllocation": 24, - "trafficAllocationSeed": -172559061, - "seed": -906334215, - "status": "ACTIVE", - "killed": true, - "defaultTreatment": "off", - "changeNumber": 1585948717645, - "algo": 2, - "configurations": {}, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 100 + }, { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "off", + "size": 0 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + ], + "label": "default rule" + } + ], + "impressionsDisabled": false + }, + { + "trafficTypeName": "user", + "name": "impression_toggle_off", + "trafficAllocation": 24, + "trafficAllocationSeed": -172559061, + "seed": -906334215, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1585948717645, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "default rule" - } - ], - "impressionsDisabled": false - }, - { - "trafficTypeName": "user", - "name": "impression_toggle_off", - "trafficAllocation": 24, - "trafficAllocationSeed": -172559061, - "seed": -906334215, - "status": "ACTIVE", - "killed": true, - "defaultTreatment": "off", - "changeNumber": 1585948717645, - "algo": 2, - "configurations": {}, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "default rule" - } - ], - "impressionsDisabled": true - } - ], - "since": -1, - "till": 1602796638344 -} + ], + "label": "default rule" + } + ], + "impressionsDisabled": true + } + ], + "s": -1, + "t": 1602796638344 + }, "rbs": {"s": -1, "t": -1, "d": []}} diff --git a/client/src/test/resources/splits_killed.json b/client/src/test/resources/splits_killed.json index 13eed1a6c..6924afc67 100644 --- a/client/src/test/resources/splits_killed.json +++ b/client/src/test/resources/splits_killed.json @@ -1,5 +1,5 @@ -{ - "splits": [ +{"ff": { + "d": [ { "trafficTypeName": "user", "name": "push_test", @@ -86,6 +86,6 @@ ] } ], - "since": 1585948850111, - "till": 1585948850112 -} \ No newline at end of file + "s": 1585948850111, + "t": 1585948850112 +}, "rbs":{"d": [], "s": 1585948850111, "t": 1585948850112}} \ No newline at end of file diff --git a/client/src/test/resources/splits_localhost.json b/client/src/test/resources/splits_localhost.json new file mode 100644 index 000000000..6f4abdb29 --- /dev/null +++ b/client/src/test/resources/splits_localhost.json @@ -0,0 +1,295 @@ +{"ff": { + "d": [ + { + "trafficTypeName": "user", + "name": "push_test", + "trafficAllocation": 100, + "trafficAllocationSeed": -2092979940, + "seed": 105482719, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on_default", + "changeNumber": 1585948850109, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "admin", + "mauro" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on_whitelist", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "V1", + "size": 0 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "tinchotest", + "trafficAllocation": 24, + "trafficAllocationSeed": -172559061, + "seed": -906334215, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1585948717645, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "test_split", + "trafficAllocation": 100, + "trafficAllocationSeed": 1582960494, + "seed": 1842944006, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1582741588594, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + }, + { + "changeNumber": 10, + "trafficTypeName": "user", + "name": "rbs_flag", + "trafficAllocation": 100, + "trafficAllocationSeed": 1828377380, + "seed": -286617921, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "IN_RULE_BASED_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "sample_rule_based_segment" + } + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "in rule based segment sample_rule_based_segment" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "ALL_KEYS", + "negate": false + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ], + "configurations": {}, + "sets": [], + "impressionsDisabled": false + } + ], + "s": -1, + "t": -1 +}, "rbs":{"d": [ + { + "changeNumber": 5, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + }, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + } + ] + }], "s": -1, "t": -1} +} diff --git a/client/src/test/resources/splits_prereq.json b/client/src/test/resources/splits_prereq.json new file mode 100644 index 000000000..5efa7feda --- /dev/null +++ b/client/src/test/resources/splits_prereq.json @@ -0,0 +1,293 @@ +{"ff": { + "d": [ + { + "trafficTypeName": "user", + "name": "test_prereq", + "prerequisites": [ + { "n": "feature_segment", "ts": ["off", "def_test"] }, + { "n": "rbs_flag", "ts": ["on"] } + ], + "trafficAllocation": 100, + "trafficAllocationSeed": 1582960494, + "seed": 1842944006, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "def_treatment", + "changeNumber": 1582741588594, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "default rule" + } + ] + }, + { + "name":"feature_segment", + "trafficTypeId":"u", + "trafficTypeName":"User", + "trafficAllocation": 100, + "trafficAllocationSeed": 1582960494, + "seed":-1177551240, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"def_test", + "changeNumber": 1582741588594, + "algo": 2, + "configurations": {}, + "conditions":[ + { + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "matcherType":"IN_SEGMENT", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"segment-test" + }, + "whitelistMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + }, + { + "treatment":"off", + "size":0 + } + ], + "label": "default label" + } + ] + }, + { + "changeNumber": 10, + "trafficTypeName": "user", + "name": "rbs_flag", + "trafficAllocation": 100, + "trafficAllocationSeed": 1828377380, + "seed": -286617921, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "IN_RULE_BASED_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "sample_rule_based_segment" + } + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "in rule based segment sample_rule_based_segment" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "ALL_KEYS", + "negate": false + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ], + "configurations": {}, + "sets": [], + "impressionsDisabled": false + }, + { + "trafficTypeName": "user", + "name": "prereq_chain", + "prerequisites": [ + { "n": "test_prereq", "ts": ["on"] } + ], + "trafficAllocation": 100, + "trafficAllocationSeed": -2092979940, + "seed": 105482719, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on_default", + "changeNumber": 1585948850109, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "bilal@split.io" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on_whitelist", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "V1", + "size": 0 + } + ], + "label": "default rule" + } + ] + } + ], + "s": -1, + "t": 1585948850109 +}, "rbs":{"d": [ + { + "changeNumber": 5, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + } + ] + }], "s": -1, "t": 1585948850109} +} diff --git a/okhttp-modules/pom.xml b/okhttp-modules/pom.xml index 54d9417c3..a8645f9ca 100644 --- a/okhttp-modules/pom.xml +++ b/okhttp-modules/pom.xml @@ -5,10 +5,10 @@ java-client-parent io.split.client - 4.15.0 + 4.16.0 4.0.0 - 4.15.0 + 4.16.0 okhttp-modules jar http-modules diff --git a/pluggable-storage/pom.xml b/pluggable-storage/pom.xml index b9cefb432..21b0bdac8 100644 --- a/pluggable-storage/pom.xml +++ b/pluggable-storage/pom.xml @@ -6,7 +6,7 @@ java-client-parent io.split.client - 4.15.0 + 4.16.0 2.1.0 diff --git a/pom.xml b/pom.xml index 0a11e0a68..7b21f1ecc 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.split.client java-client-parent - 4.15.0 + 4.16.0 diff --git a/redis-wrapper/pom.xml b/redis-wrapper/pom.xml index 6577d75f7..301739ce4 100644 --- a/redis-wrapper/pom.xml +++ b/redis-wrapper/pom.xml @@ -6,7 +6,7 @@ java-client-parent io.split.client - 4.15.0 + 4.16.0 redis-wrapper 3.1.1 diff --git a/testing/pom.xml b/testing/pom.xml index 5cbde3700..0fae6cdf6 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -5,11 +5,11 @@ io.split.client java-client-parent - 4.15.0 + 4.16.0 java-client-testing jar - 4.15.0 + 4.16.0 Java Client For Testing Testing suite for Java SDK for Split