diff --git a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java index 5c2b06b6..ad2e32a5 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java @@ -53,11 +53,12 @@ public int hashCode() { result = 31 * result + _matcher.hashCode(); int partitionsHashCode = 17; - for (Partition p : _partitions) { - partitionsHashCode = 31 * partitionsHashCode + p.treatment.hashCode(); - partitionsHashCode = 31 * partitionsHashCode + p.size; + if (_partitions != null) { + for (Partition p : _partitions) { + partitionsHashCode = 31 * partitionsHashCode + p.treatment.hashCode(); + partitionsHashCode = 31 * partitionsHashCode + p.size; + } } - result = 31 * result + partitionsHashCode; return result; } @@ -75,7 +76,9 @@ public boolean equals(Object obj) { if (!result) { return result; } - + if (_partitions == null) { + return result & (_partitions == other._partitions); + } if (_partitions.size() != other._partitions.size()) { return result; } @@ -97,6 +100,9 @@ public String toString() { bldr.append(_matcher); bldr.append(" then split "); boolean first = true; + if (_partitions == null) { + return bldr.toString(); + } for (Partition partition : _partitions) { if (!first) { bldr.append(','); diff --git a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java new file mode 100644 index 00000000..ae8c8f48 --- /dev/null +++ b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java @@ -0,0 +1,125 @@ +package io.split.engine.experiments; + +import com.google.common.collect.ImmutableList; +import io.split.engine.matchers.AttributeMatcher; +import io.split.engine.matchers.UserDefinedSegmentMatcher; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class ParsedRuleBasedSegment { + + private final String _ruleBasedSegment; + private final ImmutableList _parsedCondition; + private final String _trafficTypeName; + private final long _changeNumber; + private final List _excludedKeys; + private final List _excludedSegments; + + public static ParsedRuleBasedSegment createParsedRuleBasedSegmentForTests( + String ruleBasedSegment, + List matcherAndSplits, + String trafficTypeName, + long changeNumber, + List excludedKeys, + List excludedSegments + ) { + return new ParsedRuleBasedSegment( + ruleBasedSegment, + matcherAndSplits, + trafficTypeName, + changeNumber, + excludedKeys, + excludedSegments + ); + } + + public ParsedRuleBasedSegment( + String ruleBasedSegment, + List matcherAndSplits, + String trafficTypeName, + long changeNumber, + List excludedKeys, + List excludedSegments + ) { + _ruleBasedSegment = ruleBasedSegment; + _parsedCondition = ImmutableList.copyOf(matcherAndSplits); + _trafficTypeName = trafficTypeName; + _changeNumber = changeNumber; + _excludedKeys = excludedKeys; + _excludedSegments = excludedSegments; + } + + public String ruleBasedSegment() { + return _ruleBasedSegment; + } + + public List parsedConditions() { + return _parsedCondition; + } + + public String trafficTypeName() {return _trafficTypeName;} + + public long changeNumber() {return _changeNumber;} + + public List excludedKeys() {return _excludedKeys;} + public List excludedSegments() {return _excludedSegments;} + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + _ruleBasedSegment.hashCode(); + result = 31 * result + _parsedCondition.hashCode(); + result = 31 * result + (_trafficTypeName == null ? 0 : _trafficTypeName.hashCode()); + result = 31 * result + (int)(_changeNumber ^ (_changeNumber >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof ParsedRuleBasedSegment)) return false; + + ParsedRuleBasedSegment other = (ParsedRuleBasedSegment) obj; + + return _ruleBasedSegment.equals(other._ruleBasedSegment) + && _parsedCondition.equals(other._parsedCondition) + && _trafficTypeName == null ? other._trafficTypeName == null : _trafficTypeName.equals(other._trafficTypeName) + && _changeNumber == other._changeNumber; + } + + @Override + public String toString() { + StringBuilder bldr = new StringBuilder(); + bldr.append("name:"); + bldr.append(_ruleBasedSegment); + bldr.append(", parsedConditions:"); + bldr.append(_parsedCondition); + bldr.append(", trafficTypeName:"); + bldr.append(_trafficTypeName); + bldr.append(", changeNumber:"); + bldr.append(_changeNumber); + return bldr.toString(); + + } + + public Set getSegmentsNames() { + return parsedConditions().stream() + .flatMap(parsedCondition -> parsedCondition.matcher().attributeMatchers().stream()) + .filter(ParsedRuleBasedSegment::isSegmentMatcher) + .map(ParsedRuleBasedSegment::asSegmentMatcherForEach) + .map(UserDefinedSegmentMatcher::getSegmentName) + .collect(Collectors.toSet()); + } + + private static boolean isSegmentMatcher(AttributeMatcher attributeMatcher) { + return ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate() instanceof UserDefinedSegmentMatcher; + } + + private static UserDefinedSegmentMatcher asSegmentMatcherForEach(AttributeMatcher attributeMatcher) { + return (UserDefinedSegmentMatcher) ((AttributeMatcher.NegatableMatcher) attributeMatcher.matcher()).delegate(); + } + +} diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCache.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCache.java new file mode 100644 index 00000000..5ba55b81 --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCache.java @@ -0,0 +1,4 @@ +package io.split.storages; + +public interface RuleBasedSegmentCache extends RuleBasedSegmentCacheConsumer, RuleBasedSegmentCacheProducer { +} diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java new file mode 100644 index 00000000..0002ee1e --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java @@ -0,0 +1,16 @@ +package io.split.storages; + +import io.split.engine.experiments.ParsedRuleBasedSegment; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface RuleBasedSegmentCacheConsumer { + ParsedRuleBasedSegment get(String name); + Collection getAll(); + List ruleBasedSegmentNames(); + long getChangeNumber(); + Set getSegments(); +} \ No newline at end of file diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java new file mode 100644 index 00000000..d01ba406 --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java @@ -0,0 +1,11 @@ +package io.split.storages; + +import io.split.engine.experiments.ParsedRuleBasedSegment; + +import java.util.List; + +public interface RuleBasedSegmentCacheProducer { + boolean remove(String name); + void setChangeNumber(long changeNumber); + void update(List toAdd, List toRemove, long changeNumber); +} diff --git a/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java b/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java new file mode 100644 index 00000000..812e64b1 --- /dev/null +++ b/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java @@ -0,0 +1,101 @@ +package io.split.storages.memory; + +import com.google.common.collect.Maps; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.storages.RuleBasedSegmentCache; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.ArrayList; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +public class RuleBasedSegmentCacheInMemoryImp implements RuleBasedSegmentCache { + + private static final Logger _log = LoggerFactory.getLogger(RuleBasedSegmentCacheInMemoryImp.class); + + private final ConcurrentMap _concurrentMap; + + private AtomicLong _changeNumber; + + public RuleBasedSegmentCacheInMemoryImp() { + this(-1); + } + + public RuleBasedSegmentCacheInMemoryImp(long startingChangeNumber) { + _concurrentMap = Maps.newConcurrentMap(); + _changeNumber = new AtomicLong(startingChangeNumber); + } + + @Override + public boolean remove(String name) { + ParsedRuleBasedSegment removed = _concurrentMap.remove(name); + return removed != null; + } + + @Override + public ParsedRuleBasedSegment get(String name) { + return _concurrentMap.get(name); + } + + @Override + public Collection getAll() { + return _concurrentMap.values(); + } + + @Override + public long getChangeNumber() { + return _changeNumber.get(); + } + + @Override + public void setChangeNumber(long changeNumber) { + if (changeNumber < _changeNumber.get()) { + _log.error("ChangeNumber for feature flags cache is less than previous"); + } + + _changeNumber.set(changeNumber); + } + + @Override + public List ruleBasedSegmentNames() { + List ruleBasedSegmentNamesList = new ArrayList<>(); + for (String key: _concurrentMap.keySet()) { + ruleBasedSegmentNamesList.add(_concurrentMap.get(key).ruleBasedSegment()); + } + return ruleBasedSegmentNamesList; + } + + public void clear() { + _concurrentMap.clear(); + } + + private void putMany(List ruleBasedSegments) { + for (ParsedRuleBasedSegment ruleBasedSegment : ruleBasedSegments) { + _concurrentMap.put(ruleBasedSegment.ruleBasedSegment(), ruleBasedSegment); + } + } + + @Override + public void update(List toAdd, List toRemove, long changeNumber) { + if(toAdd != null) { + putMany(toAdd); + } + if(toRemove != null) { + for(String ruleBasedSegment : toRemove) { + remove(ruleBasedSegment); + } + } + setChangeNumber(changeNumber); + } + + public Set getSegments() { + return _concurrentMap.values().stream() + .flatMap(parsedRuleBasedSegment -> parsedRuleBasedSegment.getSegmentsNames().stream()).collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java b/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java new file mode 100644 index 00000000..d8b3efab --- /dev/null +++ b/client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java @@ -0,0 +1,30 @@ +package io.split.engine.experiments; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import io.split.client.dtos.ConditionType; +import io.split.client.dtos.MatcherCombiner; +import io.split.engine.matchers.AttributeMatcher; +import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.UserDefinedSegmentMatcher; + +import org.junit.Assert; +import org.junit.Test; + +public class ParsedRuleBasedSegmentTest { + + @Test + public void works() { + AttributeMatcher segmentMatcher = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher("employees")); + CombiningMatcher segmentCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(segmentMatcher)); + ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("another_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, segmentCombiningMatcher, null, "label")),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), Lists.newArrayList("segment1", "segment2")); + + Assert.assertEquals(Sets.newHashSet("employees"), parsedRuleBasedSegment.getSegmentsNames()); + Assert.assertEquals("another_rule_based_segment", parsedRuleBasedSegment.ruleBasedSegment()); + Assert.assertEquals(Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, segmentCombiningMatcher, null, "label")), + parsedRuleBasedSegment.parsedConditions()); + Assert.assertEquals(123, parsedRuleBasedSegment.changeNumber()); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java b/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java new file mode 100644 index 00000000..c24f8012 --- /dev/null +++ b/client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java @@ -0,0 +1,55 @@ +package io.split.storages.memory; + +import com.google.common.collect.Sets; +import io.split.client.dtos.MatcherCombiner; +import io.split.engine.experiments.ParsedRuleBasedSegment; +import io.split.engine.experiments.ParsedCondition; +import io.split.client.dtos.ConditionType; + +import io.split.engine.matchers.AttributeMatcher; +import io.split.engine.matchers.CombiningMatcher; +import io.split.engine.matchers.UserDefinedSegmentMatcher; +import io.split.engine.matchers.strings.WhitelistMatcher; +import junit.framework.TestCase; +import org.junit.Test; +import com.google.common.collect.Lists; + +public class RuleBasedSegmentCacheInMemoryImplTest extends TestCase { + + @Test + public void testAddAndDeleteSegment(){ + RuleBasedSegmentCacheInMemoryImp ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); + ParsedRuleBasedSegment parsedRuleBasedSegment = new ParsedRuleBasedSegment("sample_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, "label")),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), Lists.newArrayList()); + ruleBasedSegmentCache.update(Lists.newArrayList(parsedRuleBasedSegment), null, 123); + assertEquals(123, ruleBasedSegmentCache.getChangeNumber()); + assertEquals(parsedRuleBasedSegment, ruleBasedSegmentCache.get("sample_rule_based_segment")); + + ruleBasedSegmentCache.update(null, Lists.newArrayList("sample_rule_based_segment"), 124); + assertEquals(124, ruleBasedSegmentCache.getChangeNumber()); + assertEquals(null, ruleBasedSegmentCache.get("sample_rule_based_segment")); + } + + @Test + public void testMultipleSegment(){ + RuleBasedSegmentCacheInMemoryImp ruleBasedSegmentCache = new RuleBasedSegmentCacheInMemoryImp(); + AttributeMatcher whiteListMatcher = AttributeMatcher.vanilla(new WhitelistMatcher(Lists.newArrayList("test_1", "admin"))); + CombiningMatcher whitelistCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(whiteListMatcher)); + ParsedRuleBasedSegment parsedRuleBasedSegment1 = new ParsedRuleBasedSegment("sample_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, whitelistCombiningMatcher, null, "label")),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), Lists.newArrayList(Lists.newArrayList("segment1", "segment3"))); + + AttributeMatcher segmentMatcher = AttributeMatcher.vanilla(new UserDefinedSegmentMatcher("employees")); + CombiningMatcher segmentCombiningMatcher = new CombiningMatcher(MatcherCombiner.AND, Lists.newArrayList(segmentMatcher)); + ParsedRuleBasedSegment parsedRuleBasedSegment2 = new ParsedRuleBasedSegment("another_rule_based_segment", + Lists.newArrayList(new ParsedCondition(ConditionType.WHITELIST, segmentCombiningMatcher, null, "label")),"user", + 123, Lists.newArrayList("mauro@test.io","gaston@test.io"), Lists.newArrayList("segment1", "segment2")); + + ruleBasedSegmentCache.update(Lists.newArrayList(parsedRuleBasedSegment1, parsedRuleBasedSegment2), null, 123); + assertEquals(Lists.newArrayList("another_rule_based_segment", "sample_rule_based_segment"), ruleBasedSegmentCache.ruleBasedSegmentNames()); + assertEquals(Sets.newHashSet("employees"), ruleBasedSegmentCache.getSegments()); + } +} \ No newline at end of file