diff --git a/client/CHANGES.txt b/client/CHANGES.txt index aeca40583..f05300876 100644 --- a/client/CHANGES.txt +++ b/client/CHANGES.txt @@ -1,5 +1,6 @@ -4.4.0 (Oct 29, 2021) +4.4.0 (Jan 11, 2022) - Added support for Redis to keep consistency across multiple SDK instances. +- Added logic to fetch multiple splits at once on GetTreatments/GetTreatmentsWithChanges. 4.3.0 (Oct 19, 2021) - Added support for the SDK to run with a custom implementation of it's internal storage modules, enabling customers to implement this caching in any storage technology of choice and connect it to the SDK instance itself which will use it instead of the in-memory structures. diff --git a/client/pom.xml b/client/pom.xml index 95cb80bd8..bc8ae67a7 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,17 +5,13 @@ io.split.client java-client-parent - 4.4.0-beta + 4.4.0 java-client jar Java Client Java SDK for Split - - 1.0.0-beta - - @@ -126,7 +122,7 @@ io.split.client pluggable-storage - ${pluggable.storage} + 1.0.0 compile diff --git a/client/src/main/java/io/split/client/AlwaysReturnControlSplitClient.java b/client/src/main/java/io/split/client/AlwaysReturnControlSplitClient.java deleted file mode 100644 index 7c9573716..000000000 --- a/client/src/main/java/io/split/client/AlwaysReturnControlSplitClient.java +++ /dev/null @@ -1,80 +0,0 @@ -package io.split.client; - -import io.split.client.api.Key; -import io.split.client.api.SplitResult; -import io.split.grammar.Treatments; - -import java.util.Map; -import java.util.concurrent.TimeoutException; - -/** - * A SplitClient that ensures that all features are turned off for all users. - * Useful for testing - * - * @author adil - */ -public class AlwaysReturnControlSplitClient implements SplitClient { - - private static final SplitResult RESULT_CONTROL = new SplitResult(Treatments.CONTROL, null); - - @Override - public String getTreatment(String key, String split) { - return Treatments.CONTROL; - } - - @Override - public String getTreatment(String key, String split, Map attributes) { - return Treatments.CONTROL; - } - - @Override - public String getTreatment(Key key, String split, Map attributes) { - return Treatments.CONTROL; - } - - @Override - public SplitResult getTreatmentWithConfig(String key, String split) { - return RESULT_CONTROL; - } - - @Override - public SplitResult getTreatmentWithConfig(String key, String split, Map attributes) { - return RESULT_CONTROL; - } - - @Override - public SplitResult getTreatmentWithConfig(Key key, String split, Map attributes) { - return RESULT_CONTROL; - } - - @Override - public void destroy() { - - } - - @Override - public boolean track(String key, String trafficType, String eventType) { - return false; - } - - @Override - public boolean track(String key, String trafficType, String eventType, double value) { - return false; - } - - @Override - public boolean track(String key, String trafficType, String eventType, Map properties) { - return false; - } - - @Override - public boolean track(String key, String trafficType, String eventType, double value, Map properties) { - return false; - } - - @Override - public void blockUntilReady() throws TimeoutException, InterruptedException { - //AlwaysReturnControl is always ready - } - -} diff --git a/client/src/main/java/io/split/client/SplitClient.java b/client/src/main/java/io/split/client/SplitClient.java index 88436bce6..da3debb63 100644 --- a/client/src/main/java/io/split/client/SplitClient.java +++ b/client/src/main/java/io/split/client/SplitClient.java @@ -3,6 +3,7 @@ import io.split.client.api.Key; import io.split.client.api.SplitResult; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; @@ -139,6 +140,133 @@ public interface SplitClient { */ SplitResult getTreatmentWithConfig(Key key, String split, Map attributes); + /** + * Returns a map of feature name and treatments to show this key for these features. The set of treatments + * for a feature can be configured on the Split web console. + *

+ *

+ * This method returns for each feature the string 'control' if: + *

    + *
  1. Any of the parameters were null
  2. + *
  3. There was an exception in evaluating the treatment
  4. + *
  5. The SDK does not know of the existence of this feature
  6. + *
  7. The feature was deleted through the web console.
  8. + *
+ * 'control' is a reserved treatment (you cannot create a treatment with the + * same name) to highlight these exceptional circumstances. + *

+ *

+ * The sdk returns for each feature the default treatment of this feature if: + *

    + *
  1. The feature was killed
  2. + *
  3. The key did not match any of the conditions in the feature roll-out plan
  4. + *
+ * The default treatment of a feature is set on the Split web console. + *

+ *

