From 163afc83b0cf7386405867173d9d3a81b2cba51e Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 5 Mar 2025 16:01:15 -0800 Subject: [PATCH 1/3] model and memory storage --- splitio/models/grammar/condition.py | 10 +- splitio/models/grammar/matchers/__init__.py | 5 +- .../grammar/matchers/rule_based_segment.py | 48 ++++ splitio/models/rule_based_segments.py | 113 +++++++++ splitio/storage/__init__.py | 73 +++++- splitio/storage/inmemmory.py | 232 +++++++++++++++++- splitio/util/storage_helper.py | 29 ++- tests/models/test_rule_based_segments.py | 82 +++++++ tests/storage/test_inmemory_storage.py | 69 +++++- tests/sync/test_synchronizer.py | 2 - 10 files changed, 652 insertions(+), 11 deletions(-) create mode 100644 splitio/models/grammar/matchers/rule_based_segment.py create mode 100644 splitio/models/rule_based_segments.py create mode 100644 tests/models/test_rule_based_segments.py diff --git a/splitio/models/grammar/condition.py b/splitio/models/grammar/condition.py index 778c7867..79fdb928 100644 --- a/splitio/models/grammar/condition.py +++ b/splitio/models/grammar/condition.py @@ -119,10 +119,12 @@ def from_raw(raw_condition): :return: A condition object. :rtype: Condition """ - parsed_partitions = [ - partitions.from_raw(raw_partition) - for raw_partition in raw_condition['partitions'] - ] + parsed_partitions = [] + if raw_condition.get("partitions") is not None: + parsed_partitions = [ + partitions.from_raw(raw_partition) + for raw_partition in raw_condition['partitions'] + ] matcher_objects = [matchers.from_raw(x) for x in raw_condition['matcherGroup']['matchers']] diff --git a/splitio/models/grammar/matchers/__init__.py b/splitio/models/grammar/matchers/__init__.py index 34006e8b..def75626 100644 --- a/splitio/models/grammar/matchers/__init__.py +++ b/splitio/models/grammar/matchers/__init__.py @@ -10,6 +10,7 @@ from splitio.models.grammar.matchers.misc import BooleanMatcher, DependencyMatcher from splitio.models.grammar.matchers.semver import EqualToSemverMatcher, GreaterThanOrEqualToSemverMatcher, LessThanOrEqualToSemverMatcher, \ BetweenSemverMatcher, InListSemverMatcher +from splitio.models.grammar.matchers.rule_based_segment import RuleBasedSegmentMatcher MATCHER_TYPE_ALL_KEYS = 'ALL_KEYS' @@ -34,6 +35,7 @@ MATCHER_LESS_THAN_OR_EQUAL_TO_SEMVER = 'LESS_THAN_OR_EQUAL_TO_SEMVER' MATCHER_BETWEEN_SEMVER = 'BETWEEN_SEMVER' MATCHER_INLIST_SEMVER = 'IN_LIST_SEMVER' +MATCHER_IN_RULE_BASED_SEGMENT = 'IN_RULE_BASED_SEGMENT' _MATCHER_BUILDERS = { @@ -58,7 +60,8 @@ MATCHER_GREATER_THAN_OR_EQUAL_TO_SEMVER: GreaterThanOrEqualToSemverMatcher, MATCHER_LESS_THAN_OR_EQUAL_TO_SEMVER: LessThanOrEqualToSemverMatcher, MATCHER_BETWEEN_SEMVER: BetweenSemverMatcher, - MATCHER_INLIST_SEMVER: InListSemverMatcher + MATCHER_INLIST_SEMVER: InListSemverMatcher, + MATCHER_IN_RULE_BASED_SEGMENT: RuleBasedSegmentMatcher } def from_raw(raw_matcher): diff --git a/splitio/models/grammar/matchers/rule_based_segment.py b/splitio/models/grammar/matchers/rule_based_segment.py new file mode 100644 index 00000000..0e0aa665 --- /dev/null +++ b/splitio/models/grammar/matchers/rule_based_segment.py @@ -0,0 +1,48 @@ +"""Rule based segment matcher classes.""" +from splitio.models.grammar.matchers.base import Matcher + +class RuleBasedSegmentMatcher(Matcher): + + def _build(self, raw_matcher): + """ + Build an RuleBasedSegmentMatcher. + + :param raw_matcher: raw matcher as fetched from splitChanges response. + :type raw_matcher: dict + """ + self._rbs_segment_name = raw_matcher['userDefinedSegmentMatcherData']['segmentName'] + + def _match(self, key, attributes=None, context=None): + """ + Evaluate user input against a matcher and return whether the match is successful. + + :param key: User key. + :type key: str. + :param attributes: Custom user attributes. + :type attributes: dict. + :param context: Evaluation context + :type context: dict + + :returns: Wheter the match is successful. + :rtype: bool + """ + if self._rbs_segment_name == None: + return False + + # Check if rbs segment has exclusions + if context['ec'].segment_rbs_memberships.get(self._rbs_segment_name): + return False + + for parsed_condition in context['ec'].segment_rbs_conditions.get(self._rbs_segment_name): + if parsed_condition.matches(key, attributes, context): + return True + + return False + + def _add_matcher_specific_properties_to_json(self): + """Return UserDefinedSegment specific properties.""" + return { + 'userDefinedSegmentMatcherData': { + 'segmentName': self._rbs_segment_name + } + } \ No newline at end of file diff --git a/splitio/models/rule_based_segments.py b/splitio/models/rule_based_segments.py new file mode 100644 index 00000000..4ff548b2 --- /dev/null +++ b/splitio/models/rule_based_segments.py @@ -0,0 +1,113 @@ +"""RuleBasedSegment module.""" + +import logging + +from splitio.models import MatcherNotFoundException +from splitio.models.splits import _DEFAULT_CONDITIONS_TEMPLATE +from splitio.models.grammar import condition + +_LOGGER = logging.getLogger(__name__) + +class RuleBasedSegment(object): + """RuleBasedSegment object class.""" + + def __init__(self, name, traffic_yype_Name, change_number, status, conditions, excluded): + """ + Class constructor. + + :param name: Segment name. + :type name: str + :param traffic_yype_Name: traffic type name. + :type traffic_yype_Name: str + :param change_number: change number. + :type change_number: str + :param status: status. + :type status: str + :param conditions: List of conditions belonging to the segment. + :type conditions: List + :param excluded: excluded objects. + :type excluded: Excluded + """ + self._name = name + self._traffic_yype_Name = traffic_yype_Name + self._change_number = change_number + self._status = status + self._conditions = conditions + self._excluded = excluded + + @property + def name(self): + """Return segment name.""" + return self._name + + @property + def traffic_yype_Name(self): + """Return traffic type name.""" + return self._traffic_yype_Name + + @property + def change_number(self): + """Return change number.""" + return self._change_number + + @property + def status(self): + """Return status.""" + return self._status + + @property + def conditions(self): + """Return conditions.""" + return self._conditions + + @property + def excluded(self): + """Return excluded.""" + return self._excluded + +def from_raw(raw_rule_based_segment): + """ + Parse a Rule based segment from a JSON portion of splitChanges. + + :param raw_rule_based_segment: JSON object extracted from a splitChange's response + :type raw_rule_based_segment: dict + + :return: A parsed RuleBasedSegment object capable of performing evaluations. + :rtype: RuleBasedSegment + """ + try: + conditions = [condition.from_raw(c) for c in raw_rule_based_segment['conditions']] + except MatcherNotFoundException as e: + _LOGGER.error(str(e)) + _LOGGER.debug("Using default conditions template for feature flag: %s", raw_rule_based_segment['name']) + conditions = [condition.from_raw(_DEFAULT_CONDITIONS_TEMPLATE)] + return RuleBasedSegment( + raw_rule_based_segment['name'], + raw_rule_based_segment['trafficTypeName'], + raw_rule_based_segment['changeNumber'], + raw_rule_based_segment['status'], + conditions, + Excluded(raw_rule_based_segment['excluded']['keys'], raw_rule_based_segment['excluded']['segments']) + ) + +class Excluded(object): + + def __init__(self, keys, segments): + """ + Class constructor. + + :param keys: List of excluded keys in a rule based segment. + :type keys: List + :param segments: List of excluded segments in a rule based segment. + :type segments: List + """ + self._keys = keys + self._segments = segments + + def get_excluded_keys(self): + """Return excluded keys.""" + return self._keys + + def get_excluded_segments(self): + """Return excluded segments""" + return self._segments diff --git a/splitio/storage/__init__.py b/splitio/storage/__init__.py index cd3bf1a0..9178398a 100644 --- a/splitio/storage/__init__.py +++ b/splitio/storage/__init__.py @@ -354,4 +354,75 @@ def intersect(self, flag_sets): if not isinstance(flag_sets, set) or len(flag_sets) == 0: return False - return any(self.flag_sets.intersection(flag_sets)) \ No newline at end of file + return any(self.flag_sets.intersection(flag_sets)) + +class RuleBasedSegmentsStorage(object, metaclass=abc.ABCMeta): + """SplitRule based segment storage interface implemented as an abstract class.""" + + @abc.abstractmethod + def get(self, segment_name): + """ + Retrieve a rule based segment. + + :param segment_name: Name of the segment to fetch. + :type segment_name: str + + :rtype: str + """ + pass + + @abc.abstractmethod + def update(self, to_add, to_delete, new_change_number): + """ + Update rule based segment.. + + :param to_add: List of rule based segment. to add + :type to_add: list[splitio.models.rule_based_segments.RuleBasedSegment] + :param to_delete: List of rule based segment. to delete + :type to_delete: list[splitio.models.rule_based_segments.RuleBasedSegment] + :param new_change_number: New change number. + :type new_change_number: int + """ + pass + + @abc.abstractmethod + def get_change_number(self): + """ + Retrieve latest rule based segment change number. + + :rtype: int + """ + pass + + @abc.abstractmethod + def contains(self, segment_names): + """ + Return whether the traffic type exists in at least one rule based segment in cache. + + :param traffic_type_name: Traffic type to validate. + :type traffic_type_name: str + + :return: True if the traffic type is valid. False otherwise. + :rtype: bool + """ + pass + + @abc.abstractmethod + def get_segment_names(self): + """ + Retrieve a list of all excluded segments names. + + :return: List of segment names. + :rtype: list(str) + """ + pass + + @abc.abstractmethod + def get_large_segment_names(self): + """ + Retrieve a list of all excluded large segments names. + + :return: List of segment names. + :rtype: list(str) + """ + pass \ No newline at end of file diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index e4cf3da3..f7af8825 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -7,7 +7,7 @@ from splitio.models.segments import Segment from splitio.models.telemetry import HTTPErrors, HTTPLatencies, MethodExceptions, MethodLatencies, LastSynchronization, StreamingEvents, TelemetryConfig, TelemetryCounters, CounterConstants, \ HTTPErrorsAsync, HTTPLatenciesAsync, MethodExceptionsAsync, MethodLatenciesAsync, LastSynchronizationAsync, StreamingEventsAsync, TelemetryConfigAsync, TelemetryCountersAsync -from splitio.storage import FlagSetsFilter, SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage +from splitio.storage import FlagSetsFilter, SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage, RuleBasedSegmentsStorage from splitio.optional.loaders import asyncio MAX_SIZE_BYTES = 5 * 1024 * 1024 @@ -107,6 +107,236 @@ def remove_flag_set(self, flag_sets, feature_flag_name, should_filter): if self.flag_set_exist(flag_set) and len(self.get_flag_set(flag_set)) == 0 and not should_filter: self._remove_flag_set(flag_set) +class InMemoryRuleBasedSegmentStorage(RuleBasedSegmentsStorage): + """InMemory implementation of a feature flag storage base.""" + def __init__(self): + """Constructor.""" + self._lock = threading.RLock() + self._rule_based_segments = {} + self._change_number = -1 + + def get(self, segment_name): + """ + Retrieve a rule based segment. + + :param segment_name: Name of the segment to fetch. + :type segment_name: str + + :rtype: splitio.models.rule_based_segments.RuleBasedSegment + """ + with self._lock: + return self._rule_based_segments.get(segment_name) + + def update(self, to_add, to_delete, new_change_number): + """ + Update rule based segment. + + :param to_add: List of rule based segment. to add + :type to_add: list[splitio.models.rule_based_segments.RuleBasedSegment] + :param to_delete: List of rule based segment. to delete + :type to_delete: list[splitio.models.rule_based_segments.RuleBasedSegment] + :param new_change_number: New change number. + :type new_change_number: int + """ + [self._put(add_segment) for add_segment in to_add] + [self._remove(delete_segment) for delete_segment in to_delete] + self._set_change_number(new_change_number) + + def _put(self, rule_based_segment): + """ + Store a rule based segment. + + :param rule_based_segment: RuleBasedSegment object. + :type rule_based_segment: splitio.models.rule_based_segments.RuleBasedSegment + """ + with self._lock: + self._rule_based_segments[rule_based_segment.name] = rule_based_segment + + def _remove(self, segment_name): + """ + Remove a rule based segment. + + :param segment_name: Name of the rule based segment to remove. + :type segment_name: str + + :return: True if the rule based segment was found and removed. False otherwise. + :rtype: bool + """ + with self._lock: + rule_based_segment = self._rule_based_segments.get(segment_name) + if not rule_based_segment: + _LOGGER.warning("Tried to delete nonexistant Rule based segment %s. Skipping", segment_name) + return False + + self._rule_based_segments.pop(segment_name) + return True + + def get_change_number(self): + """ + Retrieve latest rule based segment change number. + + :rtype: int + """ + with self._lock: + return self._change_number + + def _set_change_number(self, new_change_number): + """ + Set the latest change number. + + :param new_change_number: New change number. + :type new_change_number: int + """ + with self._lock: + self._change_number = new_change_number + + def get_segment_names(self): + """ + Retrieve a list of all excluded segments names. + + :return: List of segment names. + :rtype: list(str) + """ + with self._lock: + return list(self._rule_based_segments.keys()) + + def get_large_segment_names(self): + """ + Retrieve a list of all excluded large segments names. + + :return: List of segment names. + :rtype: list(str) + """ + pass + + def contains(self, segment_names): + """ + Return whether the segment exists in storage + + :param segment_names: rule based segment name + :type segment_names: str + + :return: True if the segment exists. False otherwise. + :rtype: bool + """ + with self._lock: + return set(segment_names).issubset(self._rule_based_segments.keys()) + +class InMemoryRuleBasedSegmentStorageAsync(RuleBasedSegmentsStorage): + """InMemory implementation of a feature flag storage base.""" + def __init__(self): + """Constructor.""" + self._lock = asyncio.Lock() + self._rule_based_segments = {} + self._change_number = -1 + + async def get(self, segment_name): + """ + Retrieve a rule based segment. + + :param segment_name: Name of the segment to fetch. + :type segment_name: str + + :rtype: splitio.models.rule_based_segments.RuleBasedSegment + """ + async with self._lock: + return self._rule_based_segments.get(segment_name) + + async def update(self, to_add, to_delete, new_change_number): + """ + Update rule based segment. + + :param to_add: List of rule based segment. to add + :type to_add: list[splitio.models.rule_based_segments.RuleBasedSegment] + :param to_delete: List of rule based segment. to delete + :type to_delete: list[splitio.models.rule_based_segments.RuleBasedSegment] + :param new_change_number: New change number. + :type new_change_number: int + """ + [await self._put(add_segment) for add_segment in to_add] + [await self._remove(delete_segment) for delete_segment in to_delete] + await self._set_change_number(new_change_number) + + async def _put(self, rule_based_segment): + """ + Store a rule based segment. + + :param rule_based_segment: RuleBasedSegment object. + :type rule_based_segment: splitio.models.rule_based_segments.RuleBasedSegment + """ + async with self._lock: + self._rule_based_segments[rule_based_segment.name] = rule_based_segment + + async def _remove(self, segment_name): + """ + Remove a rule based segment. + + :param segment_name: Name of the rule based segment to remove. + :type segment_name: str + + :return: True if the rule based segment was found and removed. False otherwise. + :rtype: bool + """ + async with self._lock: + rule_based_segment = self._rule_based_segments.get(segment_name) + if not rule_based_segment: + _LOGGER.warning("Tried to delete nonexistant Rule based segment %s. Skipping", segment_name) + return False + + self._rule_based_segments.pop(segment_name) + return True + + async def get_change_number(self): + """ + Retrieve latest rule based segment change number. + + :rtype: int + """ + async with self._lock: + return self._change_number + + async def _set_change_number(self, new_change_number): + """ + Set the latest change number. + + :param new_change_number: New change number. + :type new_change_number: int + """ + async with self._lock: + self._change_number = new_change_number + + async def get_segment_names(self): + """ + Retrieve a list of all excluded segments names. + + :return: List of segment names. + :rtype: list(str) + """ + async with self._lock: + return list(self._rule_based_segments.keys()) + + async def get_large_segment_names(self): + """ + Retrieve a list of all excluded large segments names. + + :return: List of segment names. + :rtype: list(str) + """ + pass + + async def contains(self, segment_names): + """ + Return whether the segment exists in storage + + :param segment_names: rule based segment name + :type segment_names: str + + :return: True if the segment exists. False otherwise. + :rtype: bool + """ + async with self._lock: + return set(segment_names).issubset(self._rule_based_segments.keys()) + class InMemorySplitStorageBase(SplitStorage): """InMemory implementation of a feature flag storage base.""" diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index 8476cec2..b09a9f4e 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -1,6 +1,5 @@ """Storage Helper.""" import logging - from splitio.models import splits _LOGGER = logging.getLogger(__name__) @@ -33,6 +32,34 @@ def update_feature_flag_storage(feature_flag_storage, feature_flags, change_numb feature_flag_storage.update(to_add, to_delete, change_number) return segment_list +def update_rule_based_segment_storage(rule_based_segment_storage, rule_based_segments, change_number): + """ + Update rule based segment storage from given list of rule based segments + + :param rule_based_segment_storage: rule based segment storage instance + :type rule_based_segment_storage: splitio.storage.RuleBasedSegmentStorage + :param rule_based_segments: rule based segment instance to validate. + :type rule_based_segments: splitio.models.rule_based_segments.RuleBasedSegment + :param: last change number + :type: int + + :return: segments list from excluded segments list + :rtype: list(str) + """ + segment_list = set() + to_add = [] + to_delete = [] + for rule_based_segment in rule_based_segments: + if rule_based_segment.status == "ACTIVE": + to_add.append(rule_based_segment) + segment_list.update(set(rule_based_segment.excluded.get_excluded_segments())) + else: + if rule_based_segment_storage.get(rule_based_segment.name) is not None: + to_delete.append(rule_based_segment.name) + + rule_based_segment_storage.update(to_add, to_delete, change_number) + return segment_list + async def update_feature_flag_storage_async(feature_flag_storage, feature_flags, change_number): """ Update feature flag storage from given list of feature flags while checking the flag set logic diff --git a/tests/models/test_rule_based_segments.py b/tests/models/test_rule_based_segments.py new file mode 100644 index 00000000..96cbdd30 --- /dev/null +++ b/tests/models/test_rule_based_segments.py @@ -0,0 +1,82 @@ +"""Split model tests module.""" +import copy + +from splitio.models import rule_based_segments +from splitio.models import splits +from splitio.models.grammar.condition import Condition + +class RuleBasedSegmentModelTests(object): + """Rule based segment model tests.""" + + raw = { + "changeNumber": 123, + "name": "sample_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":["mauro@split.io","gaston@split.io"], + "segments":[] + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": False, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + } + ] + } + + def test_from_raw(self): + """Test split model parsing.""" + parsed = rule_based_segments.from_raw(self.raw) + assert isinstance(parsed, rule_based_segments.RuleBasedSegment) + assert parsed.change_number == 123 + assert parsed.name == 'sample_rule_based_segment' + assert parsed.status == 'ACTIVE' + assert len(parsed.conditions) == 1 + assert parsed.excluded.get_excluded_keys() == ["mauro@split.io","gaston@split.io"] + assert parsed.excluded.get_excluded_segments() == [] + conditions = parsed.conditions[0].to_json() + assert conditions['matcherGroup']['matchers'][0] == { + 'betweenMatcherData': None, 'booleanMatcherData': None, 'dependencyMatcherData': None, + 'stringMatcherData': None, 'unaryNumericMatcherData': None, 'userDefinedSegmentMatcherData': None, + "keySelector": { + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": False, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + + def test_incorrect_matcher(self): + """Test incorrect matcher in split model parsing.""" + rbs = copy.deepcopy(self.raw) + rbs['conditions'][0]['matcherGroup']['matchers'][0]['matcherType'] = 'INVALID_MATCHER' + rbs = rule_based_segments.from_raw(rbs) + assert rbs.conditions[0].to_json() == splits._DEFAULT_CONDITIONS_TEMPLATE + + # using multiple conditions + rbs = copy.deepcopy(self.raw) + rbs['conditions'].append(rbs['conditions'][0]) + rbs['conditions'][0]['matcherGroup']['matchers'][0]['matcherType'] = 'INVALID_MATCHER' + parsed = rule_based_segments.from_raw(rbs) + assert parsed.conditions[0].to_json() == splits._DEFAULT_CONDITIONS_TEMPLATE \ No newline at end of file diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index bf38ed57..1bf2f3de 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -11,7 +11,8 @@ from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageProducerAsync from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, InMemorySegmentStorageAsync, InMemorySplitStorageAsync, \ InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, InMemoryImpressionStorageAsync, InMemoryEventStorageAsync, \ - InMemoryTelemetryStorageAsync, FlagSets + InMemoryTelemetryStorageAsync, FlagSets, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync +from splitio.models.rule_based_segments import RuleBasedSegment class FlagSetsFilterTests(object): """Flag sets filter storage tests.""" @@ -1807,3 +1808,69 @@ async def test_pop_latencies(self): assert(sync_latency == {'httpLatencies': {'split': [4] + [0] * 22, 'segment': [4] + [0] * 22, 'impression': [2] + [0] * 22, 'impressionCount': [2] + [0] * 22, 'event': [2] + [0] * 22, 'telemetry': [3] + [0] * 22, 'token': [3] + [0] * 22}}) + +class InMemoryRuleBasedSegmentStorageTests(object): + """In memory rule based segment storage test cases.""" + + def test_storing_retrieving_segments(self, mocker): + """Test storing and retrieving splits works.""" + rbs_storage = InMemoryRuleBasedSegmentStorage() + + segment1 = mocker.Mock(spec=RuleBasedSegment) + name_property = mocker.PropertyMock() + name_property.return_value = 'some_segment' + type(segment1).name = name_property + + segment2 = mocker.Mock() + name2_prop = mocker.PropertyMock() + name2_prop.return_value = 'segment2' + type(segment2).name = name2_prop + + rbs_storage.update([segment1, segment2], [], -1) + assert rbs_storage.get('some_segment') == segment1 + assert rbs_storage.get_segment_names() == ['some_segment', 'segment2'] + assert rbs_storage.get('nonexistant_segment') is None + + rbs_storage.update([], ['some_segment'], -1) + assert rbs_storage.get('some_segment') is None + + def test_store_get_changenumber(self): + """Test that storing and retrieving change numbers works.""" + storage = InMemoryRuleBasedSegmentStorage() + assert storage.get_change_number() == -1 + storage.update([], [], 5) + assert storage.get_change_number() == 5 + +class InMemoryRuleBasedSegmentStorageAsyncTests(object): + """In memory rule based segment storage test cases.""" + + @pytest.mark.asyncio + async def test_storing_retrieving_segments(self, mocker): + """Test storing and retrieving splits works.""" + rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + + segment1 = mocker.Mock(spec=RuleBasedSegment) + name_property = mocker.PropertyMock() + name_property.return_value = 'some_segment' + type(segment1).name = name_property + + segment2 = mocker.Mock() + name2_prop = mocker.PropertyMock() + name2_prop.return_value = 'segment2' + type(segment2).name = name2_prop + + await rbs_storage.update([segment1, segment2], [], -1) + assert await rbs_storage.get('some_segment') == segment1 + assert await rbs_storage.get_segment_names() == ['some_segment', 'segment2'] + assert await rbs_storage.get('nonexistant_segment') is None + + await rbs_storage.update([], ['some_segment'], -1) + assert await rbs_storage.get('some_segment') is None + + @pytest.mark.asyncio + async def test_store_get_changenumber(self): + """Test that storing and retrieving change numbers works.""" + storage = InMemoryRuleBasedSegmentStorageAsync() + assert await storage.get_change_number() == -1 + await storage.update([], [], 5) + assert await storage.get_change_number() == 5 diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index 8e10d771..b2ef9fa0 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -1,6 +1,4 @@ """Synchronizer tests.""" - -from turtle import clear import unittest.mock as mock import pytest From a64a06efee623d87de12d5639668d839575db011 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 5 Mar 2025 20:17:02 -0800 Subject: [PATCH 2/3] update storage helper --- splitio/util/storage_helper.py | 28 ++++++++++++++ tests/storage/test_inmemory_storage.py | 53 ++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index b09a9f4e..f547a701 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -88,6 +88,34 @@ async def update_feature_flag_storage_async(feature_flag_storage, feature_flags, await feature_flag_storage.update(to_add, to_delete, change_number) return segment_list +async def update_rule_based_segment_storage_async(rule_based_segment_storage, rule_based_segments, change_number): + """ + Update rule based segment storage from given list of rule based segments + + :param rule_based_segment_storage: rule based segment storage instance + :type rule_based_segment_storage: splitio.storage.RuleBasedSegmentStorage + :param rule_based_segments: rule based segment instance to validate. + :type rule_based_segments: splitio.models.rule_based_segments.RuleBasedSegment + :param: last change number + :type: int + + :return: segments list from excluded segments list + :rtype: list(str) + """ + segment_list = set() + to_add = [] + to_delete = [] + for rule_based_segment in rule_based_segments: + if rule_based_segment.status == "ACTIVE": + to_add.append(rule_based_segment) + segment_list.update(set(rule_based_segment.excluded.get_excluded_segments())) + else: + if await rule_based_segment_storage.get(rule_based_segment.name) is not None: + to_delete.append(rule_based_segment.name) + + await rule_based_segment_storage.update(to_add, to_delete, change_number) + return segment_list + def get_valid_flag_sets(flag_sets, flag_set_filter): """ Check each flag set in given array, return it if exist in a given config flag set array, if config array is empty return all diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 1bf2f3de..9c5b6ed2 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -2,6 +2,7 @@ # pylint: disable=no-self-use import random import pytest +import copy from splitio.models.splits import Split from splitio.models.segments import Segment @@ -13,6 +14,7 @@ InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, InMemoryImpressionStorageAsync, InMemoryEventStorageAsync, \ InMemoryTelemetryStorageAsync, FlagSets, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync from splitio.models.rule_based_segments import RuleBasedSegment +from splitio.models import rule_based_segments class FlagSetsFilterTests(object): """Flag sets filter storage tests.""" @@ -1840,6 +1842,31 @@ def test_store_get_changenumber(self): assert storage.get_change_number() == -1 storage.update([], [], 5) assert storage.get_change_number() == 5 + + def test_contains(self): + raw = { + "changeNumber": 123, + "name": "segment1", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":[], + "segments":[] + }, + "conditions": [] + } + segment1 = rule_based_segments.from_raw(raw) + raw2 = copy.deepcopy(raw) + raw2["name"] = "segment2" + segment2 = rule_based_segments.from_raw(raw2) + raw3 = copy.deepcopy(raw) + raw3["name"] = "segment3" + segment3 = rule_based_segments.from_raw(raw3) + storage = InMemoryRuleBasedSegmentStorage() + storage.update([segment1, segment2, segment3], [], -1) + assert storage.contains(["segment1"]) + assert storage.contains(["segment1", "segment3"]) + assert not storage.contains(["segment5"]) class InMemoryRuleBasedSegmentStorageAsyncTests(object): """In memory rule based segment storage test cases.""" @@ -1874,3 +1901,29 @@ async def test_store_get_changenumber(self): assert await storage.get_change_number() == -1 await storage.update([], [], 5) assert await storage.get_change_number() == 5 + + @pytest.mark.asyncio + async def test_contains(self): + raw = { + "changeNumber": 123, + "name": "segment1", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded":{ + "keys":[], + "segments":[] + }, + "conditions": [] + } + segment1 = rule_based_segments.from_raw(raw) + raw2 = copy.deepcopy(raw) + raw2["name"] = "segment2" + segment2 = rule_based_segments.from_raw(raw2) + raw3 = copy.deepcopy(raw) + raw3["name"] = "segment3" + segment3 = rule_based_segments.from_raw(raw3) + storage = InMemoryRuleBasedSegmentStorageAsync() + await storage.update([segment1, segment2, segment3], [], -1) + assert await storage.contains(["segment1"]) + assert await storage.contains(["segment1", "segment3"]) + assert not await storage.contains(["segment5"]) From c07651e1ae432f796e944f1b8540ac159a412e25 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 6 Mar 2025 10:27:56 -0800 Subject: [PATCH 3/3] polish --- splitio/storage/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/splitio/storage/__init__.py b/splitio/storage/__init__.py index 9178398a..079ee863 100644 --- a/splitio/storage/__init__.py +++ b/splitio/storage/__init__.py @@ -397,12 +397,12 @@ def get_change_number(self): @abc.abstractmethod def contains(self, segment_names): """ - Return whether the traffic type exists in at least one rule based segment in cache. + Return whether the segments exists in rule based segment in cache. - :param traffic_type_name: Traffic type to validate. - :type traffic_type_name: str + :param segment_names: segment name to validate. + :type segment_names: str - :return: True if the traffic type is valid. False otherwise. + :return: True if segment names exists. False otherwise. :rtype: bool """ pass