From df6bc390416336ce6c12111ba48bddb918b88e56 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 4 Apr 2025 17:55:02 -0700 Subject: [PATCH 1/3] Added inmemory storage classes --- client/src/main/java/io/split/Spec.java | 2 +- .../engine/experiments/ParsedCondition.java | 16 ++- .../experiments/ParsedRuleBasedSegment.java | 125 ++++++++++++++++++ .../split/storages/RuleBasedSegmentCache.java | 4 + .../RuleBasedSegmentCacheCommons.java | 8 ++ .../RuleBasedSegmentCacheConsumer.java | 13 ++ .../RuleBasedSegmentCacheProducer.java | 11 ++ .../RuleBasedSegmentCacheInMemoryImp.java | 101 ++++++++++++++ .../ParsedRuleBasedSegmentTest.java | 30 +++++ ...RuleBasedSegmentCacheInMemoryImplTest.java | 55 ++++++++ 10 files changed, 359 insertions(+), 6 deletions(-) create mode 100644 client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java create mode 100644 client/src/main/java/io/split/storages/RuleBasedSegmentCache.java create mode 100644 client/src/main/java/io/split/storages/RuleBasedSegmentCacheCommons.java create mode 100644 client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java create mode 100644 client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java create mode 100644 client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java create mode 100644 client/src/test/java/io/split/engine/experiments/ParsedRuleBasedSegmentTest.java create mode 100644 client/src/test/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImplTest.java diff --git a/client/src/main/java/io/split/Spec.java b/client/src/main/java/io/split/Spec.java index 847e19e1c..cf9e005a0 100644 --- a/client/src/main/java/io/split/Spec.java +++ b/client/src/main/java/io/split/Spec.java @@ -6,7 +6,7 @@ private Spec() { // restrict instantiation } - public static String SPEC_VERSION = "1.3"; + public static String SPEC_VERSION = "1.1"; public static final String SPEC_1_3 = "1.3"; public static final String SPEC_1_1 = "1.1"; } diff --git a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java index 5c2b06b61..ad2e32a50 100644 --- a/client/src/main/java/io/split/engine/experiments/ParsedCondition.java +++ b/client/src/main/java/io/split/engine/experiments/ParsedCondition.java @@ -53,11 +53,12 @@ public int hashCode() { result = 31 * result + _matcher.hashCode(); int partitionsHashCode = 17; - for (Partition p : _partitions) { - partitionsHashCode = 31 * partitionsHashCode + p.treatment.hashCode(); - partitionsHashCode = 31 * partitionsHashCode + p.size; + if (_partitions != null) { + for (Partition p : _partitions) { + partitionsHashCode = 31 * partitionsHashCode + p.treatment.hashCode(); + partitionsHashCode = 31 * partitionsHashCode + p.size; + } } - result = 31 * result + partitionsHashCode; return result; } @@ -75,7 +76,9 @@ public boolean equals(Object obj) { if (!result) { return result; } - + if (_partitions == null) { + return result & (_partitions == other._partitions); + } if (_partitions.size() != other._partitions.size()) { return result; } @@ -97,6 +100,9 @@ public String toString() { bldr.append(_matcher); bldr.append(" then split "); boolean first = true; + if (_partitions == null) { + return bldr.toString(); + } for (Partition partition : _partitions) { if (!first) { bldr.append(','); diff --git a/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java b/client/src/main/java/io/split/engine/experiments/ParsedRuleBasedSegment.java new file mode 100644 index 000000000..ae8c8f484 --- /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 000000000..5ba55b819 --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCache.java @@ -0,0 +1,4 @@ +package io.split.storages; + +public interface RuleBasedSegmentCache extends RuleBasedSegmentCacheConsumer, RuleBasedSegmentCacheProducer { +} diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheCommons.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheCommons.java new file mode 100644 index 000000000..39a558ea7 --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheCommons.java @@ -0,0 +1,8 @@ +package io.split.storages; + +import java.util.Set; + +public interface RuleBasedSegmentCacheCommons { + long getChangeNumber(); + Set getSegments(); +} diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java new file mode 100644 index 000000000..fe582a97f --- /dev/null +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java @@ -0,0 +1,13 @@ +package io.split.storages; + +import io.split.engine.experiments.ParsedRuleBasedSegment; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public interface RuleBasedSegmentCacheConsumer extends RuleBasedSegmentCacheCommons { + ParsedRuleBasedSegment get(String name); + Collection getAll(); + List ruleBasedSegmentNames(); +} \ No newline at end of file diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java new file mode 100644 index 000000000..e3c480478 --- /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 extends RuleBasedSegmentCacheCommons{ + 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 000000000..a1f93fd8f --- /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(); + } + + public 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 000000000..d8b3efabb --- /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 000000000..c24f80120 --- /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 From 901761a1b8b5d750e1def050fa538023bb5d7279 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 10 Apr 2025 09:26:00 -0700 Subject: [PATCH 2/3] polish --- .../split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java b/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java index a1f93fd8f..812e64b1a 100644 --- a/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java +++ b/client/src/main/java/io/split/storages/memory/RuleBasedSegmentCacheInMemoryImp.java @@ -75,7 +75,7 @@ public void clear() { _concurrentMap.clear(); } - public void putMany(List ruleBasedSegments) { + private void putMany(List ruleBasedSegments) { for (ParsedRuleBasedSegment ruleBasedSegment : ruleBasedSegments) { _concurrentMap.put(ruleBasedSegment.ruleBasedSegment(), ruleBasedSegment); } From cc9aaa17a49a2519ab2eee8ab2cc6926d36f1dad Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 10 Apr 2025 10:51:35 -0700 Subject: [PATCH 3/3] Polish --- .../io/split/storages/RuleBasedSegmentCacheCommons.java | 8 -------- .../io/split/storages/RuleBasedSegmentCacheConsumer.java | 5 ++++- .../io/split/storages/RuleBasedSegmentCacheProducer.java | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) delete mode 100644 client/src/main/java/io/split/storages/RuleBasedSegmentCacheCommons.java diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheCommons.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheCommons.java deleted file mode 100644 index 39a558ea7..000000000 --- a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheCommons.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.split.storages; - -import java.util.Set; - -public interface RuleBasedSegmentCacheCommons { - long getChangeNumber(); - Set getSegments(); -} diff --git a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java index fe582a97f..0002ee1ef 100644 --- a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheConsumer.java @@ -5,9 +5,12 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; -public interface RuleBasedSegmentCacheConsumer extends RuleBasedSegmentCacheCommons { +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 index e3c480478..d01ba4062 100644 --- a/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java +++ b/client/src/main/java/io/split/storages/RuleBasedSegmentCacheProducer.java @@ -4,7 +4,7 @@ import java.util.List; -public interface RuleBasedSegmentCacheProducer extends RuleBasedSegmentCacheCommons{ +public interface RuleBasedSegmentCacheProducer { boolean remove(String name); void setChangeNumber(long changeNumber); void update(List toAdd, List toRemove, long changeNumber);