+ * This method does not throw any exceptions. It also never returns null. + * + * @param key a unique key of your customer (e.g. user_id, user_email, account_id, etc.) MUST not be null. + * @param splits the features we want to evaluate. MUST NOT be null. + * @return for each feature the evaluated treatment, the default treatment for each feature, or 'control'. + */ + Map getTreatments(String key, List splits); + + /** + * This method is useful when you want to determine the treatments to show + * to a customer (user, account etc.) based on an attribute of that customer + * instead of their key. + *

+ *

+ * Examples include showing different treatments to users on trial plan + * vs. premium plan. Another example is to show different treatments + * to users created after a certain date. + * + * @param key a unique key of your customer (e.g. user_id, user_email, account_id, etc.) MUST not be null. + * @param splits the features we want to evaluate. MUST NOT be null. + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. + * @return the evaluated treatment, the default treatment of this feature, or 'control'. + */ + Map getTreatments(String key, List splits, Map attributes); + + /** + * To understand why this method is useful, consider the following simple Split as an example: + * + * if user is in segment employees then split 100%:on + * else if user is in segment all then split 20%:on,80%:off + * + * There are two concepts here: matching and bucketing. Matching + * refers to ‘user is in segment employees’ or ‘user is in segment + * all’ whereas bucketing refers to ‘100%:on’ or ‘20%:on,80%:off’. + * + * By default, the same customer key is used for both matching and + * bucketing. However, for some advanced use cases, you may want + * to use different keys. For such cases, use this method. + * + * As an example, suppose you want to rollout to percentages of + * users in specific accounts. You can achieve that by matching + * via account id, but bucketing by user id. + * + * Another example is when you want to ensure that a user continues to get + * the same treatment after they sign up for your product that they used + * to get when they were simply a visitor to your site. In that case, + * before they sign up, you can use their visitor id for both matching and bucketing, but + * post log-in you can use their user id for matching and visitor id for bucketing. + * + * + * @param key the matching and bucketing keys. MUST NOT be null. + * @param splits the features we want to evaluate. MUST NOT be null. + * @param attributes of the entity (user, account etc.) to use in evaluation. Can be null or empty. + * + * @return for each feature the evaluated treatment, the default treatment of the feature, or 'control'. + */ + Map getTreatments(Key key, List splits, Map attributes); + + /** + * Same as {@link #getTreatments(String, List)} but it returns the configuration associated to the + * matching treatments if any. Otherwise {@link SplitResult.configurations()} will be null. + *

+ *

+ * Examples include showing a different treatment to users on trial plan + * vs. premium plan. Another example is to show a different treatment + * to users created after a certain date. + * + * @param key a unique key of your customer (e.g. user_id, user_email, account_id, etc.) MUST not be null. + * @param splits the features we want to evaluate. MUST NOT be null. + * @return Map containing for each feature the evaluated treatment (the default treatment of this feature, or 'control') and + * a configuration associated to this treatment if set. + */ + Map getTreatmentsWithConfig(String key, List splits); + + /** + * Same as {@link #getTreatments(String, List, Map)} but it returns for each feature the configuration associated to the + * matching treatment if any. Otherwise {@link SplitResult.configurations()} will be null. + *

+ *

+ * Examples include showing a different treatment to users on trial plan + * vs. premium plan. Another example is to show a different treatment + * to users created after a certain date. + * + * @param key a unique key of your customer (e.g. user_id, user_email, account_id, etc.) MUST not be null. + * @param splits the features we want to evaluate. MUST NOT be null. + * @param attributes of the customer (user, account etc.) to use in evaluation. Can be null or empty. + * @return for each feature a SplitResult containing the evaluated treatment (the default treatment of this feature, or 'control') and + * a configuration associated to this treatment if set. + */ + Map getTreatmentsWithConfig(String key, List splits, Map attributes); + + /** + * Same as {@link #getTreatments(Key, List, Map)} but it returns for each feature the configuration associated to the + * matching treatment if any. Otherwise {@link SplitResult.configurations()} will be null. + * + * @param key the matching and bucketing keys. MUST NOT be null. + * @param splits the features we want to evaluate. MUST NOT be null. + * @param attributes of the entity (user, account etc.) to use in evaluation. Can be null or empty. + * + * @return for each feature a SplitResult containing the evaluated treatment (the default treatment of this feature, or 'control') and + * a configuration associated to this treatment if set. + */ + Map getTreatmentsWithConfig(Key key, List splits, Map attributes); /** * Destroys the background processes and clears the cache, releasing the resources used by diff --git a/client/src/main/java/io/split/client/SplitClientConfig.java b/client/src/main/java/io/split/client/SplitClientConfig.java index 166618fd6..a02f85a49 100644 --- a/client/src/main/java/io/split/client/SplitClientConfig.java +++ b/client/src/main/java/io/split/client/SplitClientConfig.java @@ -780,6 +780,16 @@ public Builder operationMode(OperationMode mode) { return this; } + /** + * + * @param storage mode + * @return this builder + */ + public Builder storageMode(StorageMode mode) { + _storageMode = mode; + return this; + } + /** * Storage wrapper * diff --git a/client/src/main/java/io/split/client/SplitClientImpl.java b/client/src/main/java/io/split/client/SplitClientImpl.java index 0560fc43e..3a455be2c 100644 --- a/client/src/main/java/io/split/client/SplitClientImpl.java +++ b/client/src/main/java/io/split/client/SplitClientImpl.java @@ -22,11 +22,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkNotNull; @@ -100,6 +104,40 @@ public SplitResult getTreatmentWithConfig(Key key, String split, Map getTreatments(String key, List splits) { + return getTreatments(key, splits, Collections.emptyMap()); + } + + @Override + public Map getTreatments(String key, List splits, Map attributes) { + return getTreatmentsWithConfigInternal(key, null, splits, attributes, MethodEnum.TREATMENTS) + .entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment())); + } + + @Override + public Map getTreatments(Key key, List splits, Map attributes) { + return getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), splits, attributes, MethodEnum.TREATMENTS) + .entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().treatment())); + } + + @Override + public Map getTreatmentsWithConfig(String key, List splits) { + return getTreatmentsWithConfigInternal(key, null, splits, Collections.emptyMap(), MethodEnum.TREATMENTS_WITH_CONFIG); + } + + @Override + public Map getTreatmentsWithConfig(String key, List splits, Map attributes) { + return getTreatmentsWithConfigInternal(key, null, splits, attributes, MethodEnum.TREATMENTS_WITH_CONFIG); + } + + @Override + public Map getTreatmentsWithConfig(Key key, List splits, Map attributes) { + return getTreatmentsWithConfigInternal(key.matchingKey(), key.bucketingKey(), splits, attributes, MethodEnum.TREATMENTS_WITH_CONFIG); + } + @Override public boolean track(String key, String trafficType, String eventType) { Event event = createEvent(key, trafficType, eventType); @@ -185,10 +223,8 @@ private boolean track(Event event) { private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bucketingKey, String split, Map attributes, MethodEnum methodEnum) { long initTime = System.currentTimeMillis(); try { - if(!_gates.isSDKReady()){ - _log.warn(methodEnum.getMethod() + ": the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method"); - _telemetryConfigProducer.recordNonReadyUsage(); - } + checkSDKReady(methodEnum); + if (_container.isDestroyed()) { _log.error("Client has already been destroyed - no calls possible"); return SPLIT_RESULT_CONTROL; @@ -214,8 +250,8 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu if (result.treatment.equals(Treatments.CONTROL) && result.label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) { _log.warn( - "getTreatment: you passed \"" + split + "\" that does not exist in this environment, " + - "please double check what Splits exist in the web console."); + "%s: you passed \"" + split + "\" that does not exist in this environment, " + + "please double check what Splits exist in the web console.", methodEnum.getMethod()); return SPLIT_RESULT_CONTROL; } @@ -243,10 +279,68 @@ private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bu } } + private Map getTreatmentsWithConfigInternal(String matchingKey, String bucketingKey, List splits, Map attributes, MethodEnum methodEnum) { + long initTime = System.currentTimeMillis(); + if(splits == null) { + _log.error("%s: split_names must be a non-empty array", methodEnum.getMethod()); + return new HashMap<>(); + } + try{ + checkSDKReady(methodEnum); + if (_container.isDestroyed()) { + _log.error("Client has already been destroyed - no calls possible"); + return createMapControl(splits); + } + + if (!KeyValidator.isValid(matchingKey, "matchingKey", _config.maxStringLength(), methodEnum.getMethod())) { + return createMapControl(splits); + } + + if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) { + return createMapControl(splits); + } + else if(splits.isEmpty()) { + _log.error("%s: split_names must be a non-empty array", methodEnum.getMethod()); + return new HashMap<>(); + } + splits = SplitNameValidator.areValid(splits, methodEnum.getMethod()); + Map evaluatorResult = _evaluator.evaluateFeatures(matchingKey, bucketingKey, splits, attributes); + List impressions = new ArrayList<>(); + Map result = new HashMap<>(); + evaluatorResult.keySet().forEach(t -> { + if (evaluatorResult.get(t).treatment.equals(Treatments.CONTROL) && evaluatorResult.get(t).label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) { + _log.warn( + "%s: you passed \"" + t + "\" that does not exist in this environment, " + + "please double check what Splits exist in the web console.", methodEnum.getMethod()); + result.put(t, SPLIT_RESULT_CONTROL); + } + else { + result.put(t,new SplitResult(evaluatorResult.get(t).treatment, evaluatorResult.get(t).configurations)); + impressions.add(new Impression(matchingKey, bucketingKey, t, evaluatorResult.get(t).treatment, System.currentTimeMillis(), evaluatorResult.get(t).label, evaluatorResult.get(t).changeNumber, attributes)); + } + }); + + _telemetryEvaluationProducer.recordLatency(methodEnum, System.currentTimeMillis()-initTime); + //Track of impressions + if(impressions.size() > 0) { + _impressionManager.track(impressions); + } + return result; + } catch (Exception e) { + try { + _telemetryEvaluationProducer.recordException(methodEnum); + _log.error("CatchAll Exception", e); + } catch (Exception e1) { + // ignore + } + return createMapControl(splits); + } + } + private void recordStats(String matchingKey, String bucketingKey, String split, long start, String result, String operation, String label, Long changeNumber, Map attributes) { try { - _impressionManager.track(new Impression(matchingKey, bucketingKey, split, result, System.currentTimeMillis(), label, changeNumber, attributes)); + _impressionManager.track(Stream.of(new Impression(matchingKey, bucketingKey, split, result, System.currentTimeMillis(), label, changeNumber, attributes)).collect(Collectors.toList())); } catch (Throwable t) { _log.error("Exception", t); } @@ -260,4 +354,17 @@ private Event createEvent(String key, String trafficType, String eventType) { event.timestamp = System.currentTimeMillis(); return event; } + + private void checkSDKReady(MethodEnum methodEnum) { + if(!_gates.isSDKReady()){ + _log.warn(methodEnum.getMethod() + ": the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method"); + _telemetryConfigProducer.recordNonReadyUsage(); + } + } + + private Map createMapControl(List splits) { + Map result = new HashMap<>(); + splits.forEach(s -> result.put(s, SPLIT_RESULT_CONTROL)); + return result; + } } diff --git a/client/src/main/java/io/split/client/SplitFactoryBuilder.java b/client/src/main/java/io/split/client/SplitFactoryBuilder.java index dcb5a0bdd..dfaba9b69 100644 --- a/client/src/main/java/io/split/client/SplitFactoryBuilder.java +++ b/client/src/main/java/io/split/client/SplitFactoryBuilder.java @@ -41,7 +41,7 @@ public static synchronized SplitFactory build(String apiToken, SplitClientConfig if (LocalhostSplitFactory.LOCALHOST.equals(apiToken)) { return LocalhostSplitFactory.createLocalhostSplitFactory(config); } - if (StorageMode.PLUGGABLE.equals(config.storageMode())){ + if (StorageMode.PLUGGABLE.equals(config.storageMode()) || StorageMode.REDIS.equals(config.storageMode())){ return new SplitFactoryImpl(apiToken, config, config.customStorageWrapper()); } return new SplitFactoryImpl(apiToken, config); diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index c2c41f0c4..a80ee88db 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -339,7 +339,7 @@ public synchronized void destroy() { } } else if(OperationMode.CONSUMER.equals(_operationMode)) { - _safeUserStorageWrapper.close(); + _safeUserStorageWrapper.disconnect(); } _apiKeyCounter.remove(_apiToken); isTerminated = true; diff --git a/client/src/main/java/io/split/client/impressions/ImpressionsManager.java b/client/src/main/java/io/split/client/impressions/ImpressionsManager.java index e55d23b42..ac1f8a9b4 100644 --- a/client/src/main/java/io/split/client/impressions/ImpressionsManager.java +++ b/client/src/main/java/io/split/client/impressions/ImpressionsManager.java @@ -1,5 +1,7 @@ package io.split.client.impressions; +import java.util.List; + public interface ImpressionsManager { public enum Mode { @@ -7,11 +9,11 @@ public enum Mode { DEBUG } - void track(Impression impression); + void track(List impressions); final class NoOpImpressionsManager implements ImpressionsManager { @Override - public void track(Impression impression) { /* do nothing */ } + public void track(List impressions) { /* do nothing */ } } } diff --git a/client/src/main/java/io/split/client/impressions/ImpressionsManagerImpl.java b/client/src/main/java/io/split/client/impressions/ImpressionsManagerImpl.java index d77ba23ec..ed23b337a 100644 --- a/client/src/main/java/io/split/client/impressions/ImpressionsManagerImpl.java +++ b/client/src/main/java/io/split/client/impressions/ImpressionsManagerImpl.java @@ -15,12 +15,14 @@ import java.io.Closeable; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @@ -102,33 +104,31 @@ private ImpressionsManagerImpl(CloseableHttpClient client, } } - private static boolean shouldQueueImpression(Impression i) { + private boolean shouldQueueImpression(Impression i) { return Objects.isNull(i.pt()) || ImpressionUtils.truncateTimeframe(i.pt()) != ImpressionUtils.truncateTimeframe(i.time()); } - //TODO create a track method with multiple impressions. @Override - public void track(Impression impression) { - if (null == impression) { + public void track(List impressions) { + if (null == impressions) { return; } + int totalImpressions = impressions.size(); - impression = _addPreviousTimeEnabled ? impression.withPreviousTime(_impressionObserver.testAndSet(impression)) : impression; - _listener.log(impression); + impressions = processImpressions(impressions); - if (_isOptimized) { - _counter.inc(impression.split(), impression.time(), 1); - if (!shouldQueueImpression(impression)) { - _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED, 1); - return; - } + if (totalImpressions > impressions.size()) { + _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED, totalImpressions-impressions.size()); + totalImpressions = impressions.size(); } - if (!_impressionsStorageProducer.put(KeyImpression.fromImpression(impression))) { - _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 1); - return; + long queued = _impressionsStorageProducer.put(impressions.stream().map(KeyImpression::fromImpression).collect(Collectors.toList())); + if (queued < totalImpressions) { + _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, totalImpressions-queued); } - _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_QUEUED, 1); + _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_QUEUED, queued); + + impressions.forEach(imp -> _listener.log(imp)); } @Override @@ -205,4 +205,28 @@ private boolean shouldBeOptimized() { /* package private */ ImpressionCounter getCounter() { return _counter; } + + /** + * Filter in case of deduping and format impressions to let them ready to be sent. + * @param impressions + * @return + */ + private List processImpressions(List impressions) { + if(!_addPreviousTimeEnabled) { //Only STANDALONE Mode needs to iterate over impressions to add previous time. + return impressions; + } + + List impressionsToQueue = new ArrayList<>(); + for(Impression impression : impressions) { + impression = impression.withPreviousTime(_impressionObserver.testAndSet(impression)); + if (_isOptimized) { + _counter.inc(impression.split(), impression.time(), 1); + if(!shouldQueueImpression(impression)) { + continue; + } + } + impressionsToQueue.add(impression); + } + return impressionsToQueue; + } } diff --git a/client/src/main/java/io/split/client/impressions/ImpressionsStorageProducer.java b/client/src/main/java/io/split/client/impressions/ImpressionsStorageProducer.java index da30560a5..51bfb977c 100644 --- a/client/src/main/java/io/split/client/impressions/ImpressionsStorageProducer.java +++ b/client/src/main/java/io/split/client/impressions/ImpressionsStorageProducer.java @@ -5,6 +5,5 @@ import java.util.List; public interface ImpressionsStorageProducer { - boolean put(KeyImpression imps); //TODO get rid when getTreatmens is implemented. - boolean put(List imps); // TODO start using it for all impression operations and split impressionlistener vs storage producer. + long put(List imps); } diff --git a/client/src/main/java/io/split/client/impressions/InMemoryImpressionsStorage.java b/client/src/main/java/io/split/client/impressions/InMemoryImpressionsStorage.java index 0ed5b7af2..90d6b9a49 100644 --- a/client/src/main/java/io/split/client/impressions/InMemoryImpressionsStorage.java +++ b/client/src/main/java/io/split/client/impressions/InMemoryImpressionsStorage.java @@ -38,8 +38,7 @@ public boolean isFull() { return _queue.remainingCapacity() == 0; } - @Override - public boolean put(KeyImpression imp) { + private boolean put(KeyImpression imp) { try { return _queue.offer(imp); } catch (ClassCastException | NullPointerException | IllegalArgumentException e) { @@ -49,7 +48,12 @@ public boolean put(KeyImpression imp) { } @Override - public boolean put(List imps) { - return false; + public long put(List imps) { + return imps.stream().reduce(0, (accum, current) -> { + if(this.put(current)) { + return accum + 1; + } + return accum; + }, Integer::sum).longValue(); } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/evaluator/Evaluator.java b/client/src/main/java/io/split/engine/evaluator/Evaluator.java index fbcc52c35..b1f4e2aba 100644 --- a/client/src/main/java/io/split/engine/evaluator/Evaluator.java +++ b/client/src/main/java/io/split/engine/evaluator/Evaluator.java @@ -1,7 +1,9 @@ package io.split.engine.evaluator; +import java.util.List; import java.util.Map; public interface Evaluator { EvaluatorImp.TreatmentLabelAndChangeNumber evaluateFeature(String matchingKey, String bucketingKey, String split, Map attributes); + Map evaluateFeatures(String matchingKey, String bucketingKey, List splits, Map attributes); } 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 7dd52aed9..d404b5301 100644 --- a/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java +++ b/client/src/main/java/io/split/engine/evaluator/EvaluatorImp.java @@ -12,6 +12,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.List; import java.util.Map; import static com.google.common.base.Preconditions.checkNotNull; @@ -33,22 +35,19 @@ public EvaluatorImp(SplitCacheConsumer splitCacheConsumer, SegmentCacheConsumer @Override public TreatmentLabelAndChangeNumber evaluateFeature(String matchingKey, String bucketingKey, String split, Map attributes) { - try { - ParsedSplit parsedSplit = _splitCacheConsumer.get(split); - - if (parsedSplit == null) { - return new TreatmentLabelAndChangeNumber(Treatments.CONTROL, Labels.DEFINITION_NOT_FOUND); - } + ParsedSplit parsedSplit = _splitCacheConsumer.get(split); + return evaluateParsedSplit(matchingKey, bucketingKey, split, attributes, parsedSplit); + } - return getTreatment(matchingKey, bucketingKey, parsedSplit, attributes); - } - catch (ChangeNumberExceptionWrapper e) { - _log.error("Evaluator Exception", e.wrappedException()); - return new EvaluatorImp.TreatmentLabelAndChangeNumber(Treatments.CONTROL, Labels.EXCEPTION, e.changeNumber()); - } catch (Exception e) { - _log.error("Evaluator Exception", e); - return new EvaluatorImp.TreatmentLabelAndChangeNumber(Treatments.CONTROL, Labels.EXCEPTION); + @Override + public Map evaluateFeatures(String matchingKey, String bucketingKey, List splits, Map attributes) { + Map results = new HashMap<>(); + Map parsedSplits = _splitCacheConsumer.fetchMany(splits); + if(parsedSplits == null) { + return results; } + splits.forEach(s -> results.put(s, evaluateParsedSplit(matchingKey, bucketingKey, s, attributes, parsedSplits.get(s)))); + return results; } /** @@ -108,6 +107,23 @@ private TreatmentLabelAndChangeNumber getTreatment(String matchingKey, String bu } } + private TreatmentLabelAndChangeNumber evaluateParsedSplit(String matchingKey, String bucketingKey, String split, 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()); + return new EvaluatorImp.TreatmentLabelAndChangeNumber(Treatments.CONTROL, Labels.EXCEPTION, e.changeNumber()); + } catch (Exception e) { + _log.error("Evaluator Exception", e); + return new EvaluatorImp.TreatmentLabelAndChangeNumber(Treatments.CONTROL, Labels.EXCEPTION); + } + } + public static final class TreatmentLabelAndChangeNumber { public final String treatment; public final String label; diff --git a/client/src/main/java/io/split/inputValidation/SplitNameValidator.java b/client/src/main/java/io/split/inputValidation/SplitNameValidator.java index 06f00b72d..a2f784349 100644 --- a/client/src/main/java/io/split/inputValidation/SplitNameValidator.java +++ b/client/src/main/java/io/split/inputValidation/SplitNameValidator.java @@ -3,7 +3,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; public class SplitNameValidator { private static final Logger _log = LoggerFactory.getLogger(SplitNameValidator.class); @@ -27,4 +30,11 @@ public static Optional isValid(String name, String method) { return Optional.of(name); } + + public static List areValid(List splits, String method) { + return splits.stream().distinct() + .map(s -> isValid(s, method).orElse(null)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } } diff --git a/client/src/main/java/io/split/storages/SplitCacheConsumer.java b/client/src/main/java/io/split/storages/SplitCacheConsumer.java index 90c2b76a2..e802d247f 100644 --- a/client/src/main/java/io/split/storages/SplitCacheConsumer.java +++ b/client/src/main/java/io/split/storages/SplitCacheConsumer.java @@ -4,11 +4,12 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Set; public interface SplitCacheConsumer extends SplitCacheCommons{ ParsedSplit get(String name); Collection getAll(); - Collection fetchMany(List names); + Map fetchMany(List names); boolean trafficTypeExists(String trafficTypeName); } diff --git a/client/src/main/java/io/split/storages/enums/StorageMode.java b/client/src/main/java/io/split/storages/enums/StorageMode.java index 63192572f..904a14f60 100644 --- a/client/src/main/java/io/split/storages/enums/StorageMode.java +++ b/client/src/main/java/io/split/storages/enums/StorageMode.java @@ -2,5 +2,6 @@ public enum StorageMode { MEMORY, - PLUGGABLE + PLUGGABLE, + REDIS } 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 c326f36fe..fc82b14d6 100644 --- a/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java +++ b/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java @@ -11,7 +11,9 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; @@ -57,16 +59,10 @@ public Collection getAll() { } @Override - public Collection fetchMany(List names) { - List splits = new ArrayList<>(); + public Map fetchMany(List names) { + Map splits = new HashMap<>(); - for (String name : names) { - ParsedSplit split = _concurrentMap.get(name); - - if (split != null) { - splits.add(split); - } - } + names.forEach(s -> splits.put(s, _concurrentMap.get(s))); return splits; } diff --git a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomImpressionAdapterProducer.java b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomImpressionAdapterProducer.java index 80fe5db96..185bb8a1c 100644 --- a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomImpressionAdapterProducer.java +++ b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomImpressionAdapterProducer.java @@ -37,18 +37,12 @@ public UserCustomImpressionAdapterProducer(CustomStorageWrapper customStorageWra _safeUserStorageWrapper = new SafeUserStorageWrapper(checkNotNull(customStorageWrapper)); _metadata = metadata; } - @Override - public boolean put(KeyImpression imps) { - return this.put(Collections.singletonList(imps)); - } @Override - public boolean put(List imps) { + public long put(List imps) { //Impression List impressions = imps.stream().map(keyImp -> _json.toJson(new ImpressionConsumer(_metadata, keyImp))).collect(Collectors.toList()); - _safeUserStorageWrapper.pushItems(PrefixAdapter.buildImpressions(), impressions); - return true; + return _safeUserStorageWrapper.pushItems(PrefixAdapter.buildImpressions(), impressions); } - } diff --git a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterConsumer.java b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterConsumer.java index 9acee2984..fb10559df 100644 --- a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterConsumer.java +++ b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterConsumer.java @@ -14,7 +14,9 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.stream.Collectors; @@ -83,12 +85,17 @@ public boolean trafficTypeExists(String trafficTypeName) { } @Override - public Collection fetchMany(List names) { + public Map fetchMany(List names) { + Map result = new HashMap<>(); List wrapperResponse = _safeUserStorageWrapper.getItems(PrefixAdapter.buildFetchManySplits(names)); if(wrapperResponse == null) { - return new ArrayList<>(); + return result; } - return stringsToParsedSplits(wrapperResponse); + List parsedSplits = stringsToParsedSplits(wrapperResponse); + for(int i=0; i < parsedSplits.size(); i++) { + result.put(names.get(i), parsedSplits.get(i)); + } + return result; } @Override @@ -98,9 +105,14 @@ public Set getSegments() { } private List stringsToParsedSplits(List elements) { - return elements.stream() - .map(s -> Json.fromJson(s, Split.class)) - .map(_splitParser::parse) - .collect(Collectors.toList()); + List result = new ArrayList<>(); + for(String s : elements) { + if(s != null) { + result.add(_splitParser.parse(Json.fromJson(s, Split.class))); + continue; + } + result.add(null); + } + return result; } } diff --git a/client/src/main/java/io/split/storages/pluggable/domain/SafeUserStorageWrapper.java b/client/src/main/java/io/split/storages/pluggable/domain/SafeUserStorageWrapper.java index 1ee146a07..011da4a75 100644 --- a/client/src/main/java/io/split/storages/pluggable/domain/SafeUserStorageWrapper.java +++ b/client/src/main/java/io/split/storages/pluggable/domain/SafeUserStorageWrapper.java @@ -106,12 +106,13 @@ public long decrement(String key, long value) { } @Override - public void pushItems(String key, List items) { + public long pushItems(String key, List items) { try { - _customStorageWrapper.pushItems(key, items); + return _customStorageWrapper.pushItems(key, items); } catch (Exception e) { _log.error(String.format("error pushing items with key '%s' from storage. Error: '%s'", key, e.getMessage())); + return 0; } } @@ -191,12 +192,12 @@ public boolean connect(){ } @Override - public boolean close(){ + public boolean disconnect(){ try { - return _customStorageWrapper.close(); + return _customStorageWrapper.disconnect(); } catch (Exception e) { - _log.error(String.format("error trying to connect. Error: '%s'" , e.getMessage())); + _log.error(String.format("error trying to disconnect. Error: '%s'" , e.getMessage())); return false; } } diff --git a/client/src/test/java/io/split/client/SplitClientImplTest.java b/client/src/test/java/io/split/client/SplitClientImplTest.java index 9b76a070e..e2d00d9c5 100644 --- a/client/src/test/java/io/split/client/SplitClientImplTest.java +++ b/client/src/test/java/io/split/client/SplitClientImplTest.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.common.reflect.TypeToken; import io.split.client.api.Key; import io.split.client.api.SplitResult; import io.split.client.dtos.ConditionType; @@ -37,6 +38,7 @@ import java.io.IOException; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -45,11 +47,14 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; +import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -692,13 +697,18 @@ public void labels_are_populated() { Map attributes = ImmutableMap.of("age", -20, "acv", "1000000"); assertThat(client.getTreatment("pato@codigo.com", test, attributes), is(equalTo("on"))); - ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(Impression.class); + ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(List.class); verify(impressionsManager).track(impressionCaptor.capture()); - assertThat(impressionCaptor.getValue().appliedRule(), is(equalTo("foolabel"))); + List impressions = impressionCaptor.getValue(); + assertNotNull(impressions); + assertEquals(1, impressions.size()); + Impression impression = impressions.get(0); - assertThat(impressionCaptor.getValue().attributes(), is(attributes)); + assertThat(impression.appliedRule(), is(equalTo("foolabel"))); + + assertThat(impression.attributes(), is(attributes)); } @Test @@ -783,11 +793,13 @@ private void traffic_allocation(String key, int trafficAllocation, int trafficAl assertThat(client.getTreatment(key, test), is(equalTo(expected_treatment_on_or_off))); - ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(Impression.class); + ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(List.class); verify(impressionsManager).track(impressionCaptor.capture()); - - assertThat(impressionCaptor.getValue().appliedRule(), is(equalTo(label))); + assertNotNull(impressionCaptor.getValue()); + assertEquals(1, impressionCaptor.getValue().size()); + Impression impression = (Impression) impressionCaptor.getValue().get(0); + assertThat(impression.appliedRule(), is(equalTo(label))); } /** @@ -835,10 +847,13 @@ public void notInTrafficAllocationDefaultConfig() { assertThat(result.treatment(), is(equalTo(Treatments.OFF))); assertThat(result.config(), is(equalTo("{\"size\" : 30}"))); - ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(Impression.class); + ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(List.class); verify(impressionsManager, times(2)).track(impressionCaptor.capture()); - assertThat(impressionCaptor.getValue().appliedRule(), is(equalTo("not in split"))); + assertNotNull(impressionCaptor.getValue()); + assertEquals(1, impressionCaptor.getValue().size()); + Impression impression = (Impression) impressionCaptor.getValue().get(0); + assertThat(impression.appliedRule(), is(equalTo("not in split"))); } @@ -911,12 +926,17 @@ public void impression_metadata_is_propagated() { assertThat(client.getTreatment("pato@codigo.com", test, attributes), is(equalTo("on"))); - ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(Impression.class); + ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(List.class); verify(impressionsManager).track(impressionCaptor.capture()); - assertThat(impressionCaptor.getValue().appliedRule(), is(equalTo("foolabel"))); - assertThat(impressionCaptor.getValue().attributes(), is(equalTo(attributes))); + + assertNotNull(impressionCaptor.getValue()); + assertEquals(1, impressionCaptor.getValue().size()); + Impression impression = (Impression) impressionCaptor.getValue().get(0); + + assertThat(impression.appliedRule(), is(equalTo("foolabel"))); + assertThat(impression.attributes(), is(equalTo(attributes))); } private Partition partition(String treatment, int size) { @@ -1074,6 +1094,75 @@ public void track_with_invalid_keys() { org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(false))); } + @Test + public void getTreatment_with_invalid_keys() { + String test = "split"; + + 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); + + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + + Assert.assertThat(client.getTreatment("valid", "split"), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.not(Treatments.CONTROL))); + + Assert.assertThat(client.getTreatment("", "split"), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); + + Assert.assertThat(client.getTreatment(null, "split"), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); + + String invalidKeySize = new String(new char[251]).replace('\0', 'a'); + Assert.assertThat(client.getTreatment(invalidKeySize, "split"), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); + + Assert.assertThat(client.getTreatment("valid", ""), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); + + Assert.assertThat(client.getTreatment("valid", null), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); + + String matchingKey = new String(new char[250]).replace('\0', 'a'); + String bucketingKey = new String(new char[250]).replace('\0', 'a'); + Key key = new Key(matchingKey, bucketingKey); + Assert.assertThat(client.getTreatment(key, "split", null), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.not(Treatments.CONTROL))); + + key = new Key("valid", ""); + Assert.assertThat(client.getTreatment(key, "split", null), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); + + key = new Key("", "valid"); + Assert.assertThat(client.getTreatment(key, "split", null), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); + + matchingKey = new String(new char[251]).replace('\0', 'a'); + bucketingKey = new String(new char[250]).replace('\0', 'a'); + key = new Key(matchingKey, bucketingKey); + Assert.assertThat(client.getTreatment(key, "split", null), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.is(Treatments.CONTROL))); + + matchingKey = new String(new char[250]).replace('\0', 'a'); + bucketingKey = new String(new char[251]).replace('\0', 'a'); + key = new Key(matchingKey, bucketingKey); + Assert.assertThat(client.getTreatment(key, "split", null), + org.hamcrest.Matchers.is(org.hamcrest.Matchers.is(Treatments.CONTROL))); + } + @Test public void track_with_properties() { SDKReadinessGates gates = mock(SDKReadinessGates.class); @@ -1180,75 +1269,6 @@ public void track_with_properties() { Assert.assertThat(client.track("key1", "user", "purchase", properties), org.hamcrest.Matchers.is(false)); } - @Test - public void getTreatment_with_invalid_keys() { - String test = "split"; - - 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); - - SDKReadinessGates gates = mock(SDKReadinessGates.class); - SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); - SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); - when(splitCacheConsumer.get(test)).thenReturn(parsedSplit); - - SplitClientImpl client = new SplitClientImpl( - mock(SplitFactory.class), - splitCacheConsumer, - new ImpressionsManager.NoOpImpressionsManager(), - NoopEventsStorageImp.create(), - config, - gates, - new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE - ); - - Assert.assertThat(client.getTreatment("valid", "split"), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.not(Treatments.CONTROL))); - - Assert.assertThat(client.getTreatment("", "split"), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); - - Assert.assertThat(client.getTreatment(null, "split"), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); - - String invalidKeySize = new String(new char[251]).replace('\0', 'a'); - Assert.assertThat(client.getTreatment(invalidKeySize, "split"), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); - - Assert.assertThat(client.getTreatment("valid", ""), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); - - Assert.assertThat(client.getTreatment("valid", null), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); - - String matchingKey = new String(new char[250]).replace('\0', 'a'); - String bucketingKey = new String(new char[250]).replace('\0', 'a'); - Key key = new Key(matchingKey, bucketingKey); - Assert.assertThat(client.getTreatment(key, "split", null), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.not(Treatments.CONTROL))); - - key = new Key("valid", ""); - Assert.assertThat(client.getTreatment(key, "split", null), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); - - key = new Key("", "valid"); - Assert.assertThat(client.getTreatment(key, "split", null), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(Treatments.CONTROL))); - - matchingKey = new String(new char[251]).replace('\0', 'a'); - bucketingKey = new String(new char[250]).replace('\0', 'a'); - key = new Key(matchingKey, bucketingKey); - Assert.assertThat(client.getTreatment(key, "split", null), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.is(Treatments.CONTROL))); - - matchingKey = new String(new char[250]).replace('\0', 'a'); - bucketingKey = new String(new char[251]).replace('\0', 'a'); - key = new Key(matchingKey, bucketingKey); - Assert.assertThat(client.getTreatment(key, "split", null), - org.hamcrest.Matchers.is(org.hamcrest.Matchers.is(Treatments.CONTROL))); - } - @Test public void client_cannot_perform_actions_when_destroyed() throws InterruptedException, URISyntaxException, TimeoutException, IOException { String test = "split"; @@ -1370,4 +1390,294 @@ public void blockUntilReadyException() throws TimeoutException, InterruptedExcep client.blockUntilReady(); } + + @Test + public void null_key_results_in_control_getTreatments() { + 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); + Map splits = new HashMap<>(); + splits.put(test, parsedSplit); + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.fetchMany(Collections.singletonList(test))).thenReturn(splits); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + + + assertEquals(Treatments.CONTROL, client.getTreatments(null, Collections.singletonList("test1")).get("test1")); + + verifyZeroInteractions(splitCacheConsumer); + } + + @Test + public void null_splits_results_in_empty_getTreatments() { + 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); + Map splits = new HashMap<>(); + splits.put(test, parsedSplit); + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.fetchMany(Collections.singletonList(test))).thenReturn(splits); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + + assertEquals(0, client.getTreatments("key", null).size()); + + verifyZeroInteractions(splitCacheConsumer); + } + + @Test + public void exceptions_result_in_control_getTreatments() { + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.fetchMany(anyList())).thenThrow(RuntimeException.class); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + Map result = client.getTreatments("adil@relateiq.com", Arrays.asList("test1", "test2")); + assertEquals(2, result.values().size()); + assertEquals(Treatments.CONTROL, result.get("test1")); + assertEquals(Treatments.CONTROL, result.get("test2")); + + verify(splitCacheConsumer).fetchMany(anyList()); + } + + + + @Test + public void getTreatments_works() { + 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); + Map splits = new HashMap<>(); + splits.put(test, parsedSplit); + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.fetchMany(anyList())).thenReturn(splits); + when(gates.isSDKReady()).thenReturn(true); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + Map result = client.getTreatments("randomKey", Arrays.asList(test, "test2")); + assertEquals("on", result.get(test)); + assertEquals(Treatments.CONTROL, result.get("test2")); + + verify(TELEMETRY_STORAGE, times(1)).recordLatency(Mockito.anyObject(), Mockito.anyLong()); + } + + @Test + public void empty_splits_results_in_null_getTreatments() { + 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); + Map splits = new HashMap<>(); + splits.put(test, parsedSplit); + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.fetchMany(Collections.singletonList(test))).thenReturn(splits); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + + + Map result = client.getTreatments("key", new ArrayList<>()); + assertNotNull(result); + assertTrue(result.isEmpty()); + + verifyZeroInteractions(splitCacheConsumer); + } + + @Test + public void exceptions_result_in_control_treatments() { + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.get(anyString())).thenThrow(RuntimeException.class); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + Map result = client.getTreatments("adil@relateiq.com", Arrays.asList("test1")); + assertEquals(1, result.size()); + assertEquals(Treatments.CONTROL, result.get("test1")); + } + + @Test + public void works_treatments() { + String test = "test1"; + String test2 = "test2"; + + 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); + ParsedSplit parsedSplit2 = ParsedSplit.createParsedSplitForTests(test2, 123, false, Treatments.OFF, conditions, null, 1, 1); + Map parsedSplits = new HashMap<>(); + parsedSplits.put(test, parsedSplit); + parsedSplits.put(test2, parsedSplit2); + + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.fetchMany(anyList())).thenReturn(parsedSplits); + when(gates.isSDKReady()).thenReturn(true); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + + Map result = client.getTreatments("anyKey", Arrays.asList(test, test2)); + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals("on", result.get(test)); + assertEquals("on", result.get(test2)); + + + verify(splitCacheConsumer, times(1)).fetchMany(anyList()); + verify(TELEMETRY_STORAGE, times(1)).recordLatency(Mockito.anyObject(), Mockito.anyLong()); + } + + @Test + public void works_one_control_treatments() { + String test = "test1"; + String test2 = "test2"; + + 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); + Map parsedSplits = new HashMap<>(); + parsedSplits.put(test, parsedSplit); + + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.fetchMany(anyList())).thenReturn(parsedSplits); + when(gates.isSDKReady()).thenReturn(true); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + + Map result = client.getTreatments("anyKey", Arrays.asList(test, test2)); + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals("on", result.get(test)); + assertEquals("control", result.get(test2)); + + + verify(splitCacheConsumer, times(1)).fetchMany(anyList()); + verify(TELEMETRY_STORAGE, times(1)).recordLatency(Mockito.anyObject(), Mockito.anyLong()); + } + + + + @Test + public void treatments_worksAndHasConfig() { + String test = "test1"; + String test2 = "test2"; + + ParsedCondition rollOutToEveryone = ParsedCondition.createParsedConditionForTests(CombiningMatcher.of(new AllKeysMatcher()), Lists.newArrayList(partition("on", 100))); + List conditions = Lists.newArrayList(rollOutToEveryone); + + // Add config for only one treatment + Map configurations = new HashMap<>(); + configurations.put(Treatments.ON, "{\"size\" : 30}"); + configurations.put(Treatments.CONTROL, "{\"size\" : 30}"); + + + ParsedSplit parsedSplit = ParsedSplit.createParsedSplitForTests(test, 123, false, Treatments.OFF, conditions, null, 1, 1, configurations); + Map parsedSplits = new HashMap<>(); + parsedSplits.put(test, parsedSplit); + SDKReadinessGates gates = mock(SDKReadinessGates.class); + SplitCacheConsumer splitCacheConsumer = mock(SplitCacheConsumer.class); + SegmentCacheConsumer segmentCacheConsumer = mock(SegmentCacheConsumer.class); + when(splitCacheConsumer.fetchMany(anyList())).thenReturn(parsedSplits); + + SplitClientImpl client = new SplitClientImpl( + mock(SplitFactory.class), + splitCacheConsumer, + new ImpressionsManager.NoOpImpressionsManager(), + NoopEventsStorageImp.create(), + config, + gates, + new EvaluatorImp(splitCacheConsumer, segmentCacheConsumer), TELEMETRY_STORAGE, TELEMETRY_STORAGE + ); + + Map attributes = new HashMap<>(); + Map result = client.getTreatmentsWithConfig("randomKey", Arrays.asList(test, test2, "", null), attributes); + assertEquals(2, result.size()); + assertEquals(configurations.get("on"), result.get(test).config()); + assertNull(result.get(test2).config()); + assertEquals("control", result.get(test2).treatment()); + + + verify(splitCacheConsumer, times(1)).fetchMany(anyList()); + } + } diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java index 4584cfc55..2ac8cc380 100644 --- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java +++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java @@ -219,7 +219,7 @@ public void testFactoryConsumerDestroy() throws NoSuchFieldException, URISyntaxE splitFactory.destroy(); assertTrue(splitFactory.isDestroyed()); - Mockito.verify(safeUserStorageWrapper, Mockito.times(1)).close(); + Mockito.verify(safeUserStorageWrapper, Mockito.times(1)).disconnect(); } } diff --git a/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java b/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java index ee175c32a..606d88ed2 100644 --- a/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java +++ b/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java @@ -23,6 +23,8 @@ import java.util.AbstractMap; import java.util.HashMap; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; @@ -64,10 +66,10 @@ public void works() throws URISyntaxException { KeyImpression ki3 = keyImpression("test1", "pato", "on", 3L, 2L); KeyImpression ki4 = keyImpression("test2", "pato", "on", 4L, 3L); - treatmentLog.track(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, ki1.changeNumber, null)); - treatmentLog.track(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, ki2.changeNumber, null)); - treatmentLog.track(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, ki3.changeNumber, null)); - treatmentLog.track(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, ki4.changeNumber, null)); + treatmentLog.track(Stream.of(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, ki1.changeNumber, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, ki2.changeNumber, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, ki3.changeNumber, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, ki4.changeNumber, null)).collect(Collectors.toList())); // Do what the scheduler would do. treatmentLog.sendImpressions(); @@ -99,10 +101,10 @@ public void worksButDropsImpressions() throws URISyntaxException { KeyImpression ki3 = keyImpression("test3", "pato", "on", 3L, null); KeyImpression ki4 = keyImpression("test4", "pato", "on", 4L, null); - treatmentLog.track(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, null, null)); - treatmentLog.track(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, null, null)); - treatmentLog.track(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, null, null)); - treatmentLog.track(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, null, null)); + treatmentLog.track(Stream.of(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, null, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, null, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, null, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, null, null)).collect(Collectors.toList())); // Do what the scheduler would do. treatmentLog.sendImpressions(); @@ -135,10 +137,10 @@ public void works4ImpressionsInOneTest() throws URISyntaxException { KeyImpression ki3 = keyImpression("test1", "pato", "on", 3L, 1L); KeyImpression ki4 = keyImpression("test1", "pato", "on", 4L, 1L); - treatmentLog.track(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null)); - treatmentLog.track(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null)); - treatmentLog.track(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null)); - treatmentLog.track(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null)); + treatmentLog.track(Stream.of(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null)).collect(Collectors.toList())); // Do what the scheduler would do. treatmentLog.sendImpressions(); @@ -194,10 +196,10 @@ public void alreadySeenImpressionsAreMarked() throws URISyntaxException { KeyImpression ki3 = keyImpression("test1", "pato", "on", 3L, 1L); KeyImpression ki4 = keyImpression("test1", "pato2", "on", 4L, 1L); - treatmentLog.track(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null)); - treatmentLog.track(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null)); - treatmentLog.track(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null)); - treatmentLog.track(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null)); + treatmentLog.track(Stream.of(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null)).collect(Collectors.toList())); treatmentLog.sendImpressions(); verify(senderMock).postImpressionsBulk(impressionsCaptor.capture()); @@ -211,10 +213,10 @@ public void alreadySeenImpressionsAreMarked() throws URISyntaxException { // Do it again. Now they should all have a `seenAt` value Mockito.reset(senderMock); - treatmentLog.track(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null)); - treatmentLog.track(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null)); - treatmentLog.track(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null)); - treatmentLog.track(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null)); + treatmentLog.track(Stream.of(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null)).collect(Collectors.toList())); treatmentLog.sendImpressions(); verify(senderMock).postImpressionsBulk(impressionsCaptor.capture()); @@ -256,10 +258,10 @@ public void testImpressionsOptimizedMode() throws URISyntaxException { KeyImpression ki3 = keyImpression("test1", "pato", "on", 3L, 1L); KeyImpression ki4 = keyImpression("test1", "pato", "on", 4L, 1L); - treatmentLog.track(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null)); - treatmentLog.track(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null)); - treatmentLog.track(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null)); - treatmentLog.track(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null)); + treatmentLog.track(Stream.of(new Impression(ki1.keyName, null, ki1.feature, ki1.treatment, ki1.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki2.keyName, null, ki2.feature, ki2.treatment, ki2.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki3.keyName, null, ki3.feature, ki3.treatment, ki3.time, null, 1L, null)).collect(Collectors.toList())); + treatmentLog.track(Stream.of(new Impression(ki4.keyName, null, ki4.feature, ki4.treatment, ki4.time, null, 1L, null)).collect(Collectors.toList())); treatmentLog.sendImpressions(); verify(senderMock).postImpressionsBulk(impressionsCaptor.capture()); diff --git a/client/src/test/java/io/split/client/impressions/InMemoryImpressionsStorageTest.java b/client/src/test/java/io/split/client/impressions/InMemoryImpressionsStorageTest.java index 41692cdaf..f86cd3f0b 100644 --- a/client/src/test/java/io/split/client/impressions/InMemoryImpressionsStorageTest.java +++ b/client/src/test/java/io/split/client/impressions/InMemoryImpressionsStorageTest.java @@ -4,6 +4,8 @@ import org.junit.Test; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -16,9 +18,9 @@ public void testBasicUsage() { InMemoryImpressionsStorage storage = new InMemoryImpressionsStorage(10); for (int i = 0; i < 15; i++) { if (i < 10) { - assertThat(storage.put(new KeyImpression()), is(true)); + assertThat(storage.put(Stream.of(new KeyImpression()).collect(Collectors.toList())), is(1L)); } else { - assertThat(storage.put(new KeyImpression()), is(false)); + assertThat(storage.put(Stream.of(new KeyImpression()).collect(Collectors.toList())), is(0L)); } } 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 f0f2bb61d..5e7a998d4 100644 --- a/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java +++ b/client/src/test/java/io/split/engine/evaluator/EvaluatorIntegrationTest.java @@ -17,7 +17,10 @@ import org.junit.Assert; import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -125,6 +128,27 @@ public void evaluateFeatureWhenSplitNotExistsShouldReturnControl() { Assert.assertEquals(Labels.DEFINITION_NOT_FOUND, result.label); } + @Test + public void evaluateMultipleFeatures() { + Evaluator evaluator = buildEvaluatorAndLoadCache(false, 100); + + Map result = evaluator.evaluateFeatures("test_1", null, new ArrayList<>(Arrays.asList("test", "split_3")), null); + Assert.assertNotNull(result); + Assert.assertEquals(2, result.keySet().size()); + Assert.assertEquals(Labels.DEFINITION_NOT_FOUND, result.get("test").label); + Assert.assertEquals(ON_TREATMENT, result.get("split_3").treatment); + Long changeNumberExpected = 223366554L; + Assert.assertEquals(changeNumberExpected, result.get("split_3").changeNumber); + Assert.assertEquals(TEST_LABEL_VALUE_WHITELIST, result.get("split_3").label); + } + + @Test(expected = NullPointerException.class) + public void evaluateFeaturesSplitsNull() { + Evaluator evaluator = buildEvaluatorAndLoadCache(false, 100); + + Map result = evaluator.evaluateFeatures("mauro@test.io", null, null, null); + } + private Evaluator buildEvaluatorAndLoadCache(boolean killed, int trafficAllocation) { SplitCache splitCache = new InMemoryCacheImp(); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); 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 64d1ffd1c..77dd9e653 100644 --- a/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java +++ b/client/src/test/java/io/split/storages/memory/InMemoryCacheTest.java @@ -100,8 +100,9 @@ public void getMany() { names.add("split_name_2"); names.add("split_name_3"); - Collection result = _cache.fetchMany(names); - Assert.assertEquals(2, result.size()); + Map result = _cache.fetchMany(names); + Assert.assertEquals(2, result.keySet().size()); + Assert.assertNotNull(result.get("split_name_2")); } @Test @@ -154,8 +155,8 @@ public void testPutMany() { _cache.putMany(Stream.of(getParsedSplit("split_name_1"),getParsedSplit("split_name_2"),getParsedSplit("split_name_3"),getParsedSplit("split_name_4")).collect(Collectors.toList())); List names = Stream.of("split_name_1","split_name_2","split_name_3","split_name_4").collect(Collectors.toList()); - Collection result = _cache.fetchMany(names); - Assert.assertEquals(4, result.size()); + Map result = _cache.fetchMany(names); + Assert.assertEquals(4, result.keySet().size()); } @Test 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 1e39bedf8..ceb95b7fc 100644 --- a/client/src/test/java/io/split/storages/pluggable/CustomStorageWrapperImp.java +++ b/client/src/test/java/io/split/storages/pluggable/CustomStorageWrapperImp.java @@ -29,7 +29,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; -import java.util.stream.Collectors; public class CustomStorageWrapperImp implements CustomStorageWrapper { @@ -138,7 +137,7 @@ public long decrement(String key, long value) throws Exception { } @Override - public void pushItems(String key, List items) throws Exception { + public long pushItems(String key, List items) throws Exception { String value = getStorage(key); if(value.equals(IMPRESSIONS)){ items.forEach(imp -> imps.add(_json.fromJson(imp, ImpressionConsumer.class))); @@ -146,6 +145,7 @@ public void pushItems(String key, List items) throws Exception { else if(value.equals(EVENTS)) { items.forEach(ev -> events.add(_json.fromJson(ev, EventConsumer.class))); } + return 0; } @Override @@ -189,7 +189,7 @@ public boolean connect() throws Exception { } @Override - public boolean close() throws Exception { + public boolean disconnect() throws Exception { return false; } diff --git a/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomImpressionAdapterProducerTest.java b/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomImpressionAdapterProducerTest.java index db7614930..79127d4c6 100644 --- a/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomImpressionAdapterProducerTest.java +++ b/client/src/test/java/io/split/storages/pluggable/adapters/UserCustomImpressionAdapterProducerTest.java @@ -43,14 +43,16 @@ public void setUp() throws NoSuchFieldException, IllegalAccessException { @Test public void testPut() { KeyImpression keyImpression = new KeyImpression(); - Assert.assertTrue(_impressionAdapterProducer.put(keyImpression)); + Mockito.when(_safeUserStorageWrapper.pushItems(Mockito.anyString(), Mockito.anyObject())).thenReturn(1L); + Assert.assertEquals(1L, _impressionAdapterProducer.put(Stream.of(keyImpression).collect(Collectors.toList()))); Mockito.verify(_safeUserStorageWrapper, Mockito.times(1)).pushItems(Mockito.anyString(), Mockito.anyObject()); } @Test public void testPutMany() { KeyImpression keyImpression = new KeyImpression(); - Assert.assertTrue(_impressionAdapterProducer.put(Stream.of(keyImpression).collect(Collectors.toList()))); + Mockito.when(_safeUserStorageWrapper.pushItems(Mockito.anyString(), Mockito.anyObject())).thenReturn(1L); + Assert.assertEquals(1L, _impressionAdapterProducer.put(Stream.of(keyImpression).collect(Collectors.toList()))); Mockito.verify(_safeUserStorageWrapper, Mockito.times(1)).pushItems(Mockito.anyString(), Mockito.anyObject()); } 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 a2126c053..a8b09fb70 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 @@ -18,6 +18,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -158,27 +159,29 @@ public void testFetchMany(){ List listResultExpected = Stream.of(Json.toJson(split), Json.toJson(split2)).collect(Collectors.toList()); Mockito.when(_safeUserStorageWrapper.getItems(PrefixAdapter.buildFetchManySplits(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())))). thenReturn(listResultExpected); - List splitsResult = (List) _userCustomSplitAdapterConsumer.fetchMany(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())); + Map splitsResult = _userCustomSplitAdapterConsumer.fetchMany(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())); Assert.assertNotNull(splitsResult); - Assert.assertEquals(2, splitsResult.size()); + Assert.assertEquals(2, splitsResult.keySet().size()); } @Test public void testFetchManyWithWrapperFailing(){ Mockito.when(_safeUserStorageWrapper.getItems(PrefixAdapter.buildFetchManySplits(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())))). thenReturn(null); - List splitsResult = (List) _userCustomSplitAdapterConsumer.fetchMany(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())); + Map splitsResult = _userCustomSplitAdapterConsumer.fetchMany(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())); Assert.assertNotNull(splitsResult); - Assert.assertEquals(0, splitsResult.size()); + Assert.assertNull(splitsResult.get(SPLIT_NAME)); + Assert.assertNull(splitsResult.get(SPLIT_NAME+"2")); } @Test public void testFetchManyNotFound(){ Mockito.when(_safeUserStorageWrapper.getItems(PrefixAdapter.buildFetchManySplits(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())))). thenReturn(null); - List splitsResult = (List) _userCustomSplitAdapterConsumer.fetchMany(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())); + Map splitsResult = _userCustomSplitAdapterConsumer.fetchMany(Stream.of(SPLIT_NAME, SPLIT_NAME+"2").collect(Collectors.toList())); Assert.assertNotNull(splitsResult); - Assert.assertEquals(0, splitsResult.size()); + Assert.assertNull(splitsResult.get(SPLIT_NAME)); + Assert.assertNull(splitsResult.get(SPLIT_NAME+"2")); } @Test diff --git a/client/src/test/java/io/split/storages/pluggable/domain/SafeUserStorageWrapperTest.java b/client/src/test/java/io/split/storages/pluggable/domain/SafeUserStorageWrapperTest.java index d1b683dbd..b06539073 100644 --- a/client/src/test/java/io/split/storages/pluggable/domain/SafeUserStorageWrapperTest.java +++ b/client/src/test/java/io/split/storages/pluggable/domain/SafeUserStorageWrapperTest.java @@ -276,16 +276,16 @@ public void testConnectFailed() throws Exception { } @Test - public void testClose() throws Exception { - Mockito.when(_customStorageWrapper.close()).thenReturn(true); - boolean result = _safeUserStorageWrapper.close(); + public void testDisconnect() throws Exception { + Mockito.when(_customStorageWrapper.disconnect()).thenReturn(true); + boolean result = _safeUserStorageWrapper.disconnect(); Assert.assertTrue(result); } @Test - public void testCloseFailed() throws Exception { - Mockito.when(_customStorageWrapper.close()).thenThrow(Exception.class); - boolean result = _safeUserStorageWrapper.close(); + public void testDisconnectFailed() throws Exception { + Mockito.when(_customStorageWrapper.disconnect()).thenThrow(Exception.class); + boolean result = _safeUserStorageWrapper.disconnect(); Assert.assertFalse(result); Mockito.verify(_log, Mockito.times(1)).error(Mockito.anyString()); } diff --git a/pluggable-storage/pom.xml b/pluggable-storage/pom.xml index b8840f28c..336b1b268 100644 --- a/pluggable-storage/pom.xml +++ b/pluggable-storage/pom.xml @@ -6,10 +6,10 @@ java-client-parent io.split.client - 4.4.0-beta + 4.4.0 - 1.0.0-beta + 1.0.0 pluggable-storage jar Package for Pluggable Storage diff --git a/pluggable-storage/src/main/java/pluggable/CustomStorageWrapper.java b/pluggable-storage/src/main/java/pluggable/CustomStorageWrapper.java index 86a743c4b..47e531b39 100644 --- a/pluggable-storage/src/main/java/pluggable/CustomStorageWrapper.java +++ b/pluggable-storage/src/main/java/pluggable/CustomStorageWrapper.java @@ -17,7 +17,7 @@ public interface CustomStorageWrapper { long decrement(String key, long value) throws Exception; // queue operations - void pushItems(String key, List items) throws Exception; + long pushItems(String key, List items) throws Exception; List popItems(String key, long count) throws Exception; long getItemsCount(String key) throws Exception; @@ -27,5 +27,5 @@ public interface CustomStorageWrapper { void removeItems(String key, List items) throws Exception; List getItems(List keys) throws Exception; boolean connect() throws Exception; - boolean close() throws Exception; + boolean disconnect() throws Exception; } diff --git a/pom.xml b/pom.xml index d52d1d3f1..c2a87b644 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.split.client java-client-parent - 4.4.0-beta + 4.4.0 diff --git a/redis-wrapper/pom.xml b/redis-wrapper/pom.xml index 7e0e1b767..d7c104e20 100644 --- a/redis-wrapper/pom.xml +++ b/redis-wrapper/pom.xml @@ -6,21 +6,24 @@ java-client-parent io.split.client - 4.4.0-beta + 4.4.0 redis-wrapper - 1.0.0-beta + 1.0.0 + jar + Package for Redis Wrapper Implementation + Implements Redis Pluggable Storage + 8 8 - 1.0.0-beta io.split.client pluggable-storage - ${pluggable.storage} + 1.0.0 compile @@ -34,6 +37,12 @@ junit test + + com.google.guava + guava + 30.0-jre + compile + diff --git a/redis-wrapper/src/main/java/redis/RedisException.java b/redis-wrapper/src/main/java/redis/RedisException.java new file mode 100644 index 000000000..43518f1e7 --- /dev/null +++ b/redis-wrapper/src/main/java/redis/RedisException.java @@ -0,0 +1,7 @@ +package redis; + +public class RedisException extends Exception{ + public RedisException(String message) { + super(message); + } +} diff --git a/redis-wrapper/src/main/java/redis/RedisImp.java b/redis-wrapper/src/main/java/redis/RedisImp.java index 69ce8a53f..2061d6328 100644 --- a/redis-wrapper/src/main/java/redis/RedisImp.java +++ b/redis-wrapper/src/main/java/redis/RedisImp.java @@ -1,5 +1,6 @@ package redis; +import com.google.common.annotations.VisibleForTesting; import pluggable.CustomStorageWrapper; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; @@ -11,6 +12,9 @@ class RedisImp implements CustomStorageWrapper { private static final String TELEMETRY_INIT = "SPLITIO.telemetry.init" ; + private static final String EVENTS_KEY = "SPLITIO.events" ; + private static final String IMPRESSIONS_KEY = "SPLITIO.impressions" ; + private static final long IMPRESSIONS_OR_EVENTS_DEFAULT_TTL = 3600000L; private final JedisPool jedisPool; private final String prefix; @@ -25,7 +29,7 @@ public String get(String key) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { return jedis.get(buildKeyWithPrefix(key)); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -39,7 +43,7 @@ public List getMany(List keys) throws Exception { return jedis.mget(keys.toArray(new String[keys.size()])); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -53,7 +57,7 @@ public void set(String key, String item) throws Exception { } jedis.set(buildKeyWithPrefix(key), item); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -67,7 +71,7 @@ public void delete(List keys) throws Exception { jedis.del(keys.toArray(new String[keys.size()])); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -76,7 +80,7 @@ public String getAndSet(String key, String item) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { return jedis.getSet(buildKeyWithPrefix(key), item); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -85,7 +89,7 @@ public Set getKeysByPrefix(String prefix) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { return jedis.keys(buildKeyWithPrefix(prefix)); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -94,7 +98,7 @@ public long increment(String key, long value) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { return jedis.incrBy(buildKeyWithPrefix(key), value); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -103,25 +107,35 @@ public long decrement(String key, long value) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { return jedis.decrBy(buildKeyWithPrefix(key), value); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @Override - public void pushItems(String key, List items) throws Exception { + public long pushItems(String key, List items) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { - jedis.rpush(buildKeyWithPrefix(key), items.toArray(new String[items.size()])); + long addedItems = jedis.rpush(buildKeyWithPrefix(key), items.toArray(new String[items.size()])); + if(EVENTS_KEY.equals(key) || IMPRESSIONS_KEY.equals(key)) { + if(addedItems == items.size()) { + jedis.pexpire(key, IMPRESSIONS_OR_EVENTS_DEFAULT_TTL); + } + } + return addedItems; } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @Override public List popItems(String key, long count) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { - return jedis.rpop(buildKeyWithPrefix(key), (int)count); + String keyWithPrefix = buildKeyWithPrefix(key); + List items = jedis.lrange(keyWithPrefix, 0, count-1); + int fetchedCount = items.size(); + jedis.ltrim(keyWithPrefix, fetchedCount, -1); + return items; } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -131,7 +145,7 @@ public long getItemsCount(String key) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { return jedis.scard(buildKeyWithPrefix(key)); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -140,7 +154,7 @@ public boolean itemContains(String key, String item) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { return jedis.sismember(buildKeyWithPrefix(key), item); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -149,7 +163,7 @@ public void addItems(String key, List items) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { jedis.sadd(buildKeyWithPrefix(key), items.toArray(new String[items.size()])); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -158,7 +172,7 @@ public void removeItems(String key, List items) throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { jedis.srem(buildKeyWithPrefix(key), items.toArray(new String[items.size()])); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -172,7 +186,7 @@ public List getItems(List keys) throws Exception { return jedis.mget(keys.toArray(new String[keys.size()])); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @@ -181,22 +195,23 @@ public boolean connect() throws Exception { try (Jedis jedis = this.jedisPool.getResource()) { return "PONG".equalsIgnoreCase(jedis.ping()); } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } @Override - public boolean close() throws Exception { + public boolean disconnect() throws Exception { try { jedisPool.close(); return true; } catch (Exception ex) { - throw new Exception(ex); + throw new RedisException(ex.getMessage()); } } - private String buildKeyWithPrefix(String key) { + @VisibleForTesting + String buildKeyWithPrefix(String key) { if (!key.startsWith(this.prefix)) { key = String.format("%s.%s", prefix, key); } diff --git a/redis-wrapper/src/test/java/redis/RedisImpTest.java b/redis-wrapper/src/test/java/redis/RedisImpTest.java index 06f34658b..cc484abf0 100644 --- a/redis-wrapper/src/test/java/redis/RedisImpTest.java +++ b/redis-wrapper/src/test/java/redis/RedisImpTest.java @@ -1,14 +1,17 @@ package redis; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; import pluggable.CustomStorageWrapper; import redis.clients.jedis.JedisPool; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -60,5 +63,179 @@ public void testSetAndGetMany() throws Exception { storageWrapper.delete(new ArrayList<>(map.keySet())); } + + @Test + public void testGetSet() throws Exception { + String key = "test-getSet"; + Map map = new HashMap<>(); + map.put(key, "5"); + + CustomStorageWrapper storageWrapper = new RedisImp(new JedisPool(), "test-prefix:."); + storageWrapper.set(key, "5"); + String result = storageWrapper.getAndSet(key, "7"); + Assert.assertEquals("5", result); + Assert.assertEquals("7", storageWrapper.get(key)); + + storageWrapper.delete(new ArrayList<>(map.keySet())); + } + + @Test + public void testGetKeysByPrefix() throws Exception { + Map map = new HashMap<>(); + map.put("item-1", "1"); + map.put("item-2", "2"); + map.put("item-3", "3"); + map.put("i-4", "4"); + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + try { + for (Map.Entry entry : map.entrySet()) { + storageWrapper.set(entry.getKey(), entry.getValue()); + } + + Set result = storageWrapper.getKeysByPrefix("item*"); + + Assert.assertEquals(3, result.size()); + Assert.assertTrue(result.contains(storageWrapper.buildKeyWithPrefix("item-1"))); + Assert.assertTrue(result.contains(storageWrapper.buildKeyWithPrefix("item-2"))); + Assert.assertTrue(result.contains(storageWrapper.buildKeyWithPrefix("item-3"))); + } + finally { + storageWrapper.delete(new ArrayList<>(map.keySet())); + } + } + + @Test + public void testIncrementAndDecrement() throws Exception { + Map map = new HashMap<>(); + map.put("item-1", "2"); + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + try { + for (Map.Entry entry : map.entrySet()) { + storageWrapper.set(entry.getKey(), entry.getValue()); + } + + long result = storageWrapper.increment("item-1", 2L); + Assert.assertEquals(4L, result); + + result = storageWrapper.decrement("item-1", 3L); + Assert.assertEquals(1L, result); + } + finally { + storageWrapper.delete(new ArrayList<>(map.keySet())); + } + } + + @Test + public void testPushAndPopItems() throws Exception { + Map map = new HashMap<>(); + map.put("item-1", "1"); + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + try { + long push = storageWrapper.pushItems("item-1", Arrays.asList("1", "2", "3", "4")); + Assert.assertEquals(4L, push); + + List result = storageWrapper.popItems("item-1", 3); + Assert.assertEquals(3L, result.size()); + + push = storageWrapper.pushItems("item-1", Arrays.asList("5")); + Assert.assertEquals(2L, push); + + } + finally { + storageWrapper.delete(new ArrayList<>(map.keySet())); + } + } + + @Test + public void testGetItemsCount() throws Exception { + Map map = new HashMap<>(); + map.put("item-1", "1"); + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + try { + storageWrapper.addItems("item-1", Arrays.asList("1", "2", "3", "4")); + long result = storageWrapper.getItemsCount("item-1"); + + Assert.assertEquals(4L, result); + } + finally { + storageWrapper.delete(new ArrayList<>(map.keySet())); + } + } + + @Test + public void testItemContains() throws Exception { + Map map = new HashMap<>(); + map.put("item-1", "1"); + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + try { + storageWrapper.addItems("item-1", Arrays.asList("1", "2", "3", "4")); + boolean result = storageWrapper.itemContains("item-1", "2"); + + Assert.assertTrue(result); + } + finally { + storageWrapper.delete(new ArrayList<>(map.keySet())); + } + } + + @Test + public void testRemoveItems() throws Exception { + Map map = new HashMap<>(); + map.put("item-1", "1"); + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + try { + storageWrapper.addItems("item-1", Arrays.asList("1", "2", "3", "4")); + boolean result = storageWrapper.itemContains("item-1", "2"); + Assert.assertTrue(result); + + storageWrapper.removeItems("item-1", Arrays.asList("2", "4")); + result = storageWrapper.itemContains("item-1", "2"); + Assert.assertFalse(result); + result = storageWrapper.itemContains("item-1", "4"); + Assert.assertFalse(result); + } + finally { + storageWrapper.delete(new ArrayList<>(map.keySet())); + } + } + + @Test + public void testGetItems() throws Exception { + Map map = new HashMap<>(); + map.put("item-1", "1"); + map.put("item-2", "2"); + map.put("item-3", "3"); + map.put("i-4", "4"); + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + try { + for (Map.Entry entry : map.entrySet()) { + storageWrapper.set(entry.getKey(), entry.getValue()); + } + + Set result = storageWrapper.getKeysByPrefix("item*"); + + Assert.assertEquals(3, result.size()); + List items = storageWrapper.getItems(new ArrayList<>(result)); + Assert.assertEquals(3, items.size()); + Assert.assertTrue(items.containsAll(Arrays.asList("1", "2", "3"))); + } + finally { + storageWrapper.delete(new ArrayList<>(map.keySet())); + } + } + + @Test + public void testConnect() throws Exception { + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + Assert.assertTrue(storageWrapper.connect()); + } + + + + @Test + public void testDisconnect() throws Exception { + RedisImp storageWrapper = new RedisImp(new JedisPool(), "test-prefix"); + Assert.assertTrue(storageWrapper.disconnect()); + } } diff --git a/testing/pom.xml b/testing/pom.xml index 27bcfd751..2cfc78bed 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -6,7 +6,7 @@ io.split.client java-client-parent - 4.4.0-beta + 4.4.0 java-client-testing diff --git a/testing/src/main/java/io/split/client/testing/SplitClientForTest.java b/testing/src/main/java/io/split/client/testing/SplitClientForTest.java index 42cf2c92b..3bc95718a 100644 --- a/testing/src/main/java/io/split/client/testing/SplitClientForTest.java +++ b/testing/src/main/java/io/split/client/testing/SplitClientForTest.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.List; import java.util.concurrent.TimeoutException; public class SplitClientForTest implements SplitClient { @@ -71,6 +72,66 @@ public SplitResult getTreatmentWithConfig(Key key, String split, Map getTreatments(String key, List splits) { + Map treatments = new HashMap<>(); + for (String split : splits) { + treatments.put(split, _tests.containsKey(split) ? _tests.get(split) : Treatments.CONTROL); + } + return treatments; + } + + @Override + public Map getTreatments(String key, List splits, Map attributes){ + Map treatments = new HashMap<>(); + for (String split : splits) { + treatments.put(split, _tests.containsKey(split) ? _tests.get(split) : Treatments.CONTROL); + } + return treatments; + } + + @Override + public Map getTreatments(Key key, List splits, Map attributes) { + Map treatments = new HashMap<>(); + for (String split : splits) { + treatments.put(split, _tests.containsKey(split) ? _tests.get(split) : Treatments.CONTROL); + } + return treatments; + } + + @Override + public Map getTreatmentsWithConfig(String key, List splits) { + Map treatments = new HashMap<>(); + for (String split : splits) { + treatments.put(split, new SplitResult(_tests.containsKey(split) + ? _tests.get(split) + : Treatments.CONTROL, null)); + } + return treatments; + } + + @Override + public Map getTreatmentsWithConfig(String key, List splits, Map attributes) { + Map treatments = new HashMap<>(); + for (String split : splits) { + treatments.put(split, new SplitResult(_tests.containsKey(split) + ? _tests.get(split) + : Treatments.CONTROL, null)); + } + return treatments; + } + + @Override + public Map getTreatmentsWithConfig(Key key, List splits, Map attributes) { + Map treatments = new HashMap<>(); + for (String split : splits) { + treatments.put(split, new SplitResult(_tests.containsKey(split) + ? _tests.get(split) + : Treatments.CONTROL, null)); + } + return treatments; + } + @Override public void destroy() {