diff --git a/client/src/main/java/io/split/client/SplitClientImpl.java b/client/src/main/java/io/split/client/SplitClientImpl.java index 140e581d1..4b61c7d6c 100644 --- a/client/src/main/java/io/split/client/SplitClientImpl.java +++ b/client/src/main/java/io/split/client/SplitClientImpl.java @@ -192,7 +192,7 @@ private boolean track(Event event) { event.trafficTypeName = event.trafficTypeName.toLowerCase(); } - if (!_splitFetcher.fetchKnownTrafficTypes().contains(event.trafficTypeName)) { + if (!_splitFetcher.trafficTypeExists(event.trafficTypeName)) { _log.warn("track: Traffic Type " + event.trafficTypeName + " does not have any corresponding Splits in this environment, " + "make sure you’re tracking your events to a valid traffic type defined in the Split console."); } diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index 7a3bfb43f..dafda7bf6 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -11,6 +11,8 @@ import io.split.client.metrics.CachedMetrics; import io.split.client.metrics.FireAndForgetMetrics; import io.split.client.metrics.HttpMetrics; +import io.split.engine.cache.InMemoryCacheImp; +import io.split.engine.cache.SplitCache; import io.split.engine.evaluator.Evaluator; import io.split.engine.evaluator.EvaluatorImp; import io.split.engine.SDKReadinessGates; @@ -200,7 +202,8 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn // Feature Changes SplitChangeFetcher splitChangeFetcher = HttpSplitChangeFetcher.create(httpclient, rootTarget, uncachedFireAndForget); - final RefreshableSplitFetcherProvider splitFetcherProvider = new RefreshableSplitFetcherProvider(splitChangeFetcher, splitParser, findPollingPeriod(RANDOM, config.featuresRefreshRate()), gates); + final SplitCache cache = new InMemoryCacheImp(); + final RefreshableSplitFetcherProvider splitFetcherProvider = new RefreshableSplitFetcherProvider(splitChangeFetcher, splitParser, findPollingPeriod(RANDOM, config.featuresRefreshRate()), gates, cache); List impressionListeners = new ArrayList<>(); diff --git a/client/src/main/java/io/split/engine/cache/InMemoryCacheImp.java b/client/src/main/java/io/split/engine/cache/InMemoryCacheImp.java new file mode 100644 index 000000000..af4e5e4d1 --- /dev/null +++ b/client/src/main/java/io/split/engine/cache/InMemoryCacheImp.java @@ -0,0 +1,108 @@ +package io.split.engine.cache; + +import com.google.common.collect.ConcurrentHashMultiset; +import com.google.common.collect.Maps; +import com.google.common.collect.Multiset; +import com.google.common.collect.Sets; +import io.split.engine.experiments.ParsedSplit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; + +public class InMemoryCacheImp implements SplitCache { + + private static final Logger _log = LoggerFactory.getLogger(InMemoryCacheImp.class); + + private final ConcurrentMap _concurrentMap; + private final Multiset _concurrentTrafficTypeNameSet; + + private AtomicLong _changeNumber; + + public InMemoryCacheImp() { + this(-1); + } + + public InMemoryCacheImp(long startingChangeNumber) { + _concurrentMap = Maps.newConcurrentMap(); + _changeNumber = new AtomicLong(startingChangeNumber); + _concurrentTrafficTypeNameSet = ConcurrentHashMultiset.create(); + } + + @Override + public void put(ParsedSplit split) { + _concurrentMap.put(split.feature(), split); + + if (split.trafficTypeName() != null) { + _concurrentTrafficTypeNameSet.add(split.trafficTypeName()); + } + } + + @Override + public boolean remove(String name) { + ParsedSplit removed = _concurrentMap.remove(name); + + if (removed != null && removed.trafficTypeName() != null) { + _concurrentTrafficTypeNameSet.remove(removed.trafficTypeName()); + } + + return removed != null; + } + + @Override + public ParsedSplit get(String name) { + return _concurrentMap.get(name); + } + + @Override + public Collection getAll() { + return _concurrentMap.values(); + } + + @Override + public Collection getMany(List names) { + List splits = new ArrayList<>(); + + for (String name : names) { + ParsedSplit split = _concurrentMap.get(name); + + if (split != null) { + splits.add(split); + } + } + + return splits; + } + + @Override + public long getChangeNumber() { + return _changeNumber.get(); + } + + @Override + public void setChangeNumber(long changeNumber) { + if (changeNumber < _changeNumber.get()) { + _log.error("ChangeNumber for splits cache is less than previous"); + } + + _changeNumber.set(changeNumber); + } + + @Override + public boolean trafficTypeExists(String trafficTypeName) { + // If the multiset has [{"user",2}.{"account",0}], elementSet only returns + // ["user"] (it ignores "account") + return Sets.newHashSet(_concurrentTrafficTypeNameSet.elementSet()).contains(trafficTypeName); + } + + @Override + public void clear() { + _concurrentMap.clear(); + _concurrentTrafficTypeNameSet.clear(); + } +} diff --git a/client/src/main/java/io/split/engine/cache/SplitCache.java b/client/src/main/java/io/split/engine/cache/SplitCache.java new file mode 100644 index 000000000..3ab9916bd --- /dev/null +++ b/client/src/main/java/io/split/engine/cache/SplitCache.java @@ -0,0 +1,19 @@ +package io.split.engine.cache; + +import io.split.engine.experiments.ParsedSplit; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public interface SplitCache { + void put(ParsedSplit split); + boolean remove(String name); + ParsedSplit get(String name); + Collection getAll(); + Collection getMany(List names); + long getChangeNumber(); + void setChangeNumber(long changeNumber); + boolean trafficTypeExists(String trafficTypeName); + void clear(); +} diff --git a/client/src/main/java/io/split/engine/experiments/RefreshableSplitFetcher.java b/client/src/main/java/io/split/engine/experiments/RefreshableSplitFetcher.java index 4ef49750f..982c7184a 100644 --- a/client/src/main/java/io/split/engine/experiments/RefreshableSplitFetcher.java +++ b/client/src/main/java/io/split/engine/experiments/RefreshableSplitFetcher.java @@ -1,26 +1,16 @@ package io.split.engine.experiments; -import com.google.common.collect.ConcurrentHashMultiset; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Multiset; -import com.google.common.collect.Multisets; -import com.google.common.collect.Sets; -import io.split.client.dtos.Condition; -import io.split.client.dtos.Matcher; -import io.split.client.dtos.MatcherType; import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; import io.split.client.dtos.Status; import io.split.engine.SDKReadinessGates; +import io.split.engine.cache.SplitCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicLong; import static com.google.common.base.Preconditions.checkNotNull; @@ -35,9 +25,9 @@ public class RefreshableSplitFetcher implements SplitFetcher, Runnable { private final SplitParser _parser; private final SplitChangeFetcher _splitChangeFetcher; - private final AtomicLong _changeNumber; - - private Map _concurrentMap = Maps.newConcurrentMap(); + private final SplitCache _splitCache; + private final SDKReadinessGates _gates; + private final Object _lock = new Object(); /** * Contains all the traffic types that are currently being used by the splits and also the count @@ -48,36 +38,12 @@ public class RefreshableSplitFetcher implements SplitFetcher, Runnable { * The count is used to maintain how many splits are using a traffic type, so when * an ARCHIVED split is received, we know if we need to remove a traffic type from the multiset. */ - Multiset _concurrentTrafficTypeNameSet = ConcurrentHashMultiset.create(); - private final SDKReadinessGates _gates; - - private final Object _lock = new Object(); - - public RefreshableSplitFetcher(SplitChangeFetcher splitChangeFetcher, SplitParser parser, SDKReadinessGates gates) { - this(splitChangeFetcher, parser, gates, -1); - } - - /** - * This constructor is package private because it is meant primarily for unit tests - * where we want to set the starting change number. All regular clients should use - * the public constructor. - * - * @param splitChangeFetcher MUST NOT be null - * @param parser MUST NOT be null - * @param startingChangeNumber - */ - /*package private*/ RefreshableSplitFetcher(SplitChangeFetcher splitChangeFetcher, - SplitParser parser, - SDKReadinessGates gates, - long startingChangeNumber) { - _splitChangeFetcher = splitChangeFetcher; - _parser = parser; - _gates = gates; - _changeNumber = new AtomicLong(startingChangeNumber); - - checkNotNull(_parser); - checkNotNull(_splitChangeFetcher); + public RefreshableSplitFetcher(SplitChangeFetcher splitChangeFetcher, SplitParser parser, SDKReadinessGates gates, SplitCache splitCache) { + _splitChangeFetcher = checkNotNull(splitChangeFetcher); + _parser = checkNotNull(parser); + _gates = checkNotNull(gates); + _splitCache = checkNotNull(splitCache); } @Override @@ -85,9 +51,9 @@ public void forceRefresh() { _log.debug("Force Refresh splits starting ..."); try { while (true) { - long start = _changeNumber.get(); + long start = _splitCache.getChangeNumber(); runWithoutExceptionHandling(); - long end = _changeNumber.get(); + long end = _splitCache.getChangeNumber(); if (start >= end) { break; @@ -103,13 +69,13 @@ public void forceRefresh() { @Override public long changeNumber() { - return _changeNumber.get(); + return _splitCache.getChangeNumber(); } @Override public void killSplit(String splitName, String defaultTreatment, long changeNumber) { synchronized (_lock) { - ParsedSplit parsedSplit = _concurrentMap.get(splitName); + ParsedSplit parsedSplit = _splitCache.get(splitName); ParsedSplit updatedSplit = new ParsedSplit(parsedSplit.feature(), parsedSplit.seed(), @@ -123,40 +89,36 @@ public void killSplit(String splitName, String defaultTreatment, long changeNumb parsedSplit.algo(), parsedSplit.configurations()); - _concurrentMap.put(splitName, updatedSplit); + _splitCache.put(updatedSplit); } } @Override public ParsedSplit fetch(String test) { - return _concurrentMap.get(test); + return _splitCache.get(test); } public List fetchAll() { - return Lists.newArrayList(_concurrentMap.values()); + return Lists.newArrayList(_splitCache.getAll()); } @Override - public Set fetchKnownTrafficTypes() { - // We return the "keys" of the multiset that have a count greater than 0 - // If the multiset has [{"user",2}.{"account",0}], elementSet only returns - // ["user"] (it ignores "account") - return Sets.newHashSet(_concurrentTrafficTypeNameSet.elementSet()); + public boolean trafficTypeExists(String trafficTypeName) { + return _splitCache.trafficTypeExists(trafficTypeName); } public Collection fetch() { - return _concurrentMap.values(); + return _splitCache.getAll(); } public void clear() { - _concurrentMap.clear(); - _concurrentTrafficTypeNameSet.clear(); + _splitCache.clear(); } @Override public void run() { _log.debug("Fetch splits starting ..."); - long start = _changeNumber.get(); + long start = _splitCache.getChangeNumber(); try { runWithoutExceptionHandling(); _gates.splitsAreReady(); @@ -170,47 +132,42 @@ public void run() { } } finally { if (_log.isDebugEnabled()) { - _log.debug("split fetch before: " + start + ", after: " + _changeNumber.get()); + _log.debug("split fetch before: " + start + ", after: " + _splitCache.getChangeNumber()); } } } - public void runWithoutExceptionHandling() throws InterruptedException { - SplitChange change = _splitChangeFetcher.fetch(_changeNumber.get()); + private void runWithoutExceptionHandling() throws InterruptedException { + SplitChange change = _splitChangeFetcher.fetch(_splitCache.getChangeNumber()); if (change == null) { throw new IllegalStateException("SplitChange was null"); } - if (change.till == _changeNumber.get()) { + if (change.till == _splitCache.getChangeNumber()) { // no change. return; } - if (change.since != _changeNumber.get() || change.till < _changeNumber.get()) { + if (change.since != _splitCache.getChangeNumber() || change.till < _splitCache.getChangeNumber()) { // some other thread may have updated the shared state. exit return; } if (change.splits.isEmpty()) { // there are no changes. weird! - _changeNumber.set(change.till); + _splitCache.setChangeNumber(change.till); return; } synchronized (_lock) { // check state one more time. - if (change.since != _changeNumber.get() - || change.till < _changeNumber.get()) { + if (change.since != _splitCache.getChangeNumber() + || change.till < _splitCache.getChangeNumber()) { // some other thread may have updated the shared state. exit return; } - Set toRemove = Sets.newHashSet(); - Map toAdd = Maps.newHashMap(); - List trafficTypeNamesToRemove = Lists.newArrayList(); - List trafficTypeNamesToAdd = Lists.newArrayList(); - for (Split split : change.splits) { if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); @@ -218,59 +175,36 @@ public void runWithoutExceptionHandling() throws InterruptedException { if (split.status != Status.ACTIVE) { // archive. - toRemove.add(split.name); - if (split.trafficTypeName != null) { - trafficTypeNamesToRemove.add(split.trafficTypeName); - } + _splitCache.remove(split.name); continue; } ParsedSplit parsedSplit = _parser.parse(split); if (parsedSplit == null) { _log.info("We could not parse the experiment definition for: " + split.name + " so we are removing it completely to be careful"); - toRemove.add(split.name); - if (split.trafficTypeName != null) { - trafficTypeNamesToRemove.add(split.trafficTypeName); - } + + _splitCache.remove(split.name); + _log.debug("Deleted feature: " + split.name); + continue; } - toAdd.put(split.name, parsedSplit); - // If the split already exists, this is either an update, or the split has been // deleted and recreated (possibly with a different traffic type). // If it's an update, the traffic type should NOT be increased. // If it's deleted & recreated, the old one should be decreased and the new one increased. // To handle both cases, we simply delete the old one if the split is present. // The new one is always increased. - ParsedSplit current = _concurrentMap.get(split.name); - if (current != null && current.trafficTypeName() != null) { - trafficTypeNamesToRemove.add(current.trafficTypeName()); - } - - if (split.trafficTypeName != null) { - trafficTypeNamesToAdd.add(split.trafficTypeName); + ParsedSplit current = _splitCache.get(split.name); + if (current != null) { + _splitCache.remove(split.name); } - } - - _concurrentMap.putAll(toAdd); - _concurrentTrafficTypeNameSet.addAll(trafficTypeNamesToAdd); - //removeAll does not work here, since it wont remove all the occurrences, just one - Multisets.removeOccurrences(_concurrentTrafficTypeNameSet, trafficTypeNamesToRemove); - - for (String remove : toRemove) { - _concurrentMap.remove(remove); - } - - if (!toAdd.isEmpty()) { - _log.debug("Updated features: " + toAdd.keySet()); - } - if (!toRemove.isEmpty()) { - _log.debug("Deleted features: " + toRemove); + _splitCache.put(parsedSplit); + _log.debug("Updated feature: " + parsedSplit.feature()); } - _changeNumber.set(change.till); + _splitCache.setChangeNumber(change.till); } } } diff --git a/client/src/main/java/io/split/engine/experiments/RefreshableSplitFetcherProvider.java b/client/src/main/java/io/split/engine/experiments/RefreshableSplitFetcherProvider.java index 04760b941..5113a9a85 100644 --- a/client/src/main/java/io/split/engine/experiments/RefreshableSplitFetcherProvider.java +++ b/client/src/main/java/io/split/engine/experiments/RefreshableSplitFetcherProvider.java @@ -2,6 +2,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.split.engine.SDKReadinessGates; +import io.split.engine.cache.SplitCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,30 +28,26 @@ public class RefreshableSplitFetcherProvider implements Closeable { private static final Logger _log = LoggerFactory.getLogger(RefreshableSplitFetcherProvider.class); + private final AtomicReference _splitFetcher = new AtomicReference(); + private final AtomicReference _executorService = new AtomicReference<>(); private final SplitParser _splitParser; private final SplitChangeFetcher _splitChangeFetcher; private final AtomicLong _refreshEveryNSeconds; - private final AtomicReference _splitFetcher = new AtomicReference(); private final SDKReadinessGates _gates; - private final AtomicReference _executorService = new AtomicReference<>(); private final ScheduledExecutorService _scheduledExecutorService; private final Object _lock = new Object(); private final AtomicBoolean _running; + private final SplitCache _splitCache; private ScheduledFuture _scheduledFuture; - public RefreshableSplitFetcherProvider(SplitChangeFetcher splitChangeFetcher, SplitParser splitParser, long refreshEveryNSeconds, SDKReadinessGates sdkBuildBlocker) { - _splitChangeFetcher = splitChangeFetcher; - checkNotNull(_splitChangeFetcher); - - _splitParser = splitParser; - checkNotNull(_splitParser); - + public RefreshableSplitFetcherProvider(SplitChangeFetcher splitChangeFetcher, SplitParser splitParser, long refreshEveryNSeconds, SDKReadinessGates sdkBuildBlocker, SplitCache splitCache) { + _splitChangeFetcher = checkNotNull(splitChangeFetcher); + _splitParser = checkNotNull(splitParser); checkArgument(refreshEveryNSeconds >= 0L); _refreshEveryNSeconds = new AtomicLong(refreshEveryNSeconds); - - _gates = sdkBuildBlocker; - checkNotNull(_gates); + _gates = checkNotNull(sdkBuildBlocker); + _splitCache = checkNotNull(splitCache); ThreadFactory threadFactory = new ThreadFactoryBuilder() .setDaemon(true) @@ -75,7 +72,7 @@ public RefreshableSplitFetcher getFetcher() { return _splitFetcher.get(); } - RefreshableSplitFetcher splitFetcher = new RefreshableSplitFetcher(_splitChangeFetcher, _splitParser, _gates); + RefreshableSplitFetcher splitFetcher = new RefreshableSplitFetcher(_splitChangeFetcher, _splitParser, _gates, _splitCache); _splitFetcher.set(splitFetcher); return splitFetcher; @@ -130,5 +127,4 @@ public void close() { Thread.currentThread().interrupt(); } } - } diff --git a/client/src/main/java/io/split/engine/experiments/SplitFetcher.java b/client/src/main/java/io/split/engine/experiments/SplitFetcher.java index 585a53846..203fbb588 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitFetcher.java +++ b/client/src/main/java/io/split/engine/experiments/SplitFetcher.java @@ -1,7 +1,6 @@ package io.split.engine.experiments; import java.util.List; -import java.util.Set; /** * Created by adilaijaz on 5/8/15. @@ -11,15 +10,7 @@ public interface SplitFetcher { List fetchAll(); - /** - * Fetches all the traffic types that are being used by the splits that are currently stored. - * - * For example, if the fetcher currently contains three splits, one of traffic type "account" - * and two of traffic type "user", this method will return ["account", "user"] - * - * @return a set of all the traffic types used by the parsed splits - */ - Set fetchKnownTrafficTypes(); + boolean trafficTypeExists(String trafficTypeName); /** * Forces a sync of splits, outside of any scheduled diff --git a/client/src/test/java/io/split/engine/cache/InMemoryCacheTest.java b/client/src/test/java/io/split/engine/cache/InMemoryCacheTest.java new file mode 100644 index 000000000..58db3905a --- /dev/null +++ b/client/src/test/java/io/split/engine/cache/InMemoryCacheTest.java @@ -0,0 +1,98 @@ +package io.split.engine.cache; + +import io.split.engine.experiments.ParsedSplit; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.*; + +public class InMemoryCacheTest { + private SplitCache _cache; + + @Before + public void before() { + _cache = new InMemoryCacheImp(); + } + + @Test + public void putAndGetSplit() { + ParsedSplit split = getParsedSplit("split_name"); + _cache.put(split); + + ParsedSplit result = _cache.get("split_name"); + Assert.assertNotNull(result); + Assert.assertEquals(split.changeNumber(), result.changeNumber()); + Assert.assertEquals(split.trafficTypeName(), result.trafficTypeName()); + Assert.assertEquals(split.defaultTreatment(), result.defaultTreatment()); + } + + @Test + public void putDuplicateSplit() { + ParsedSplit split = getParsedSplit("split_name"); + ParsedSplit split2 = getParsedSplit("split_name"); + _cache.put(split); + _cache.put(split2); + + int result = _cache.getAll().size(); + + Assert.assertEquals(1, result); + } + + @Test + public void getInExistentSplit() { + ParsedSplit split = getParsedSplit("split_name"); + _cache.put(split); + + ParsedSplit result = _cache.get("split_name_2"); + Assert.assertNull(result); + } + + @Test + public void removeSplit() { + ParsedSplit split = getParsedSplit("split_name"); + ParsedSplit split2 = getParsedSplit("split_name_2"); + _cache.put(split); + _cache.put(split2); + + int result = _cache.getAll().size(); + Assert.assertEquals(2, result); + + _cache.remove("split_name"); + result = _cache.getAll().size(); + Assert.assertEquals(1, result); + + Assert.assertNull(_cache.get("split_name")); + } + + @Test + public void setAndGetChangeNumber() { + _cache.setChangeNumber(223); + + long changeNumber = _cache.getChangeNumber(); + Assert.assertEquals(223, changeNumber); + + _cache.setChangeNumber(539); + changeNumber = _cache.getChangeNumber(); + Assert.assertEquals(539, changeNumber); + } + + @Test + public void getMany() { + _cache.put(getParsedSplit("split_name_1")); + _cache.put(getParsedSplit("split_name_2")); + _cache.put(getParsedSplit("split_name_3")); + _cache.put(getParsedSplit("split_name_4")); + + List names = new ArrayList<>(); + names.add("split_name_2"); + names.add("split_name_3"); + + Collection result = _cache.getMany(names); + Assert.assertEquals(2, result.size()); + } + + private ParsedSplit getParsedSplit(String splitName) { + return ParsedSplit.createParsedSplitForTests(splitName, 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2); + } +} diff --git a/client/src/test/java/io/split/engine/experiments/RefreshableSplitFetcherTest.java b/client/src/test/java/io/split/engine/experiments/RefreshableSplitFetcherTest.java index dd00b0744..67e07b45b 100644 --- a/client/src/test/java/io/split/engine/experiments/RefreshableSplitFetcherTest.java +++ b/client/src/test/java/io/split/engine/experiments/RefreshableSplitFetcherTest.java @@ -1,7 +1,6 @@ package io.split.engine.experiments; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import io.split.client.dtos.Condition; import io.split.client.dtos.Matcher; import io.split.client.dtos.MatcherGroup; @@ -10,6 +9,8 @@ import io.split.client.dtos.Status; import io.split.engine.ConditionsTestUtil; import io.split.engine.SDKReadinessGates; +import io.split.engine.cache.InMemoryCacheImp; +import io.split.engine.cache.SplitCache; import io.split.engine.matchers.AllKeysMatcher; import io.split.engine.matchers.CombiningMatcher; import io.split.engine.segments.NoChangeSegmentChangeFetcher; @@ -19,16 +20,13 @@ import io.split.engine.segments.StaticSegment; import io.split.engine.segments.StaticSegmentFetcher; import io.split.grammar.Treatments; -import org.hamcrest.Matchers; -import org.junit.Assert; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Set; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -38,7 +36,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -63,8 +61,9 @@ private void works(long startingChangeNumber) throws InterruptedException { SegmentFetcher segmentFetcher = new StaticSegmentFetcher(Collections.emptyMap()); SDKReadinessGates gates = new SDKReadinessGates(); + SplitCache cache = new InMemoryCacheImp(startingChangeNumber); - RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(splitChangeFetcher, new SplitParser(segmentFetcher), gates, startingChangeNumber); + RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(splitChangeFetcher, new SplitParser(segmentFetcher), gates, cache); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 3, TimeUnit.SECONDS); @@ -133,8 +132,8 @@ public void when_parser_fails_we_remove_the_experiment() throws InterruptedExcep when(splitChangeFetcher.fetch(0L)).thenReturn(invalidReturn); when(splitChangeFetcher.fetch(1L)).thenReturn(noReturn); - - RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(splitChangeFetcher, new SplitParser(segmentFetcher), new SDKReadinessGates(), -1L); + SplitCache cache = new InMemoryCacheImp(-1); + RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(splitChangeFetcher, new SplitParser(segmentFetcher), new SDKReadinessGates(), cache); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -148,11 +147,12 @@ public void when_parser_fails_we_remove_the_experiment() throws InterruptedExcep public void if_there_is_a_problem_talking_to_split_change_count_down_latch_is_not_decremented() throws Exception { SegmentFetcher segmentFetcher = new StaticSegmentFetcher(Collections.emptyMap()); SDKReadinessGates gates = new SDKReadinessGates(); + SplitCache cache = new InMemoryCacheImp(-1); SplitChangeFetcher splitChangeFetcher = mock(SplitChangeFetcher.class); when(splitChangeFetcher.fetch(-1L)).thenThrow(new RuntimeException()); - RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(splitChangeFetcher, new SplitParser(segmentFetcher), gates, -1L); + RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(splitChangeFetcher, new SplitParser(segmentFetcher), gates, cache); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -186,11 +186,12 @@ public void works_with_user_defined_segments() throws Exception { String segmentName = "foosegment"; AChangePerCallSplitChangeFetcher experimentChangeFetcher = new AChangePerCallSplitChangeFetcher(segmentName); SDKReadinessGates gates = new SDKReadinessGates(); + SplitCache cache = new InMemoryCacheImp(startingChangeNumber); SegmentChangeFetcher segmentChangeFetcher = new NoChangeSegmentChangeFetcher(); SegmentFetcher segmentFetcher = new RefreshableSegmentFetcher(segmentChangeFetcher, 1,10, gates); segmentFetcher.startPeriodicFetching(); - RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(experimentChangeFetcher, new SplitParser(segmentFetcher), gates, startingChangeNumber); + RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(experimentChangeFetcher, new SplitParser(segmentFetcher), gates, cache); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -211,155 +212,28 @@ public void works_with_user_defined_segments() throws Exception { } @Test - public void fetch_traffic_type_names_works_with_adds() throws Exception { - long startingChangeNumber = -1; - SplitChangeFetcherWithTrafficTypeNames changeFetcher = new SplitChangeFetcherWithTrafficTypeNames(); - SDKReadinessGates gates = new SDKReadinessGates(); - - SegmentChangeFetcher segmentChangeFetcher = new NoChangeSegmentChangeFetcher(); - SegmentFetcher segmentFetcher = new RefreshableSegmentFetcher(segmentChangeFetcher, 1,10, gates); - RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(changeFetcher, new SplitParser(segmentFetcher), gates, startingChangeNumber); - - // Before, it should be empty - Set usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - Set expected = Sets.newHashSet(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, it starts with since -1; - changeFetcher.addSplitForSince(-1L, "test_1", "user"); - executeOnce(fetcher); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - expected.add("user"); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, now with 0; - changeFetcher.addSplitForSince(0L, "test_2", "user"); - changeFetcher.addSplitForSince(0L, "test_3", "account"); - executeOnce(fetcher); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - expected.add("account"); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, now with 1; - changeFetcher.addSplitForSince(1L, "test_4", "experiment"); - executeOnce(fetcher); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - expected.add("experiment"); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, now with 2; - changeFetcher.addSplitForSince(2L, "test_2", "user"); - changeFetcher.addSplitForSince(2L, "test_4", "account"); - changeFetcher.addSplitForSince(2L, "test_5", "experiment"); - executeOnce(fetcher); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - } - - @Test - public void fetch_traffic_type_names_already_existing_split() throws Exception { - long startingChangeNumber = -1; + public void trafficTypesExist() { SplitChangeFetcherWithTrafficTypeNames changeFetcher = new SplitChangeFetcherWithTrafficTypeNames(); SDKReadinessGates gates = new SDKReadinessGates(); + SplitCache cache = new InMemoryCacheImp(-1); SegmentChangeFetcher segmentChangeFetcher = new NoChangeSegmentChangeFetcher(); SegmentFetcher segmentFetcher = new RefreshableSegmentFetcher(segmentChangeFetcher, 1,10, gates); - RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(changeFetcher, new SplitParser(segmentFetcher), gates, startingChangeNumber); - - // Before, it should be empty - Set usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - Set expected = Sets.newHashSet(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, it starts with since -1; - changeFetcher.addSplitForSince(-1L, "test_1", "user"); - executeOnce(fetcher); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - expected.add("user"); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // Simulate that the split arrives again as active because it has been updated - changeFetcher.addSplitForSince(0L, "test_1", "user"); - executeOnce(fetcher); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - expected.add("user"); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // Simulate that the split is now removed. Traffic type user should no longer be present. - changeFetcher.removeSplitForSince(1L, "test_1", "user"); - executeOnce(fetcher); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - expected.clear(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - } + RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(changeFetcher, new SplitParser(segmentFetcher), gates, cache); + cache.put(ParsedSplit.createParsedSplitForTests("splitName_1", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2)); + cache.put(ParsedSplit.createParsedSplitForTests("splitName_2", 0, false, "default_treatment", new ArrayList<>(), "tt", 123, 2)); + cache.put(ParsedSplit.createParsedSplitForTests("splitName_3", 0, false, "default_treatment", new ArrayList<>(), "tt_2", 123, 2)); + cache.put(ParsedSplit.createParsedSplitForTests("splitName_4", 0, false, "default_treatment", new ArrayList<>(), "tt_3", 123, 2)); - @Test - public void fetch_traffic_type_names_works_with_remove() throws Exception { - long startingChangeNumber = -1; - SplitChangeFetcherWithTrafficTypeNames changeFetcher = new SplitChangeFetcherWithTrafficTypeNames(); - SDKReadinessGates gates = new SDKReadinessGates(); + assertTrue(fetcher.trafficTypeExists("tt_2")); + assertTrue(fetcher.trafficTypeExists("tt")); + assertFalse(fetcher.trafficTypeExists("tt_5")); - SegmentChangeFetcher segmentChangeFetcher = new NoChangeSegmentChangeFetcher(); - SegmentFetcher segmentFetcher = new RefreshableSegmentFetcher(segmentChangeFetcher, 1,10, gates); - RefreshableSplitFetcher fetcher = new RefreshableSplitFetcher(changeFetcher, new SplitParser(segmentFetcher), gates, startingChangeNumber); - - Set expected = Sets.newHashSet(); - - // execute once, it starts with since -1; - changeFetcher.addSplitForSince(-1L, "test_1", "user"); - executeOnce(fetcher); - Set usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - expected.add("user"); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, now with 0; - changeFetcher.addSplitForSince(0L, "test_2", "user"); - changeFetcher.addSplitForSince(0L, "test_3", "account"); - executeOnce(fetcher); - expected.add("account"); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, now with 1; - // This removes test_1, but still test_2 exists with user, so it should still return user and account - changeFetcher.removeSplitForSince(1L, "test_1", "user"); - executeOnce(fetcher); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, now with 2; - // This removes test_2, so now there are no more splits with traffic type user. - changeFetcher.removeSplitForSince(2L, "test_2", "user"); - executeOnce(fetcher); - expected.remove("user"); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, now with 3; - // This removes test_3, which removes account, now it should be empty - changeFetcher.removeSplitForSince(3L, "test_3", "account"); - executeOnce(fetcher); - expected.remove("account"); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - - // execute once, now with 4; - // Adding user once more - changeFetcher.addSplitForSince(4L, "test_1", "user"); - executeOnce(fetcher); - expected.add("user"); - usedTrafficTypes = fetcher.fetchKnownTrafficTypes(); - Assert.assertThat(usedTrafficTypes, Matchers.is(Matchers.equalTo(expected))); - } + cache.remove("splitName_2"); + assertTrue(fetcher.trafficTypeExists("tt")); - private void executeOnce(Runnable runnable) throws InterruptedException { - // execute the fetcher for a little bit. - ExecutorService executor = Executors.newSingleThreadExecutor(); - executor.submit(runnable); - executor.shutdown(); - executor.awaitTermination(10, TimeUnit.SECONDS); + cache.remove("splitName_1"); + assertFalse(fetcher.trafficTypeExists("tt")); } - - }