From 8ab6b6ef9541d8950ad6d585026f657e9ef52fd2 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 22 Aug 2023 12:57:51 -0700 Subject: [PATCH 01/77] Update split model --- splitio/models/splits.py | 22 +++++++++++++++++----- tests/models/test_splits.py | 5 ++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/splitio/models/splits.py b/splitio/models/splits.py index 5e0ab394..9cf0eba0 100644 --- a/splitio/models/splits.py +++ b/splitio/models/splits.py @@ -7,7 +7,7 @@ SplitView = namedtuple( 'SplitView', - ['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs'] + ['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'sets'] ) @@ -41,7 +41,8 @@ def __init__( # pylint: disable=too-many-arguments algo=None, traffic_allocation=None, traffic_allocation_seed=None, - configurations=None + configurations=None, + sets=[] ): """ Class constructor. @@ -62,6 +63,8 @@ def __init__( # pylint: disable=too-many-arguments :type traffic_allocation: int :pram traffic_allocation_seed: Seed used to hash traffic allocation. :type traffic_allocation_seed: int + :pram sets: list of flag sets + :type sets: list """ self._name = name self._seed = seed @@ -90,6 +93,7 @@ def __init__( # pylint: disable=too-many-arguments self._algo = HashAlgorithm.LEGACY self._configurations = configurations + self._sets = sets @property def name(self): @@ -146,6 +150,11 @@ def traffic_allocation_seed(self): """Return the traffic allocation seed of the split.""" return self._traffic_allocation_seed + @property + def sets(self): + """Return the flag sets of the split.""" + return self._sets + def get_configurations_for(self, treatment): """Return the mapping of treatments to configurations.""" return self._configurations.get(treatment) if self._configurations else None @@ -173,7 +182,8 @@ def to_json(self): 'defaultTreatment': self.default_treatment, 'algo': self.algo.value, 'conditions': [c.to_json() for c in self.conditions], - 'configurations': self._configurations + 'configurations': self._configurations, + 'sets': self._sets } def to_split_view(self): @@ -189,7 +199,8 @@ def to_split_view(self): self.killed, list(set(part.treatment for cond in self.conditions for part in cond.partitions)), self.change_number, - self._configurations if self._configurations is not None else {} + self._configurations if self._configurations is not None else {}, + self._sets ) def local_kill(self, default_treatment, change_number): @@ -238,5 +249,6 @@ def from_raw(raw_split): raw_split.get('algo'), traffic_allocation=raw_split.get('trafficAllocation'), traffic_allocation_seed=raw_split.get('trafficAllocationSeed'), - configurations=raw_split.get('configurations') + configurations=raw_split.get('configurations'), + sets=raw_split['sets'] ) diff --git a/tests/models/test_splits.py b/tests/models/test_splits.py index 847448b0..da289ad0 100644 --- a/tests/models/test_splits.py +++ b/tests/models/test_splits.py @@ -60,6 +60,7 @@ class SplitTests(object): 'configurations': { 'on': '{"color": "blue", "size": 13}' }, + 'sets': ['set1', 'set2'] } def test_from_raw(self): @@ -79,6 +80,7 @@ def test_from_raw(self): assert len(parsed.conditions) == 2 assert parsed.get_configurations_for('on') == '{"color": "blue", "size": 13}' assert parsed._configurations == {'on': '{"color": "blue", "size": 13}'} + assert parsed.sets == ['set1', 'set2'] def test_get_segment_names(self, mocker): """Test fetching segment names.""" @@ -89,7 +91,6 @@ def test_get_segment_names(self, mocker): split1 = splits.Split( 'some_split', 123, False, 'off', 'user', 'ACTIVE', 123, [cond1, cond2]) assert split1.get_segment_names() == ['segment%d' % i for i in range(1, 5)] - def test_to_json(self): """Test json serialization.""" as_json = splits.from_raw(self.raw).to_json() @@ -105,6 +106,7 @@ def test_to_json(self): assert as_json['defaultTreatment'] == 'off' assert as_json['algo'] == 2 assert len(as_json['conditions']) == 2 + assert as_json['sets'] == ['set1', 'set2'] def test_to_split_view(self): """Test SplitView creation.""" @@ -115,3 +117,4 @@ def test_to_split_view(self): assert as_split_view.killed == self.raw['killed'] assert as_split_view.traffic_type == self.raw['trafficTypeName'] assert set(as_split_view.treatments) == set(['on', 'off']) + assert as_split_view.sets == self.raw['sets'] From 43df7f4da2d682bdab40fa2c03daa0003637fd64 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 22 Aug 2023 13:29:12 -0700 Subject: [PATCH 02/77] polish --- splitio/models/splits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/models/splits.py b/splitio/models/splits.py index 9cf0eba0..31f0fd0b 100644 --- a/splitio/models/splits.py +++ b/splitio/models/splits.py @@ -250,5 +250,5 @@ def from_raw(raw_split): traffic_allocation=raw_split.get('trafficAllocation'), traffic_allocation_seed=raw_split.get('trafficAllocationSeed'), configurations=raw_split.get('configurations'), - sets=raw_split['sets'] + sets=raw_split.get('sets') ) From 80d77df8313633eea22192ec7b6fbd3fbd003670 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 22 Aug 2023 14:01:27 -0700 Subject: [PATCH 03/77] updated api.split and api.commons --- splitio/api/commons.py | 15 ++++++++++++++- splitio/api/splits.py | 3 +++ tests/api/test_splits_api.py | 8 ++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/splitio/api/commons.py b/splitio/api/commons.py index 92004cb8..2b83fd02 100644 --- a/splitio/api/commons.py +++ b/splitio/api/commons.py @@ -57,7 +57,7 @@ def record_telemetry(status_code, elapsed, metric_name, telemetry_runtime_produc class FetchOptions(object): """Fetch Options object.""" - def __init__(self, cache_control_headers=False, change_number=None): + def __init__(self, cache_control_headers=False, change_number=None, sets=None): """ Class constructor. @@ -66,9 +66,13 @@ def __init__(self, cache_control_headers=False, change_number=None): :param change_number: ChangeNumber to use for bypassing CDN in request. :type change_number: int + + :param sets: list of flag sets + :type sets: list """ self._cache_control_headers = cache_control_headers self._change_number = change_number + self._sets = sets @property def cache_control_headers(self): @@ -80,12 +84,19 @@ def change_number(self): """Return change number.""" return self._change_number + @property + def sets(self): + """Return change number.""" + return self._sets + def __eq__(self, other): """Match between other options.""" if self._cache_control_headers != other._cache_control_headers: return False if self._change_number != other._change_number: return False + if self._sets != other._sets: + return False return True @@ -113,4 +124,6 @@ def build_fetch(change_number, fetch_options, metadata): extra_headers[_CACHE_CONTROL] = _CACHE_CONTROL_NO_CACHE if fetch_options.change_number is not None: query['till'] = fetch_options.change_number + if fetch_options.sets is not None: + query['sets'] = ','.join(fetch_options.sets) return query, extra_headers \ No newline at end of file diff --git a/splitio/api/splits.py b/splitio/api/splits.py index b584111b..8cb23cfc 100644 --- a/splitio/api/splits.py +++ b/splitio/api/splits.py @@ -56,6 +56,9 @@ def fetch_splits(self, change_number, fetch_options): query=query, ) record_telemetry(response.status_code, get_current_epoch_time_ms() - start, HTTPExceptionsAndLatencies.SPLIT, self._telemetry_runtime_producer) + if response.status_code == 414: + _LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.') + if 200 <= response.status_code < 300: return json.loads(response.body) else: diff --git a/tests/api/test_splits_api.py b/tests/api/test_splits_api.py index 3c37b199..8caa55ae 100644 --- a/tests/api/test_splits_api.py +++ b/tests/api/test_splits_api.py @@ -19,7 +19,7 @@ def test_fetch_split_changes(self, mocker): httpclient.get.return_value = client.HttpResponse(200, '{"prop1": "value1"}') split_api = splits.SplitsAPI(httpclient, 'some_api_key', SdkMetadata('1.0', 'some', '1.2.3.4'), mocker.Mock()) - response = split_api.fetch_splits(123, FetchOptions()) + response = split_api.fetch_splits(123, FetchOptions(False, None, ['set1', 'set2'])) assert response['prop1'] == 'value1' assert httpclient.get.mock_calls == [mocker.call('sdk', '/splitChanges', 'some_api_key', extra_headers={ @@ -27,7 +27,7 @@ def test_fetch_split_changes(self, mocker): 'SplitSDKMachineIP': '1.2.3.4', 'SplitSDKMachineName': 'some' }, - query={'since': 123})] + query={'since': 123, 'sets': 'set1,set2'})] httpclient.reset_mock() response = split_api.fetch_splits(123, FetchOptions(True)) @@ -42,7 +42,7 @@ def test_fetch_split_changes(self, mocker): query={'since': 123})] httpclient.reset_mock() - response = split_api.fetch_splits(123, FetchOptions(True, 123)) + response = split_api.fetch_splits(123, FetchOptions(True, 123, ['set3'])) assert response['prop1'] == 'value1' assert httpclient.get.mock_calls == [mocker.call('sdk', '/splitChanges', 'some_api_key', extra_headers={ @@ -51,7 +51,7 @@ def test_fetch_split_changes(self, mocker): 'SplitSDKMachineName': 'some', 'Cache-Control': 'no-cache' }, - query={'since': 123, 'till': 123})] + query={'since': 123, 'till': 123, 'sets': 'set3'})] httpclient.reset_mock() def raise_exception(*args, **kwargs): From cc10af76ce0ed73eee058c167c4d7e6f564a6dd2 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 22 Aug 2023 14:04:14 -0700 Subject: [PATCH 04/77] polish --- splitio/api/commons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/api/commons.py b/splitio/api/commons.py index 2b83fd02..9126c861 100644 --- a/splitio/api/commons.py +++ b/splitio/api/commons.py @@ -86,7 +86,7 @@ def change_number(self): @property def sets(self): - """Return change number.""" + """Return sets.""" return self._sets def __eq__(self, other): From 231281fc4095fe819d192c5bebe4c5fd657d3373 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 22 Aug 2023 14:15:26 -0700 Subject: [PATCH 05/77] added sorting sets in uri --- splitio/api/commons.py | 2 +- tests/api/test_splits_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/splitio/api/commons.py b/splitio/api/commons.py index 9126c861..6d7c90b7 100644 --- a/splitio/api/commons.py +++ b/splitio/api/commons.py @@ -125,5 +125,5 @@ def build_fetch(change_number, fetch_options, metadata): if fetch_options.change_number is not None: query['till'] = fetch_options.change_number if fetch_options.sets is not None: - query['sets'] = ','.join(fetch_options.sets) + query['sets'] = ','.join(sorted(fetch_options.sets)) return query, extra_headers \ No newline at end of file diff --git a/tests/api/test_splits_api.py b/tests/api/test_splits_api.py index 8caa55ae..b5f2086b 100644 --- a/tests/api/test_splits_api.py +++ b/tests/api/test_splits_api.py @@ -19,7 +19,7 @@ def test_fetch_split_changes(self, mocker): httpclient.get.return_value = client.HttpResponse(200, '{"prop1": "value1"}') split_api = splits.SplitsAPI(httpclient, 'some_api_key', SdkMetadata('1.0', 'some', '1.2.3.4'), mocker.Mock()) - response = split_api.fetch_splits(123, FetchOptions(False, None, ['set1', 'set2'])) + response = split_api.fetch_splits(123, FetchOptions(False, None, ['set2', 'set1'])) assert response['prop1'] == 'value1' assert httpclient.get.mock_calls == [mocker.call('sdk', '/splitChanges', 'some_api_key', extra_headers={ From ff3eeaa3cec7926a1b54280a9742cc07dabbad25 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 23 Aug 2023 08:27:55 -0700 Subject: [PATCH 06/77] updated model telemetry --- splitio/models/telemetry.py | 21 ++++++++++++++++++++- tests/models/test_telemetry_model.py | 8 +++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index e2976bd3..d64797d2 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -747,6 +747,7 @@ def _reset_all(self): self._http_proxy = None self._active_factory_count = 0 self._redundant_factory_count = 0 + self._flag_sets = 0 def record_config(self, config, extra_config): """ @@ -787,6 +788,15 @@ def record_active_and_redundant_factories(self, active_factory_count, redundant_ self._active_factory_count = active_factory_count self._redundant_factory_count = redundant_factory_count + def record_flag_sets(self, flag_sets): + """ + Record flag sets + + :param flag_sets: flag sets count + :type flag_sets: int + """ + with self._lock: + self._flag_sets = flag_sets def record_ready_time(self, ready_time): """ @@ -814,6 +824,14 @@ def record_not_ready_usage(self): with self._lock: self._not_ready += 1 + def get_flag_sets(self): + """ + Get flag sets + + """ + with self._lock: + return self._flag_sets + def get_bur_time_outs(self): """ Get block until ready timeout. @@ -865,7 +883,8 @@ def get_stats(self): 'iL': self._impression_listener, 'hp': self._http_proxy, 'aF': self._active_factory_count, - 'rF': self._redundant_factory_count + 'rF': self._redundant_factory_count, + 'fS': self._flag_sets } def _get_operation_mode(self, op_mode): diff --git a/tests/models/test_telemetry_model.py b/tests/models/test_telemetry_model.py index 8e6392fe..26e705a0 100644 --- a/tests/models/test_telemetry_model.py +++ b/tests/models/test_telemetry_model.py @@ -6,7 +6,6 @@ from splitio.models.telemetry import StorageType, OperationMode, MethodLatencies, MethodExceptions, \ HTTPLatencies, HTTPErrors, LastSynchronization, TelemetryCounters, TelemetryConfig, \ StreamingEvent, StreamingEvents, UpdateFromSSE - import splitio.models.telemetry as ModelTelemetry class TelemetryModelTests(object): @@ -88,7 +87,6 @@ def test_http_latencies(self, mocker): http_latencies = HTTPLatencies() for resource in ModelTelemetry.HTTPExceptionsAndLatencies: -# pytest.set_trace() if self._get_http_latency(resource, http_latencies) == None: continue http_latencies.add_latency(resource, 50) @@ -271,12 +269,16 @@ def test_telemetry_config(self): 'nR': 0, 'bT': 0, 'aF': 0, - 'rF': 0} + 'rF': 0, + 'fS': 0} ) telemetry_config.record_ready_time(10) assert(telemetry_config._time_until_ready == 10) + telemetry_config.record_flag_sets(5) + assert(telemetry_config._flag_sets == 5) + [telemetry_config.record_bur_time_out() for i in range(2)] assert(telemetry_config.get_bur_time_outs() == 2) From 4a675f1d50192bca3de1ad3d8ae750ae2bdfcb4f Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 23 Aug 2023 09:12:45 -0700 Subject: [PATCH 07/77] updated storage inmemory telemetry --- splitio/storage/inmemmory.py | 8 ++++++++ tests/storage/test_inmemory_storage.py | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 00dbb16b..3967bdf8 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -499,6 +499,10 @@ def record_ready_time(self, ready_time): """Record ready time.""" self._tel_config.record_ready_time(ready_time) + def record_flag_sets(self, flag_sets): + """Record flag sets.""" + self._tel_config.record_flag_sets(flag_sets) + def add_tag(self, tag): """Record tag string.""" with self._lock: @@ -567,6 +571,10 @@ def record_update_from_sse(self, event): """Record update from sse.""" self._counters.record_update_from_sse(event) + def get_flag_sets(self): + """Get flag sets.""" + self._tel_config.get_flag_sets() + def get_bur_time_outs(self): """Get block until ready timeout.""" return self._tel_config.get_bur_time_outs() diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 7319548d..dea130fd 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -467,7 +467,8 @@ def test_resets(self): 'iL': False, 'hp': None, 'aF': 0, - 'rF': 0 + 'rF': 0, + 'fS': 0 }) assert(storage._streaming_events.pop_streaming_events() == {'streamingEvents': []}) assert(storage._tags == []) @@ -492,6 +493,7 @@ def test_record_config(self): } storage.record_config(config, {}) storage.record_active_and_redundant_factories(1, 0) + storage.record_flag_sets(2) assert(storage._tel_config.get_stats() == {'oM': 0, 'sT': storage._tel_config._get_storage_type(config['operationMode'], config['storageType']), 'sE': config['streamingEnabled'], @@ -506,7 +508,8 @@ def test_record_config(self): 'tR': 0, 'nR': 0, 'aF': 1, - 'rF': 0} + 'rF': 0, + 'fS': 2} ) def test_record_counters(self): From a4d8ee81783591f2774b0eb04217c211439a6c72 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 23 Aug 2023 09:47:59 -0700 Subject: [PATCH 08/77] polish --- splitio/api/commons.py | 2 +- tests/api/test_splits_api.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/splitio/api/commons.py b/splitio/api/commons.py index 6d7c90b7..0766ae49 100644 --- a/splitio/api/commons.py +++ b/splitio/api/commons.py @@ -125,5 +125,5 @@ def build_fetch(change_number, fetch_options, metadata): if fetch_options.change_number is not None: query['till'] = fetch_options.change_number if fetch_options.sets is not None: - query['sets'] = ','.join(sorted(fetch_options.sets)) + query['sets'] = fetch_options.sets return query, extra_headers \ No newline at end of file diff --git a/tests/api/test_splits_api.py b/tests/api/test_splits_api.py index b5f2086b..e8d1784e 100644 --- a/tests/api/test_splits_api.py +++ b/tests/api/test_splits_api.py @@ -19,7 +19,7 @@ def test_fetch_split_changes(self, mocker): httpclient.get.return_value = client.HttpResponse(200, '{"prop1": "value1"}') split_api = splits.SplitsAPI(httpclient, 'some_api_key', SdkMetadata('1.0', 'some', '1.2.3.4'), mocker.Mock()) - response = split_api.fetch_splits(123, FetchOptions(False, None, ['set2', 'set1'])) + response = split_api.fetch_splits(123, FetchOptions(False, None, 'set1,set2')) assert response['prop1'] == 'value1' assert httpclient.get.mock_calls == [mocker.call('sdk', '/splitChanges', 'some_api_key', extra_headers={ @@ -42,7 +42,7 @@ def test_fetch_split_changes(self, mocker): query={'since': 123})] httpclient.reset_mock() - response = split_api.fetch_splits(123, FetchOptions(True, 123, ['set3'])) + response = split_api.fetch_splits(123, FetchOptions(True, 123, 'set3')) assert response['prop1'] == 'value1' assert httpclient.get.mock_calls == [mocker.call('sdk', '/splitChanges', 'some_api_key', extra_headers={ From 587cbe1396435d56c8a2e6125001d2c4ffa02731 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 24 Aug 2023 09:47:56 -0700 Subject: [PATCH 09/77] updated engine.telemetry class --- splitio/engine/telemetry.py | 4 ++++ tests/engine/test_telemetry.py | 43 +++++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/splitio/engine/telemetry.py b/splitio/engine/telemetry.py index f2ecf6f8..e1802131 100644 --- a/splitio/engine/telemetry.py +++ b/splitio/engine/telemetry.py @@ -48,6 +48,10 @@ def record_ready_time(self, ready_time): """Record ready time.""" self._telemetry_storage.record_ready_time(ready_time) + def record_flag_sets(self, flag_sets): + """Record flag sets.""" + self._telemetry_storage.record_flag_sets(flag_sets) + def record_bur_time_out(self): """Record block until ready timeout.""" self._telemetry_storage.record_bur_time_out() diff --git a/tests/engine/test_telemetry.py b/tests/engine/test_telemetry.py index 78466e87..57c60eae 100644 --- a/tests/engine/test_telemetry.py +++ b/tests/engine/test_telemetry.py @@ -20,15 +20,42 @@ def test_instances(self): assert(telemetry_producer._telemetry_runtime_producer == telemetry_producer.get_telemetry_runtime_producer()) def test_record_config(self, mocker): - telemetry_storage = mocker.Mock() + telemetry_storage = InMemoryTelemetryStorage() telemetry_init_producer = TelemetryInitProducer(telemetry_storage) - - def record_config(*args, **kwargs): - self.passed_config = args[0] - - telemetry_storage.record_config.side_effect = record_config - telemetry_init_producer.record_config({'bT':0, 'nR':0, 'uC': 0}, {}) - assert(self.passed_config == {'bT':0, 'nR':0, 'uC': 0}) + config = {'operationMode': 'standalone', + 'streamingEnabled': True, + 'impressionsQueueSize': 100, + 'eventsQueueSize': 200, + 'impressionsMode': 'DEBUG','' + 'impressionListener': None, + 'featuresRefreshRate': 30, + 'segmentsRefreshRate': 30, + 'impressionsRefreshRate': 60, + 'eventsPushRate': 60, + 'metricsRefreshRate': 10, + 'storageType': None + } + telemetry_init_producer.record_config(config, {}) + telemetry_init_producer.record_active_and_redundant_factories(1, 0) + telemetry_init_producer.record_flag_sets(2) + + assert(telemetry_storage._tel_config.get_stats() == {'oM': 0, + 'sT': telemetry_storage._tel_config._get_storage_type(config['operationMode'], config['storageType']), + 'sE': config['streamingEnabled'], + 'rR': {'sp': 30, 'se': 30, 'im': 60, 'ev': 60, 'te': 10}, + 'uO': {'s': False, 'e': False, 'a': False, 'st': False, 't': False}, + 'iQ': config['impressionsQueueSize'], + 'eQ': config['eventsQueueSize'], + 'iM': telemetry_storage._tel_config._get_impressions_mode(config['impressionsMode']), + 'iL': True if config['impressionListener'] is not None else False, + 'hp': telemetry_storage._tel_config._check_if_proxy_detected(), + 'bT': 0, + 'tR': 0, + 'nR': 0, + 'aF': 1, + 'rF': 0, + 'fS': 2} + ) def test_record_ready_time(self, mocker): telemetry_storage = mocker.Mock() From 9917aebd2e733441954045de771b88e2537f7a45 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 24 Aug 2023 14:21:07 -0700 Subject: [PATCH 10/77] updated storage inmemory split storage --- splitio/models/splits.py | 2 +- splitio/storage/inmemmory.py | 61 +++++++++++++++++++++----- tests/storage/test_inmemory_storage.py | 57 +++++++++++++++++++++++- 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/splitio/models/splits.py b/splitio/models/splits.py index 31f0fd0b..cf6a3c7b 100644 --- a/splitio/models/splits.py +++ b/splitio/models/splits.py @@ -42,7 +42,7 @@ def __init__( # pylint: disable=too-many-arguments traffic_allocation=None, traffic_allocation_seed=None, configurations=None, - sets=[] + sets=None ): """ Class constructor. diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 3967bdf8..28ba8e02 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -2,6 +2,7 @@ import logging import threading import queue +import bisect from collections import Counter from splitio.models.segments import Segment @@ -23,6 +24,7 @@ def __init__(self): self._splits = {} self._change_number = -1 self._traffic_types = Counter() + self._sets_feature_flag_map = {} def get(self, split_name): """ @@ -57,9 +59,15 @@ def put(self, split): """ with self._lock: if split.name in self._splits: + self._remove_flag_sets(self._splits[split.name]) self._decrease_traffic_type_count(self._splits[split.name].traffic_type_name) self._splits[split.name] = split self._increase_traffic_type_count(split.traffic_type_name) + if split.sets is not None: + for flag_set in split.sets: + if flag_set not in self._sets_feature_flag_map.keys(): + self._sets_feature_flag_map[flag_set] = set() + self._sets_feature_flag_map[flag_set].add(split.name) def remove(self, split_name): """ @@ -79,11 +87,40 @@ def remove(self, split_name): self._splits.pop(split_name) self._decrease_traffic_type_count(split.traffic_type_name) + self._remove_flag_sets(split) return True + def _remove_flag_sets(self, feature_flag): + """ + Remove flag sets associated to a split + + :param feature_flag: feature flag object + :type feature_flag: splitio.models.splits.Split + """ + if feature_flag.sets is not None: + for flag_set in feature_flag.sets: + self._sets_feature_flag_map[flag_set].remove(feature_flag.name) + if len(self._sets_feature_flag_map[flag_set]) == 0: + del self._sets_feature_flag_map[flag_set] + + def get_feature_flags_by_set(self, set): + """ + Get list of feature flag names associated to a set, if it does not exist will return empty list + + :param set: flag set + :type set: str + + :return: list of feature flag names + :rtype: list + """ + with self._lock: + if set not in self._sets_feature_flag_map: + return [] + return list(self._sets_feature_flag_map[set]) + def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ @@ -102,9 +139,9 @@ def set_change_number(self, new_change_number): def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ with self._lock: @@ -112,9 +149,9 @@ def get_split_names(self): def get_all_splits(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ with self._lock: @@ -122,7 +159,7 @@ def get_all_splits(self): def get_splits_count(self): """ - Return splits count. + Return feature flags count. :rtype: int """ @@ -131,7 +168,7 @@ def get_splits_count(self): def is_valid_traffic_type(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -142,12 +179,12 @@ def is_valid_traffic_type(self, traffic_type_name): with self._lock: return traffic_type_name in self._traffic_types - def kill_locally(self, split_name, default_treatment, change_number): + def kill_locally(self, feature_flag_name, default_treatment, change_number): """ - Local kill for split + Local kill for feature flag - :param split_name: name of the split to perform kill - :type split_name: str + :param feature_flag_name: name of the feature flag to perform kill + :type feature_flag_name: str :param default_treatment: name of the default treatment to return :type default_treatment: str :param change_number: change_number @@ -156,7 +193,7 @@ def kill_locally(self, split_name, default_treatment, change_number): with self._lock: if self.get_change_number() > change_number: return - split = self._splits.get(split_name) + split = self._splits.get(feature_flag_name) if not split: return split.local_kill(default_treatment, change_number) diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index dea130fd..5c0352ac 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -9,7 +9,6 @@ from splitio.models.events import Event, EventWrapper import splitio.models.telemetry as ModelTelemetry from splitio.engine.telemetry import TelemetryStorageProducer - from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage @@ -25,6 +24,9 @@ def test_storing_retrieving_splits(self, mocker): name_property = mocker.PropertyMock() name_property.return_value = 'some_split' type(split).name = name_property + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split).sets = sets_property storage.put(split) assert storage.get('some_split') == split @@ -45,6 +47,10 @@ def test_get_splits(self, mocker): name2_prop = mocker.PropertyMock() name2_prop.return_value = 'split2' type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property storage = InMemorySplitStorage() storage.put(split1) @@ -73,6 +79,10 @@ def test_get_split_names(self, mocker): name2_prop = mocker.PropertyMock() name2_prop.return_value = 'split2' type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property storage = InMemorySplitStorage() storage.put(split1) @@ -90,6 +100,10 @@ def test_get_all_splits(self, mocker): name2_prop = mocker.PropertyMock() name2_prop.return_value = 'split2' type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property storage = InMemorySplitStorage() storage.put(split1) @@ -120,6 +134,11 @@ def test_is_valid_traffic_type(self, mocker): type(split1).traffic_type_name = tt_user type(split2).traffic_type_name = tt_account type(split3).traffic_type_name = tt_user + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property + type(split3).sets = sets_property storage = InMemorySplitStorage() @@ -155,11 +174,14 @@ def test_traffic_type_inc_dec_logic(self, mocker): name1_prop = mocker.PropertyMock() name1_prop.return_value = 'split1' type(split1).name = name1_prop - split2 = mocker.Mock() name2_prop = mocker.PropertyMock() name2_prop.return_value = 'split1' type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = None + type(split1).sets = sets_property + type(split2).sets = sets_property tt_user = mocker.PropertyMock() tt_user.return_value = 'user' @@ -198,6 +220,37 @@ def test_kill_locally(self): storage.kill_locally('some_split', 'default_treatment', 3) assert storage.get('some_split').change_number == 3 + def test_flag_sets(self): + storage = InMemorySplitStorage() + assert storage._sets_feature_flag_map == {} + + split1 = Split('split1', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set10', 'set02']) + storage.put(split1) + assert storage.get_feature_flags_by_set('set10') == ['split1'] + assert storage.get_feature_flags_by_set('set02') == ['split1'] + + split2 = Split('split2', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set05', 'set02']) + storage.put(split2) + assert storage.get_feature_flags_by_set('set05') == ['split2'] + assert sorted(storage.get_feature_flags_by_set('set02')) == ['split1', 'split2'] + + storage.remove(split2.name) + assert 'set5' not in storage._sets_feature_flag_map + assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_set('set05') == [] + + split1 = Split('split1', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set02']) + storage.put(split1) + assert 'set10' not in storage._sets_feature_flag_map + assert storage.get_feature_flags_by_set('set02') == ['split1'] + + storage.remove(split1.name) + assert storage._sets_feature_flag_map == {} + assert storage.get_feature_flags_by_set('set02') == [] + class InMemorySegmentStorageTests(object): """In memory segment storage tests.""" From a7366a7d68f2d60aa82f90b0da662e1bbfad9c23 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 24 Aug 2023 14:25:32 -0700 Subject: [PATCH 11/77] polish --- splitio/storage/inmemmory.py | 1 - 1 file changed, 1 deletion(-) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 28ba8e02..694279c9 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -2,7 +2,6 @@ import logging import threading import queue -import bisect from collections import Counter from splitio.models.segments import Segment From 89788f5d43cc23620d53c1aa4a47d1543a1d6db4 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Mon, 28 Aug 2023 08:34:23 -0700 Subject: [PATCH 12/77] Update tests/engine/test_telemetry.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gastón Thea --- tests/engine/test_telemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/engine/test_telemetry.py b/tests/engine/test_telemetry.py index 57c60eae..79bcd744 100644 --- a/tests/engine/test_telemetry.py +++ b/tests/engine/test_telemetry.py @@ -26,7 +26,7 @@ def test_record_config(self, mocker): 'streamingEnabled': True, 'impressionsQueueSize': 100, 'eventsQueueSize': 200, - 'impressionsMode': 'DEBUG','' + 'impressionsMode': 'DEBUG', 'impressionListener': None, 'featuresRefreshRate': 30, 'segmentsRefreshRate': 30, From 89671a634c0589683c7a55ecfce2b83d92691caf Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 28 Aug 2023 12:32:03 -0700 Subject: [PATCH 13/77] Added "update" to split storage to replace put, remove and set_change_number --- splitio/storage/__init__.py | 35 ++++-------------- splitio/storage/inmemmory.py | 23 ++++++++++-- splitio/storage/pluggable.py | 31 +++++++++++----- splitio/storage/redis.py | 33 ++++------------- tests/storage/test_inmemory_storage.py | 51 +++++++++++--------------- 5 files changed, 79 insertions(+), 94 deletions(-) diff --git a/splitio/storage/__init__.py b/splitio/storage/__init__.py index 5467bc14..a701ac8e 100644 --- a/splitio/storage/__init__.py +++ b/splitio/storage/__init__.py @@ -30,25 +30,16 @@ def fetch_many(self, split_names): pass @abc.abstractmethod - def put(self, split): + def update(self, to_add, to_delete, new_change_number): """ - Store a split. - - :param split: Split object to store - :type split_name: splitio.models.splits.Split - """ - pass + Update feature flag strage. - @abc.abstractmethod - def remove(self, split_name): - """ - Remove a split from storage. - - :param split_name: Name of the feature to remove. - :type split_name: str - - :return: True if the split was found and removed. False otherwise. - :rtype: bool + :param to_add: List of feature flags to add + :type to_add: list[splitio.models.splits.Split] + :param to_delete: List of feature flags to delete + :type to_delete: list[splitio.models.splits.Split] + :param new_change_number: New change number. + :type new_change_number: int """ pass @@ -61,16 +52,6 @@ def get_change_number(self): """ pass - @abc.abstractmethod - 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 - """ - pass - @abc.abstractmethod def get_split_names(self): """ diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 694279c9..9c36ab60 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -49,7 +49,22 @@ def fetch_many(self, split_names): """ return {split_name: self.get(split_name) for split_name in split_names} - def put(self, split): + def update(self, to_add, to_delete, new_change_number): + """ + Update feature flag strage. + + :param to_add: List of feature flags to add + :type to_add: list[splitio.models.splits.Split] + :param to_delete: List of feature flags to delete + :type to_delete: list[str] + :param new_change_number: New change number. + :type new_change_number: int + """ + [self._put(add_split) for add_split in to_add] + [self._remove(delete_split) for delete_split in to_delete] + self._set_change_number(new_change_number) + + def _put(self, split): """ Store a split. @@ -68,7 +83,7 @@ def put(self, split): self._sets_feature_flag_map[flag_set] = set() self._sets_feature_flag_map[flag_set].add(split.name) - def remove(self, split_name): + def _remove(self, split_name): """ Remove a split from storage. @@ -126,7 +141,7 @@ def get_change_number(self): with self._lock: return self._change_number - def set_change_number(self, new_change_number): + def _set_change_number(self, new_change_number): """ Set the latest change number. @@ -196,7 +211,7 @@ def kill_locally(self, feature_flag_name, default_treatment, change_number): if not split: return split.local_kill(default_treatment, change_number) - self.put(split) + self._put(split) def _increase_traffic_type_count(self, traffic_type_name): """ diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index a15df284..85af5ae4 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -87,7 +87,21 @@ def fetch_many(self, split_names): # _LOGGER.error('Error storing splits in storage') # _LOGGER.debug('Error: ', exc_info=True) - def remove(self, split_name): + def update(self, to_add, to_delete, new_change_number): + """ + Update feature flag strage. + + :param to_add: List of feature flags to add + :type to_add: list[splitio.models.splits.Split] + :param to_delete: List of feature flags to delete + :type to_delete: list[splitio.models.splits.Split] + :param new_change_number: New change number. + :type new_change_number: int + """ + pass + + # TODO: To be added when producer mode is aupported +# def _remove(self, split_name): """ Remove a split from storage. @@ -97,8 +111,7 @@ def remove(self, split_name): :return: True if the split was found and removed. False otherwise. :rtype: bool """ - pass - # TODO: To be added when producer mode is aupported +# pass # try: # split = self.get(split_name) # if not split: @@ -125,15 +138,15 @@ def get_change_number(self): _LOGGER.debug('Error: ', exc_info=True) return None - def set_change_number(self, new_change_number): + # TODO: To be added when producer mode is aupported +# 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 """ - pass - # TODO: To be added when producer mode is aupported +# pass # try: # self._pluggable_adapter.set(self._split_till_prefix, new_change_number) # except Exception: @@ -280,15 +293,15 @@ def is_valid_traffic_type(self, traffic_type_name): _LOGGER.debug('Error: ', exc_info=True) return None - def put(self, split): + # TODO: To be added when producer mode is aupported +# def _put(self, split): """ Store a split. :param split: Split object. :type split: splitio.models.split.Split """ - pass - # TODO: To be added when producer mode is aupported +# pass # try: # existing_split = self.get(split.name) # self._pluggable_adapter.set(self._prefix.format(split_name=split.name), split.to_json()) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index d2aa2788..aa0e670c 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -128,24 +128,16 @@ def is_valid_traffic_type(self, traffic_type_name): # pylint: disable=method-hi _LOGGER.debug('Error: ', exc_info=True) return False - def put(self, split): + def update(self, to_add, to_delete, new_change_number): """ - Store a split. - - :param split: Split object to store - :type split_name: splitio.models.splits.Split - """ - raise NotImplementedError('Only redis-consumer mode is supported.') - - def remove(self, split_name): - """ - Remove a split from storage. - - :param split_name: Name of the feature to remove. - :type split_name: str + Update feature flag strage. - :return: True if the split was found and removed. False otherwise. - :rtype: bool + :param to_add: List of feature flags to add + :type to_add: list[splitio.models.splits.Split] + :param to_delete: List of feature flags to delete + :type to_delete: list[splitio.models.splits.Split] + :param new_change_number: New change number. + :type new_change_number: int """ raise NotImplementedError('Only redis-consumer mode is supported.') @@ -164,15 +156,6 @@ def get_change_number(self): _LOGGER.debug('Error: ', exc_info=True) return None - 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 - """ - raise NotImplementedError('Only redis-consumer mode is supported.') - def get_split_names(self): """ Retrieve a list of all split names. diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 5c0352ac..061159d4 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -28,13 +28,14 @@ def test_storing_retrieving_splits(self, mocker): sets_property.return_value = None type(split).sets = sets_property - storage.put(split) + storage.update([split], [], 0) + assert storage.get('some_split') == split assert storage.get_split_names() == ['some_split'] assert storage.get_all_splits() == [split] assert storage.get('nonexistant_split') is None - storage.remove('some_split') + storage.update([], ['some_split'], 0) assert storage.get('some_split') is None def test_get_splits(self, mocker): @@ -53,8 +54,7 @@ def test_get_splits(self, mocker): type(split2).sets = sets_property storage = InMemorySplitStorage() - storage.put(split1) - storage.put(split2) + storage.update([split1, split2], [], 0) splits = storage.fetch_many(['split1', 'split2', 'split3']) assert len(splits) == 3 @@ -66,7 +66,7 @@ def test_store_get_changenumber(self): """Test that storing and retrieving change numbers works.""" storage = InMemorySplitStorage() assert storage.get_change_number() == -1 - storage.set_change_number(5) + storage.update([], [], 5) assert storage.get_change_number() == 5 def test_get_split_names(self, mocker): @@ -85,8 +85,7 @@ def test_get_split_names(self, mocker): type(split2).sets = sets_property storage = InMemorySplitStorage() - storage.put(split1) - storage.put(split2) + storage.update([split1, split2], [], 0) assert set(storage.get_split_names()) == set(['split1', 'split2']) @@ -106,8 +105,7 @@ def test_get_all_splits(self, mocker): type(split2).sets = sets_property storage = InMemorySplitStorage() - storage.put(split1) - storage.put(split2) + storage.update([split1, split2], [], 0) all_splits = storage.get_all_splits() assert next(s for s in all_splits if s.name == 'split1') @@ -142,27 +140,23 @@ def test_is_valid_traffic_type(self, mocker): storage = InMemorySplitStorage() - storage.put(split1) + storage.update([split1], [], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is False - storage.put(split2) - assert storage.is_valid_traffic_type('user') is True - assert storage.is_valid_traffic_type('account') is True - - storage.put(split3) + storage.update([split2, split3], [], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is True - storage.remove('split1') + storage.update([], ['split1'], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is True - storage.remove('split2') + storage.update([], ['split2'], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is False - storage.remove('split3') + storage.update([], ['split3'], 0) assert storage.is_valid_traffic_type('user') is False assert storage.is_valid_traffic_type('account') is False @@ -192,11 +186,11 @@ def test_traffic_type_inc_dec_logic(self, mocker): type(split1).traffic_type_name = tt_user type(split2).traffic_type_name = tt_account - storage.put(split1) + storage.update([split1], [], 0) assert storage.is_valid_traffic_type('user') is True assert storage.is_valid_traffic_type('account') is False - storage.put(split2) + storage.update([split2], [], 0) assert storage.is_valid_traffic_type('user') is False assert storage.is_valid_traffic_type('account') is True @@ -206,8 +200,7 @@ def test_kill_locally(self): split = Split('some_split', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1) - storage.put(split) - storage.set_change_number(1) + storage.update([split], [], 1) storage.kill_locally('test', 'default_treatment', 2) assert storage.get('test') is None @@ -226,28 +219,28 @@ def test_flag_sets(self): split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set10', 'set02']) - storage.put(split1) + split2 = Split('split2', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set05', 'set02']) + storage.update([split1], [], 1) assert storage.get_feature_flags_by_set('set10') == ['split1'] assert storage.get_feature_flags_by_set('set02') == ['split1'] - split2 = Split('split2', 123456789, False, 'some', 'traffic_type', - 'ACTIVE', 1, sets=['set05', 'set02']) - storage.put(split2) + storage.update([split2], [], 1) assert storage.get_feature_flags_by_set('set05') == ['split2'] assert sorted(storage.get_feature_flags_by_set('set02')) == ['split1', 'split2'] - storage.remove(split2.name) + storage.update([], [split2.name], 1) assert 'set5' not in storage._sets_feature_flag_map assert storage.get_feature_flags_by_set('set02') == ['split1'] assert storage.get_feature_flags_by_set('set05') == [] split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set02']) - storage.put(split1) + storage.update([split1], [], 1) assert 'set10' not in storage._sets_feature_flag_map assert storage.get_feature_flags_by_set('set02') == ['split1'] - storage.remove(split1.name) + storage.update([], [split1.name], 1) assert storage._sets_feature_flag_map == {} assert storage.get_feature_flags_by_set('set02') == [] From be21ee2fa20e0eb622103c5f15b13fef4e53d9e0 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 28 Aug 2023 13:38:39 -0700 Subject: [PATCH 14/77] added flag set validations to config --- splitio/client/config.py | 37 ++++++++++++++++++++++++++++++++++--- tests/client/test_config.py | 21 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index 4531e40a..cd171319 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -1,13 +1,14 @@ """Default settings for the Split.IO SDK Python client.""" import os.path import logging +import re from splitio.engine.impressions import ImpressionsMode _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 - +_FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$' DEFAULT_CONFIG = { 'operationMode': 'standalone', @@ -58,10 +59,10 @@ 'dataSampling': DEFAULT_DATA_SAMPLING, 'storageWrapper': None, 'storagePrefix': None, - 'storageType': None + 'storageType': None, + 'FlagSets': None } - def _parse_operation_mode(sdk_key, config): """ Process incoming config to determine operation mode and storage type @@ -119,6 +120,34 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None): return mode, refresh_rate +def _sanitize_flag_sets(flag_sets): + """ + Check supplied flag sets list + + :param flag_set: list of flag sets + :type flag_set: list[str] + + :returns: Sanitized and sorted flag sets + :rtype: list[str] + """ + sanitized_flag_sets = set() + for flag_set in flag_sets: + if flag_set != flag_set.strip(): + _LOGGER.warning("SDK config: Flag Set name %s has extra whitespace, trimming" % (flag_set)) + flag_set = flag_set.strip() + + if flag_set != flag_set.lower(): + _LOGGER.warning("SDK config: Flag Set name %s should be all lowercase - converting string to lowercase" % (flag_set)) + flag_set = flag_set.lower() + + if re.search(_FLAG_SETS_REGEX, flag_set) is None or re.search(_FLAG_SETS_REGEX, flag_set).group() != flag_set: + _LOGGER.warning("SDK config: you passed %s, Flag Set must adhere to the regular expressions %s. This means a Flag Set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.", flag_set, _FLAG_SETS_REGEX, flag_set) + continue + + sanitized_flag_sets.add(flag_set.strip()) + + return sorted(list(sanitized_flag_sets)) + def sanitize(sdk_key, config): """ Look for inconsistencies or ill-formed configs and tune it accordingly. @@ -143,4 +172,6 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 + processed['FlagSets'] = _sanitize_flag_sets(processed['FlagSets']) if processed['FlagSets'] is not None else None + return processed diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 0d96b478..e9a1c284 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -1,5 +1,6 @@ """Configuration unit tests.""" # pylint: disable=protected-access,no-self-use,line-too-long +import pytest from splitio.client import config from splitio.engine.impressions.impressions import ImpressionsMode @@ -68,3 +69,23 @@ def test_sanitize(self): processed = config.sanitize('some', configs) assert processed['redisLocalCacheEnabled'] # check default is True + + def test_sanitize_flag_sets(self): + """Test sanitization for flag sets.""" + flag_sets = config._sanitize_flag_sets([' set1', 'set2 ', 'set3']) + assert flag_sets == ['set1', 'set2', 'set3'] + + flag_sets = config._sanitize_flag_sets(['1set', '_set2']) + assert flag_sets == ['1set'] + + flag_sets = config._sanitize_flag_sets(['Set1', 'SET2']) + assert flag_sets == ['set1', 'set2'] + + flag_sets = config._sanitize_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5']) + assert flag_sets == [] + + flag_sets = config._sanitize_flag_sets(['set4', 'set1', 'set3', 'set1']) + assert flag_sets == ['set1', 'set3', 'set4'] + + flag_sets = config._sanitize_flag_sets(['w' * 50, 's' * 51]) + assert flag_sets == ['w' * 50] From 3649ada36def9f0426a0258581f523efb86afe8b Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 28 Aug 2023 13:41:48 -0700 Subject: [PATCH 15/77] polish --- splitio/api/splits.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/splitio/api/splits.py b/splitio/api/splits.py index 8cb23cfc..78e15ef2 100644 --- a/splitio/api/splits.py +++ b/splitio/api/splits.py @@ -56,12 +56,11 @@ def fetch_splits(self, change_number, fetch_options): query=query, ) record_telemetry(response.status_code, get_current_epoch_time_ms() - start, HTTPExceptionsAndLatencies.SPLIT, self._telemetry_runtime_producer) - if response.status_code == 414: - _LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.') - if 200 <= response.status_code < 300: return json.loads(response.body) else: + if response.status_code == 414: + _LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.') raise APIException(response.body, response.status_code) except HttpClientException as exc: _LOGGER.error('Error fetching feature flags because an exception was raised by the HTTPClient') From 0bf33de77f7057de5c436f9102ffb3d163bb94bb Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Tue, 29 Aug 2023 08:07:25 -0700 Subject: [PATCH 16/77] Update splitio/storage/__init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gastón Thea --- splitio/storage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/storage/__init__.py b/splitio/storage/__init__.py index a701ac8e..4930a95e 100644 --- a/splitio/storage/__init__.py +++ b/splitio/storage/__init__.py @@ -32,7 +32,7 @@ def fetch_many(self, split_names): @abc.abstractmethod def update(self, to_add, to_delete, new_change_number): """ - Update feature flag strage. + Update feature flag storage. :param to_add: List of feature flags to add :type to_add: list[splitio.models.splits.Split] From 06de1c915fcc24741a58e4c7a18ac32c5b987209 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Tue, 29 Aug 2023 08:07:33 -0700 Subject: [PATCH 17/77] Update splitio/storage/inmemmory.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gastón Thea --- splitio/storage/inmemmory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 9c36ab60..746fdb61 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -51,7 +51,7 @@ def fetch_many(self, split_names): def update(self, to_add, to_delete, new_change_number): """ - Update feature flag strage. + Update feature flag storage. :param to_add: List of feature flags to add :type to_add: list[splitio.models.splits.Split] From ebb9e6838a161cf12fa3979067660db593ff6d86 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Tue, 29 Aug 2023 08:07:40 -0700 Subject: [PATCH 18/77] Update splitio/storage/pluggable.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gastón Thea --- splitio/storage/pluggable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 85af5ae4..2be0f6d3 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -89,7 +89,7 @@ def fetch_many(self, split_names): def update(self, to_add, to_delete, new_change_number): """ - Update feature flag strage. + Update feature flag storage. :param to_add: List of feature flags to add :type to_add: list[splitio.models.splits.Split] From 954465b0fbcd3a9dc17f7c194ed6d7b2a80d7af5 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Tue, 29 Aug 2023 08:07:45 -0700 Subject: [PATCH 19/77] Update splitio/storage/redis.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gastón Thea --- splitio/storage/redis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index aa0e670c..9433fdd4 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -130,7 +130,7 @@ def is_valid_traffic_type(self, traffic_type_name): # pylint: disable=method-hi def update(self, to_add, to_delete, new_change_number): """ - Update feature flag strage. + Update feature flag storage. :param to_add: List of feature flags to add :type to_add: list[splitio.models.splits.Split] From 85840f864bb661ac25fdad59cc21f756b189e048 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 29 Aug 2023 15:51:47 -0700 Subject: [PATCH 20/77] Added sync.split logic, updated storage.inmemory.split and sync.synchronizer classes --- splitio/storage/inmemmory.py | 29 +++- splitio/sync/split.py | 44 ++++-- splitio/sync/synchronizer.py | 8 +- tests/storage/test_inmemory_storage.py | 62 +++++++- tests/sync/test_splits_synchronizer.py | 203 ++++++++++++++++--------- tests/sync/test_synchronizer.py | 25 ++- 6 files changed, 270 insertions(+), 101 deletions(-) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 746fdb61..00a70ccd 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -17,13 +17,16 @@ class InMemorySplitStorage(SplitStorage): """InMemory implementation of a split storage.""" - def __init__(self): + def __init__(self, flag_sets=[]): """Constructor.""" self._lock = threading.RLock() self._splits = {} self._change_number = -1 self._traffic_types = Counter() self._sets_feature_flag_map = {} + self.config_flag_sets_used = len(flag_sets) + for flag_set in flag_sets: + self._sets_feature_flag_map[flag_set] = set() def get(self, split_name): """ @@ -73,12 +76,14 @@ def _put(self, split): """ with self._lock: if split.name in self._splits: - self._remove_flag_sets(self._splits[split.name]) + self._remove_from_flag_sets(self._splits[split.name]) self._decrease_traffic_type_count(self._splits[split.name].traffic_type_name) self._splits[split.name] = split self._increase_traffic_type_count(split.traffic_type_name) if split.sets is not None: for flag_set in split.sets: + if flag_set not in self._sets_feature_flag_map.keys() and self.config_flag_sets_used > 0: + continue if flag_set not in self._sets_feature_flag_map.keys(): self._sets_feature_flag_map[flag_set] = set() self._sets_feature_flag_map[flag_set].add(split.name) @@ -101,10 +106,10 @@ def _remove(self, split_name): self._splits.pop(split_name) self._decrease_traffic_type_count(split.traffic_type_name) - self._remove_flag_sets(split) + self._remove_from_flag_sets(split) return True - def _remove_flag_sets(self, feature_flag): + def _remove_from_flag_sets(self, feature_flag): """ Remove flag sets associated to a split @@ -114,7 +119,7 @@ def _remove_flag_sets(self, feature_flag): if feature_flag.sets is not None: for flag_set in feature_flag.sets: self._sets_feature_flag_map[flag_set].remove(feature_flag.name) - if len(self._sets_feature_flag_map[flag_set]) == 0: + if len(self._sets_feature_flag_map[flag_set]) == 0 and self.config_flag_sets_used == 0: del self._sets_feature_flag_map[flag_set] def get_feature_flags_by_set(self, set): @@ -232,6 +237,20 @@ def _decrease_traffic_type_count(self, traffic_type_name): self._traffic_types.subtract([traffic_type_name]) self._traffic_types += Counter() + def is_flag_set_exist(self, flag_set): + """ + Return whether a flag set exists in at least one feature flag in cache. + + :param flag_set: Flag set to validate. + :type flag_set: str + + :return: True if the flag_set exist. False otherwise. + :rtype: bool + """ + if flag_set in self._sets_feature_flag_map.keys(): + return True + return False + class InMemorySegmentStorage(SegmentStorage): """In-memory implementation of a segment storage.""" diff --git a/splitio/sync/split.py b/splitio/sync/split.py index a39f42d1..c904d9d1 100644 --- a/splitio/sync/split.py +++ b/splitio/sync/split.py @@ -80,17 +80,38 @@ def _fetch_until(self, fetch_options, till=None): _LOGGER.debug('Exception information: ', exc_info=True) raise exc + to_add = [] + to_delete = [] for feature_flag in feature_flag_changes.get('splits', []): - if feature_flag['status'] == splits.Status.ACTIVE.value: + if (self._feature_flag_storage.config_flag_sets_used == 0 and feature_flag['status'] == splits.Status.ACTIVE.value) or \ + (feature_flag['status'] == splits.Status.ACTIVE.value and self._check_flag_sets(feature_flag)): parsed = splits.from_raw(feature_flag) - self._feature_flag_storage.put(parsed) + to_add.append(parsed) segment_list.update(set(parsed.get_segment_names())) else: - self._feature_flag_storage.remove(feature_flag['name']) - self._feature_flag_storage.set_change_number(feature_flag_changes['till']) + if self._feature_flag_storage.get(feature_flag['name']) is not None: + to_delete.append(feature_flag['name']) + + self._feature_flag_storage.update(to_add, to_delete, feature_flag_changes['till']) if feature_flag_changes['till'] == feature_flag_changes['since']: return feature_flag_changes['till'], segment_list + def _check_flag_sets(self, feature_flag): + """ + Check all flag sets in a feature flag, return True if any of sets exist in storage + + :param feature_flag: Flag set to validate. + :type feature_flag: json + + :return: True if any of its flag_set exist. False otherwise. + :rtype: bool + """ + for flag_set in feature_flag['sets']: + if self._feature_flag_storage.is_flag_set_exist(flag_set): + return True + return False + + def _attempt_feature_flag_sync(self, fetch_options, till=None): """ Hit endpoint, update storage and return True if sync is complete. @@ -347,11 +368,10 @@ def _synchronize_legacy(self): fetched = self._read_feature_flags_from_legacy_file(self._filename) to_delete = [name for name in self._feature_flag_storage.get_split_names() if name not in fetched.keys()] - for feature_flag in fetched.values(): - self._feature_flag_storage.put(feature_flag) + to_add = [] + [to_add.append(feature_flag) for feature_flag in fetched.values()] - for feature_flag in to_delete: - self._feature_flag_storage.remove(feature_flag) + self._feature_flag_storage.update(to_add, to_delete, 0) return [] @@ -371,16 +391,18 @@ def _synchronize_json(self): self._current_json_sha = fecthed_sha if self._feature_flag_storage.get_change_number() > till and till != self._DEFAULT_FEATURE_FLAG_TILL: return [] + to_add = [] + to_delete = [] for feature_flag in fetched: if feature_flag['status'] == splits.Status.ACTIVE.value: parsed = splits.from_raw(feature_flag) - self._feature_flag_storage.put(parsed) + to_add.append(parsed) _LOGGER.debug("feature flag %s is updated", parsed.name) segment_list.update(set(parsed.get_segment_names())) else: - self._feature_flag_storage.remove(feature_flag['name']) + to_delete.append(feature_flag['name']) - self._feature_flag_storage.set_change_number(till) + self._feature_flag_storage.update(to_add, to_delete, till) return segment_list except Exception as exc: raise ValueError("Error reading feature flags from json.") from exc diff --git a/splitio/sync/synchronizer.py b/splitio/sync/synchronizer.py index 3b5a4251..59c57f01 100644 --- a/splitio/sync/synchronizer.py +++ b/splitio/sync/synchronizer.py @@ -251,6 +251,7 @@ def __init__(self, split_synchronizers, split_tasks): self._periodic_data_recording_tasks.append(self._split_tasks.unique_keys_task) if self._split_tasks.clear_filter_task: self._periodic_data_recording_tasks.append(self._split_tasks.clear_filter_task) + self._break_sync_all = False @property def split_sync(self): @@ -289,6 +290,7 @@ def synchronize_splits(self, till, sync_segments=True): :returns: whether the synchronization was successful or not. :rtype: bool """ + self._break_sync_all = False _LOGGER.debug('Starting feature flags synchronization') try: new_segments = [] @@ -304,7 +306,9 @@ def synchronize_splits(self, till, sync_segments=True): else: _LOGGER.debug('Segment sync scheduled.') return True - except APIException: + except APIException as exc: + if exc._status_code is not None and exc._status_code == 414: + self._break_sync_all = True _LOGGER.error('Failed syncing feature flags') _LOGGER.debug('Error: ', exc_info=True) return False @@ -334,7 +338,7 @@ def sync_all(self, max_retry_attempts=_SYNC_ALL_NO_RETRIES): _LOGGER.debug('Error: ', exc_info=True) if max_retry_attempts != _SYNC_ALL_NO_RETRIES: retry_attempts += 1 - if retry_attempts > max_retry_attempts: + if retry_attempts > max_retry_attempts or self._break_sync_all: break how_long = self._backoff.get() time.sleep(how_long) diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 061159d4..a0e7fff3 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -213,36 +213,88 @@ def test_kill_locally(self): storage.kill_locally('some_split', 'default_treatment', 3) assert storage.get('some_split').change_number == 3 - def test_flag_sets(self): - storage = InMemorySplitStorage() - assert storage._sets_feature_flag_map == {} + def test_flag_sets_with_config_sets(self): + storage = InMemorySplitStorage(['set10', 'set02', 'set05']) + assert storage.config_flag_sets_used == 3 + assert storage._sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set10', 'set02']) split2 = Split('split2', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set05', 'set02']) + split3 = Split('split3', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set04', 'set05']) storage.update([split1], [], 1) assert storage.get_feature_flags_by_set('set10') == ['split1'] assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.is_flag_set_exist('set10') + assert storage.is_flag_set_exist('set02') + assert not storage.is_flag_set_exist('set03') storage.update([split2], [], 1) assert storage.get_feature_flags_by_set('set05') == ['split2'] assert sorted(storage.get_feature_flags_by_set('set02')) == ['split1', 'split2'] + assert storage.is_flag_set_exist('set05') storage.update([], [split2.name], 1) - assert 'set5' not in storage._sets_feature_flag_map + assert storage.is_flag_set_exist('set05') assert storage.get_feature_flags_by_set('set02') == ['split1'] assert storage.get_feature_flags_by_set('set05') == [] split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set02']) storage.update([split1], [], 1) - assert 'set10' not in storage._sets_feature_flag_map + assert storage.is_flag_set_exist('set10') assert storage.get_feature_flags_by_set('set02') == ['split1'] storage.update([], [split1.name], 1) + assert storage.get_feature_flags_by_set('set02') == [] + assert storage._sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} + + storage.update([split3], [], 1) + assert storage.get_feature_flags_by_set('set05') == ['split3'] + assert not storage.is_flag_set_exist('set04') + + def test_flag_sets_withut_config_sets(self): + storage = InMemorySplitStorage() assert storage._sets_feature_flag_map == {} + assert storage.config_flag_sets_used == 0 + + split1 = Split('split1', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set10', 'set02']) + split2 = Split('split2', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set05', 'set02']) + split3 = Split('split3', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set04', 'set05']) + storage.update([split1], [], 1) + assert storage.get_feature_flags_by_set('set10') == ['split1'] + assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.is_flag_set_exist('set10') + assert storage.is_flag_set_exist('set02') + assert not storage.is_flag_set_exist('set03') + + storage.update([split2], [], 1) + assert storage.get_feature_flags_by_set('set05') == ['split2'] + assert sorted(storage.get_feature_flags_by_set('set02')) == ['split1', 'split2'] + assert storage.is_flag_set_exist('set05') + + storage.update([], [split2.name], 1) + assert not storage.is_flag_set_exist('set05') + assert storage.get_feature_flags_by_set('set02') == ['split1'] + + split1 = Split('split1', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1, sets=['set02']) + storage.update([split1], [], 1) + assert not storage.is_flag_set_exist('set10') + assert storage.get_feature_flags_by_set('set02') == ['split1'] + + storage.update([], [split1.name], 1) assert storage.get_feature_flags_by_set('set02') == [] + assert storage._sets_feature_flag_map == {} + + storage.update([split3], [], 1) + assert storage.get_feature_flags_by_set('set05') == ['split3'] + assert storage.get_feature_flags_by_set('set04') == ['split3'] class InMemorySegmentStorageTests(object): diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index 9799ba4d..69df2bec 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -13,12 +13,49 @@ from splitio.sync.split import SplitSynchronizer, LocalSplitSynchronizer, LocalhostMode from tests.integration import splits_json +splits = [{ + 'changeNumber': 123, + 'trafficTypeName': 'user', + 'name': 'some_name', + 'trafficAllocation': 100, + 'trafficAllocationSeed': 123456, + 'seed': 321654, + 'status': 'ACTIVE', + 'killed': False, + 'defaultTreatment': 'off', + 'algo': 2, + 'conditions': [ + { + 'partitions': [ + {'treatment': 'on', 'size': 50}, + {'treatment': 'off', 'size': 50} + ], + 'contitionType': 'WHITELIST', + 'label': 'some_label', + 'matcherGroup': { + 'matchers': [ + { + 'matcherType': 'WHITELIST', + 'whitelistMatcherData': { + 'whitelist': ['k1', 'k2', 'k3'] + }, + 'negate': False, + } + ], + 'combiner': 'AND' + } + } + ], + 'sets': ['set1', 'set2'] +}] + + class SplitsSynchronizerTests(object): """Split synchronizer test cases.""" def test_synchronize_splits_error(self, mocker): """Test that if fetching splits fails at some_point, the task will continue running.""" - storage = mocker.Mock(spec=SplitStorage) + storage = mocker.Mock(spec=InMemorySplitStorage) api = mocker.Mock() def run(x, c): @@ -34,7 +71,7 @@ def run(x, c): def test_synchronize_splits(self, mocker): """Test split sync.""" - storage = mocker.Mock(spec=SplitStorage) + storage = mocker.Mock(spec=InMemorySplitStorage) def change_number_mock(): change_number_mock._calls += 1 @@ -43,43 +80,9 @@ def change_number_mock(): return 123 change_number_mock._calls = 0 storage.get_change_number.side_effect = change_number_mock + storage.config_flag_sets_used = 0 api = mocker.Mock() - splits = [{ - 'changeNumber': 123, - 'trafficTypeName': 'user', - 'name': 'some_name', - 'trafficAllocation': 100, - 'trafficAllocationSeed': 123456, - 'seed': 321654, - 'status': 'ACTIVE', - 'killed': False, - 'defaultTreatment': 'off', - 'algo': 2, - 'conditions': [ - { - 'partitions': [ - {'treatment': 'on', 'size': 50}, - {'treatment': 'off', 'size': 50} - ], - 'contitionType': 'WHITELIST', - 'label': 'some_label', - 'matcherGroup': { - 'matchers': [ - { - 'matcherType': 'WHITELIST', - 'whitelistMatcherData': { - 'whitelist': ['k1', 'k2', 'k3'] - }, - 'negate': False, - } - ], - 'combiner': 'AND' - } - } - ] - }] - def get_changes(*args, **kwargs): get_changes.called += 1 @@ -104,13 +107,13 @@ def get_changes(*args, **kwargs): assert mocker.call(-1, FetchOptions(True)) in api.fetch_splits.mock_calls assert mocker.call(123, FetchOptions(True)) in api.fetch_splits.mock_calls - inserted_split = storage.put.mock_calls[0][1][0] + inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' def test_not_called_on_till(self, mocker): """Test that sync is not called when till is less than previous changenumber""" - storage = mocker.Mock(spec=SplitStorage) + storage = mocker.Mock(spec=InMemorySplitStorage) def change_number_mock(): return 2 @@ -134,7 +137,7 @@ def test_synchronize_splits_cdn(self, mocker): """Test split sync with bypassing cdn.""" mocker.patch('splitio.sync.split._ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES', new=3) - storage = mocker.Mock(spec=SplitStorage) + storage = mocker.Mock(spec=InMemorySplitStorage) def change_number_mock(): change_number_mock._calls += 1 @@ -149,41 +152,6 @@ def change_number_mock(): storage.get_change_number.side_effect = change_number_mock api = mocker.Mock() - splits = [{ - 'changeNumber': 123, - 'trafficTypeName': 'user', - 'name': 'some_name', - 'trafficAllocation': 100, - 'trafficAllocationSeed': 123456, - 'seed': 321654, - 'status': 'ACTIVE', - 'killed': False, - 'defaultTreatment': 'off', - 'algo': 2, - 'conditions': [ - { - 'partitions': [ - {'treatment': 'on', 'size': 50}, - {'treatment': 'off', 'size': 50} - ], - 'contitionType': 'WHITELIST', - 'label': 'some_label', - 'matcherGroup': { - 'matchers': [ - { - 'matcherType': 'WHITELIST', - 'whitelistMatcherData': { - 'whitelist': ['k1', 'k2', 'k3'] - }, - 'negate': False, - } - ], - 'combiner': 'AND' - } - } - ] - }] - def get_changes(*args, **kwargs): get_changes.called += 1 if get_changes.called == 1: @@ -199,6 +167,7 @@ def get_changes(*args, **kwargs): return { 'splits': [], 'since': 12345, 'till': 12345 } get_changes.called = 0 api.fetch_splits.side_effect = get_changes + storage.config_flag_sets_used = 0 split_synchronizer = SplitSynchronizer(api, storage) split_synchronizer._backoff = Backoff(1, 1) @@ -212,10 +181,92 @@ def get_changes(*args, **kwargs): assert mocker.call(12345, FetchOptions(True, 1234)) in api.fetch_splits.mock_calls assert len(api.fetch_splits.mock_calls) == 8 # 2 ok + BACKOFF(2 since==till + 2 re-attempts) + CDN(2 since==till) - inserted_split = storage.put.mock_calls[0][1][0] + inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' + def test_sync_flag_sets_with_config_sets(self, mocker): + """Test split sync with flag sets.""" + storage = InMemorySplitStorage(['set1', 'set2']) + + split = splits[0].copy() + split['name'] = 'second' + splits1 = [splits[0].copy(), split] + splits2 = splits.copy() + splits3 = splits.copy() + splits4 = splits.copy() + api = mocker.Mock() + def get_changes(*args, **kwargs): + get_changes.called += 1 + if get_changes.called == 1: + return { 'splits': splits1, 'since': 123, 'till': 123 } + elif get_changes.called == 2: + splits2[0]['sets'] = ['set3'] + return { 'splits': splits2, 'since': 124, 'till': 124 } + elif get_changes.called == 3: + splits3[0]['sets'] = ['set1'] + return { 'splits': splits3, 'since': 12434, 'till': 12434 } + splits4[0]['sets'] = ['set6'] + splits4[0]['name'] = 'new_split' + return { 'splits': splits4, 'since': 12438, 'till': 12438 } + get_changes.called = 0 + api.fetch_splits.side_effect = get_changes + + split_synchronizer = SplitSynchronizer(api, storage) + split_synchronizer._backoff = Backoff(1, 1) + split_synchronizer.synchronize_splits() + assert isinstance(storage.get('some_name'), Split) + + split_synchronizer.synchronize_splits(124) + assert storage.get('some_name') == None + + split_synchronizer.synchronize_splits(12434) + assert isinstance(storage.get('some_name'), Split) + + split_synchronizer.synchronize_splits(12438) + assert storage.get('new_name') == None + + def test_sync_flag_sets_without_config_sets(self, mocker): + """Test split sync with flag sets.""" + storage = InMemorySplitStorage() + + split = splits[0].copy() + split['name'] = 'second' + splits1 = [splits[0].copy(), split] + splits2 = splits.copy() + splits3 = splits.copy() + splits4 = splits.copy() + api = mocker.Mock() + def get_changes(*args, **kwargs): + get_changes.called += 1 + if get_changes.called == 1: + return { 'splits': splits1, 'since': 123, 'till': 123 } + elif get_changes.called == 2: + splits2[0]['sets'] = ['set3'] + return { 'splits': splits2, 'since': 124, 'till': 124 } + elif get_changes.called == 3: + splits3[0]['sets'] = ['set1'] + return { 'splits': splits3, 'since': 12434, 'till': 12434 } + splits4[0]['sets'] = ['set6'] + splits4[0]['name'] = 'third_split' + return { 'splits': splits4, 'since': 12438, 'till': 12438 } + get_changes.called = 0 + api.fetch_splits.side_effect = get_changes + + split_synchronizer = SplitSynchronizer(api, storage) + split_synchronizer._backoff = Backoff(1, 1) + split_synchronizer.synchronize_splits() + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(124) + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(12434) + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(12438) + assert isinstance(storage.get('third_split'), Split) + class LocalSplitsSynchronizerTests(object): """Split synchronizer test cases.""" diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index c57c9453..70c61ff2 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -2,7 +2,7 @@ from turtle import clear import unittest.mock as mock - +import pytest from splitio.sync.synchronizer import Synchronizer, SplitTasks, SplitSynchronizers, LocalhostSynchronizer from splitio.tasks.split_sync import SplitSynchronizationTask from splitio.tasks.unique_keys_sync import UniqueKeysSyncTask, ClearFilterSyncTask @@ -38,6 +38,26 @@ def run(x, c): # test forcing to have only one retry attempt and then exit sychronizer.sync_all(1) # sync_all should not throw! + def test_sync_all_failed_splits_with_flagsets(self, mocker): + api = mocker.Mock() + storage = mocker.Mock() + + def run(x, c): + raise APIException("something broke", 414) + api.fetch_splits.side_effect = run + + split_sync = SplitSynchronizer(api, storage) + split_synchronizers = SplitSynchronizers(split_sync, mocker.Mock(), mocker.Mock(), + mocker.Mock(), mocker.Mock()) + synchronizer = Synchronizer(split_synchronizers, mocker.Mock(spec=SplitTasks)) + + synchronizer.synchronize_splits(None) # APIExceptions are handled locally and should not be propagated! + + # test forcing to have only one retry attempt and then exit + synchronizer.sync_all(3) # sync_all should not throw! + assert synchronizer._break_sync_all + assert synchronizer._backoff._attempt == 0 + def test_sync_all_failed_segments(self, mocker): api = mocker.Mock() storage = mocker.Mock() @@ -141,6 +161,7 @@ def test_sync_all(self, mocker): split_storage = mocker.Mock(spec=SplitStorage) split_storage.get_change_number.return_value = 123 split_storage.get_segment_names.return_value = ['segmentA'] + split_storage.config_flag_sets_used = 0 split_api = mocker.Mock() split_api.fetch_splits.return_value = {'splits': self.splits, 'since': 123, 'till': 123} @@ -159,7 +180,7 @@ def test_sync_all(self, mocker): synchronizer = Synchronizer(split_synchronizers, mocker.Mock(spec=SplitTasks)) synchronizer.sync_all() - inserted_split = split_storage.put.mock_calls[0][1][0] + inserted_split = split_storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' From 8bccfc920fe8136ee3d07e9111e269aac422b847 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 29 Aug 2023 16:01:15 -0700 Subject: [PATCH 21/77] polish --- splitio/storage/inmemmory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 00a70ccd..b8a621a6 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -82,9 +82,9 @@ def _put(self, split): self._increase_traffic_type_count(split.traffic_type_name) if split.sets is not None: for flag_set in split.sets: - if flag_set not in self._sets_feature_flag_map.keys() and self.config_flag_sets_used > 0: - continue if flag_set not in self._sets_feature_flag_map.keys(): + if self.config_flag_sets_used > 0: + continue self._sets_feature_flag_map[flag_set] = set() self._sets_feature_flag_map[flag_set].add(split.name) From d53bda2b85250597737e5b618e5b2f0e9f4399b5 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 30 Aug 2023 11:41:35 -0700 Subject: [PATCH 22/77] Added flagset support in client, updated client.config and models.telemetry --- splitio/client/client.py | 128 ++++++++- splitio/client/config.py | 9 +- splitio/models/telemetry.py | 56 +++- tests/client/test_client.py | 389 ++++++++++++++++++++++++++- tests/client/test_config.py | 18 +- tests/models/test_telemetry_model.py | 49 +++- 6 files changed, 624 insertions(+), 25 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 91e88447..e84df605 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -6,7 +6,7 @@ from splitio.models.impressions import Impression, Label from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies -from splitio.client import input_validator +from splitio.client import input_validator, config from splitio.util.time import get_current_epoch_time_ms, utctime_ms _LOGGER = logging.getLogger(__name__) @@ -309,6 +309,132 @@ def get_treatments(self, key, feature_flags, attributes=None): MethodExceptionsAndLatencies.TREATMENTS) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} + def get_treatments_by_flag_set(self, key, flag_set, attributes=None): + """ + Get treatments for feature flags that contain given flag set. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_set: flag set + :type flag_sets: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: The treatment for the key and feature flag + :rtype: str + """ + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes) + + def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): + """ + Get treatments for feature flags that contain given flag sets. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_sets: list of flag sets + :type flag_sets: list + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: The treatment for the key and feature flag + :rtype: dict + """ + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes) + + def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None): + """ + Get treatments for feature flags that contain given flag set. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_set: flag set + :type flag_sets: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: The treatment for the key and feature flag + :rtype: str + """ + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes) + + def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None): + """ + Get treatments for feature flags that contain given flag set. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_set: flag set + :type flag_sets: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: The treatment for the key and feature flag + :rtype: str + """ + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes) + + def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): + """ + Get treatments for feature flags that contain given flag sets. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param flag_sets: list of flag sets + :type flag_sets: list + :param method: Treatment by flag set method flavor + :type method: splitio.models.telemetry.MethodExceptionsAndLatencies + :param attributes: An optional dictionary of attributes + :type attributes: dict + + :return: The treatment for the key and feature flag + :rtype: str + """ + feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets) + if feature_flags_names == []: + _LOGGER.warning("No valid Flag set or no feature flags found for evaluating treatments") + return {} + + if 'config' in method.value: + return self._make_evaluations(key, feature_flags_names, attributes, method.value, + method) + + with_config = self._make_evaluations(key, feature_flags_names, attributes, method.value, + method) + return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} + + + def _get_feature_flag_names_by_flag_sets(self, flag_sets): + """ + Sanitize given flag sets and return list of feature flag names associated with them + + :param flag_sets: list of flag sets + :type flag_sets: list + + :return: list of feature flag names + :rtype: list + """ + sanitized_flag_sets = config.sanitize_flag_sets(flag_sets) + feature_flags = [] + [feature_flags.extend(self._split_storage.get_feature_flags_by_set(flag_set)) for flag_set in sanitized_flag_sets] + feature_flags_names = [] + [feature_flags_names.append(feature_flag) for feature_flag in feature_flags] + return feature_flags_names + def _build_impression( # pylint: disable=too-many-arguments self, matching_key, diff --git a/splitio/client/config.py b/splitio/client/config.py index cd171319..aa5c391b 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -120,7 +120,7 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None): return mode, refresh_rate -def _sanitize_flag_sets(flag_sets): +def sanitize_flag_sets(flag_sets): """ Check supplied flag sets list @@ -130,8 +130,15 @@ def _sanitize_flag_sets(flag_sets): :returns: Sanitized and sorted flag sets :rtype: list[str] """ + if not isinstance(flag_sets, list): + _LOGGER.warning("SDK config: FlagSets config parameters type should be list object, parameter is discarded") + return [] + sanitized_flag_sets = set() for flag_set in flag_sets: + if not isinstance(flag_set, str): + _LOGGER.warning("SDK config: Flag Set name %s should be str object, this flag set is discarded" % (flag_set)) + continue if flag_set != flag_set.strip(): _LOGGER.warning("SDK config: Flag Set name %s has extra whitespace, trimming" % (flag_set)) flag_set = flag_set.strip() diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index d64797d2..c06a2bbd 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -82,6 +82,10 @@ class MethodExceptionsAndLatencies(Enum): TREATMENTS = 'treatments' TREATMENT_WITH_CONFIG = 'treatment_with_config' TREATMENTS_WITH_CONFIG = 'treatments_with_config' + TREATMENTS_BY_FLAG_SET = 'treatments_by_flag_set' + TREATMENTS_BY_FLAG_SETS = 'treatments_by_flag_sets' + TREATMENTS_WITH_CONFIG_BY_FLAG_SET = 'treatments_with_config_by_flag_set' + TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 'treatments_with_config_by_flag_sets' TRACK = 'track' class LastSynchronizationConstants(Enum): @@ -166,6 +170,10 @@ def _reset_all(self): self._treatments = [0] * MAX_LATENCY_BUCKET_COUNT self._treatment_with_config = [0] * MAX_LATENCY_BUCKET_COUNT self._treatments_with_config = [0] * MAX_LATENCY_BUCKET_COUNT + self._treatments_by_flag_set = [0] * MAX_LATENCY_BUCKET_COUNT + self._treatments_by_flag_sets = [0] * MAX_LATENCY_BUCKET_COUNT + self._treatments_with_config_by_flag_set = [0] * MAX_LATENCY_BUCKET_COUNT + self._treatments_with_config_by_flag_sets = [0] * MAX_LATENCY_BUCKET_COUNT self._track = [0] * MAX_LATENCY_BUCKET_COUNT def add_latency(self, method, latency): @@ -187,6 +195,14 @@ def add_latency(self, method, latency): self._treatment_with_config[latency_bucket] += 1 elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG: self._treatments_with_config[latency_bucket] += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET: + self._treatments_by_flag_set[latency_bucket] += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS: + self._treatments_by_flag_sets[latency_bucket] += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET: + self._treatments_with_config_by_flag_set[latency_bucket] += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS: + self._treatments_with_config_by_flag_sets[latency_bucket] += 1 elif method == MethodExceptionsAndLatencies.TRACK: self._track[latency_bucket] += 1 else: @@ -200,10 +216,18 @@ def pop_all(self): :rtype: dict """ with self._lock: - latencies = {MethodExceptionsAndLatencies.METHOD_LATENCIES.value: {MethodExceptionsAndLatencies.TREATMENT.value: self._treatment, MethodExceptionsAndLatencies.TREATMENTS.value: self._treatments, - MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG.value: self._treatment_with_config, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG.value: self._treatments_with_config, - MethodExceptionsAndLatencies.TRACK.value: self._track} + latencies = {MethodExceptionsAndLatencies.METHOD_LATENCIES.value: { + MethodExceptionsAndLatencies.TREATMENT.value: self._treatment, + MethodExceptionsAndLatencies.TREATMENTS.value: self._treatments, + MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG.value: self._treatment_with_config, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG.value: self._treatments_with_config, + MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET.value: self._treatments_by_flag_set, + MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS.value: self._treatments_by_flag_sets, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET.value: self._treatments_with_config_by_flag_set, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS.value: self._treatments_with_config_by_flag_sets, + MethodExceptionsAndLatencies.TRACK.value: self._track } + } self._reset_all() return latencies @@ -288,6 +312,10 @@ def _reset_all(self): self._treatments = 0 self._treatment_with_config = 0 self._treatments_with_config = 0 + self._treatments_by_flag_set = 0 + self._treatments_by_flag_sets = 0 + self._treatments_with_config_by_flag_set = 0 + self._treatments_with_config_by_flag_sets = 0 self._track = 0 def add_exception(self, method): @@ -306,6 +334,14 @@ def add_exception(self, method): self._treatment_with_config += 1 elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG: self._treatments_with_config += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET: + self._treatments_by_flag_set += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS: + self._treatments_by_flag_sets += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET: + self._treatments_with_config_by_flag_set += 1 + elif method == MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS: + self._treatments_with_config_by_flag_sets += 1 elif method == MethodExceptionsAndLatencies.TRACK: self._track += 1 else: @@ -319,10 +355,18 @@ def pop_all(self): :rtype: dict """ with self._lock: - exceptions = {MethodExceptionsAndLatencies.METHOD_EXCEPTIONS.value: {MethodExceptionsAndLatencies.TREATMENT.value: self._treatment, MethodExceptionsAndLatencies.TREATMENTS.value: self._treatments, - MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG.value: self._treatment_with_config, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG.value: self._treatments_with_config, - MethodExceptionsAndLatencies.TRACK.value: self._track} + exceptions = {MethodExceptionsAndLatencies.METHOD_EXCEPTIONS.value: { + MethodExceptionsAndLatencies.TREATMENT.value: self._treatment, + MethodExceptionsAndLatencies.TREATMENTS.value: self._treatments, + MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG.value: self._treatment_with_config, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG.value: self._treatments_with_config, + MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET.value: self._treatments_by_flag_set, + MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS.value: self._treatments_by_flag_sets, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET.value: self._treatments_with_config_by_flag_set, + MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS.value: self._treatments_with_config_by_flag_sets, + MethodExceptionsAndLatencies.TRACK.value: self._track } + } self._reset_all() return exceptions diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 207b302a..6c78a3ff 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -41,7 +41,6 @@ def test_get_treatment(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) factory = SplitFactory(mocker.Mock(), {'splits': split_storage, @@ -98,7 +97,7 @@ def _raise(*_): ) in impmanager.process_impressions.mock_calls def test_get_treatment_with_config(self, mocker): - """Test get_treatment execution paths.""" + """Test get_treatment with config execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) @@ -110,7 +109,6 @@ def test_get_treatment_with_config(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) factory = SplitFactory(mocker.Mock(), {'splits': split_storage, @@ -175,7 +173,7 @@ def _raise(*_): ) in impmanager.process_impressions.mock_calls def test_get_treatments(self, mocker): - """Test get_treatment execution paths.""" + """Test get_treatments execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) @@ -249,7 +247,7 @@ def _raise(*_): assert client.get_treatments('key', ['f1', 'f2']) == {'f1': 'control', 'f2': 'control'} def test_get_treatments_with_config(self, mocker): - """Test get_treatment execution paths.""" + """Test get_treatments with config execution paths.""" split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) @@ -260,7 +258,6 @@ def test_get_treatments_with_config(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) factory = SplitFactory(mocker.Mock(), {'splits': split_storage, @@ -326,6 +323,380 @@ def _raise(*_): 'f2': ('control', None) } + def test_get_treatments_by_flag_set(self, mocker): + """Test get_treatments by flagset execution paths.""" + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + impmanager = mocker.Mock(spec=ImpressionManager) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + def get_feature_flags_by_set(flag_sets): + if flag_sets == 'set1': + return ['f1', 'f2'] + if flag_sets == 'set2': + return ['f3', 'f4'] + if flag_sets == 'set3': + return ['some_feature'] + split_storage.get_feature_flags_by_set = get_feature_flags_by_set + + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': '{"color": "red"}', + 'impression': { + 'label': 'some_label', + 'change_number': 123 + } + } + def evaluate_features(feature_flag_names, matching_key, bucketing_key, attributes=None): + return {feature_flag_name: evaluation for feature_flag_name in feature_flag_names} + client._evaluator.evaluate_features = evaluate_features + + _logger = mocker.Mock() + client._send_impression_to_listener = mocker.Mock() + assert client.get_treatments_by_flag_set('key', 'set1') == {'f1': 'on', 'f2': 'on'} + + impressions_called = impmanager.process_impressions.mock_calls[0][1][0] + assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + assert client.get_treatments_by_flag_set('key', 'set2') == {'f3': 'on', 'f4': 'on'} + impressions_called = impmanager.process_impressions.mock_calls[1][1][0] + assert (Impression('key', 'f3', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f4', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + # Test with client not ready + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property + impmanager.process_impressions.reset_mock() + assert client.get_treatments_by_flag_set('some_key', 'set3', {'some_attribute': 1}) == {'some_feature': 'control'} + assert mocker.call( + [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] + ) in impmanager.process_impressions.mock_calls + + # Test with exception: + ready_property.return_value = True + split_storage.get_change_number.return_value = -1 + + def _raise(*_): + raise Exception('something') + client._evaluator.evaluate_features = _raise + assert client.get_treatments_by_flag_set('key', 'set1') == {'f1': 'control', 'f2': 'control'} + + def test_get_treatments_by_flag_sets(self, mocker): + """Test get_treatments by flagsets execution paths.""" + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + impmanager = mocker.Mock(spec=ImpressionManager) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + def get_feature_flags_by_set(flag_sets): + if flag_sets == 'set1': + return ['f1'] + if flag_sets == 'set2': + return ['f2'] + if flag_sets == 'set3': + return ['f3', 'f4'] + if flag_sets == 'set4': + return [] + if flag_sets == 'set5': + return ['some_feature'] + split_storage.get_feature_flags_by_set = get_feature_flags_by_set + + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': '{"color": "red"}', + 'impression': { + 'label': 'some_label', + 'change_number': 123 + } + } + def evaluate_features(feature_flag_names, matching_key, bucketing_key, attributes=None): + return {feature_flag_name: evaluation for feature_flag_name in feature_flag_names} + + client._evaluator.evaluate_features = evaluate_features + _logger = mocker.Mock() + client._send_impression_to_listener = mocker.Mock() + assert client.get_treatments_by_flag_sets('key', ['set1', 'set2']) == {'f1': 'on', 'f2': 'on'} + + impressions_called = impmanager.process_impressions.mock_calls[0][1][0] + assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + assert client.get_treatments_by_flag_sets('key', ['set3', 'set4']) == {'f3': 'on', 'f4': 'on'} + impressions_called = impmanager.process_impressions.mock_calls[1][1][0] + assert (Impression('key', 'f3', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f4', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + # Test with client not ready + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property + impmanager.process_impressions.reset_mock() + assert client.get_treatments_by_flag_sets('some_key', ['set5'], {'some_attribute': 1}) == {'some_feature': 'control'} + assert mocker.call( + [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] + ) in impmanager.process_impressions.mock_calls + + # Test with exception: + ready_property.return_value = True + split_storage.get_change_number.return_value = -1 + + def _raise(*_): + raise Exception('something') + client._evaluator.evaluate_features = _raise + assert client.get_treatments_by_flag_sets('key', ['set1', 'set2']) == {'f1': 'control', 'f2': 'control'} + + def test_get_treatments_with_config_by_flag_set(self, mocker): + """Test get_treatments with config by flagset execution paths.""" + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + impmanager = mocker.Mock(spec=ImpressionManager) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + def get_feature_flags_by_set(flag_sets): + if flag_sets == 'set1': + return ['f1', 'f2'] + if flag_sets == 'set2': + return ['f3', 'f4'] + if flag_sets == 'set3': + return ['some_feature'] + split_storage.get_feature_flags_by_set = get_feature_flags_by_set + + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': '{"color": "red"}', + 'impression': { + 'label': 'some_label', + 'change_number': 123 + } + } + def evaluate_features(feature_flag_names, matching_key, bucketing_key, attributes=None): + return {feature_flag_name: evaluation for feature_flag_name in feature_flag_names} + client._evaluator.evaluate_features = evaluate_features + + _logger = mocker.Mock() + assert client.get_treatments_with_config_by_flag_set('key', 'set1') == { + 'f1': ('on', '{"color": "red"}'), + 'f2': ('on', '{"color": "red"}') + } + + impressions_called = impmanager.process_impressions.mock_calls[0][1][0] + assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + _logger = mocker.Mock() + assert client.get_treatments_with_config_by_flag_set('key', 'set2') == { + 'f3': ('on', '{"color": "red"}'), + 'f4': ('on', '{"color": "red"}') + } + + impressions_called = impmanager.process_impressions.mock_calls[1][1][0] + assert (Impression('key', 'f3', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f4', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + # Test with client not ready + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property + impmanager.process_impressions.reset_mock() + assert client.get_treatments_with_config_by_flag_set('some_key', 'set3', {'some_attribute': 1}) == {'some_feature': ('control', None)} + assert mocker.call( + [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] + ) in impmanager.process_impressions.mock_calls + + # Test with exception: + ready_property.return_value = True + split_storage.get_change_number.return_value = -1 + + def _raise(*_): + raise Exception('something') + client._evaluator.evaluate_features = _raise + assert client.get_treatments_with_config_by_flag_set('key', 'set1') == { + 'f1': ('control', None), + 'f2': ('control', None) + } + + def test_get_treatments_with_config_by_flag_sets(self, mocker): + """Test get_treatments with config by flagsets execution paths.""" + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + impmanager = mocker.Mock(spec=ImpressionManager) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + def get_feature_flags_by_set(flag_sets): + if flag_sets == 'set1': + return ['f1'] + if flag_sets == 'set2': + return ['f2'] + if flag_sets == 'set3': + return ['f3', 'f4'] + if flag_sets == 'set4': + return [] + if flag_sets == 'set5': + return ['some_feature'] + split_storage.get_feature_flags_by_set = get_feature_flags_by_set + + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': '{"color": "red"}', + 'impression': { + 'label': 'some_label', + 'change_number': 123 + } + } + def evaluate_features(feature_flag_names, matching_key, bucketing_key, attributes=None): + return {feature_flag_name: evaluation for feature_flag_name in feature_flag_names} + client._evaluator.evaluate_features = evaluate_features + + _logger = mocker.Mock() + assert client.get_treatments_with_config_by_flag_sets('key', ['set1', 'set2']) == { + 'f1': ('on', '{"color": "red"}'), + 'f2': ('on', '{"color": "red"}') + } + + impressions_called = impmanager.process_impressions.mock_calls[0][1][0] + assert (Impression('key', 'f1', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f2', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + _logger = mocker.Mock() + assert client.get_treatments_with_config_by_flag_sets('key', ['set3', 'set4']) == { + 'f3': ('on', '{"color": "red"}'), + 'f4': ('on', '{"color": "red"}') + } + + impressions_called = impmanager.process_impressions.mock_calls[1][1][0] + assert (Impression('key', 'f3', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert (Impression('key', 'f4', 'on', 'some_label', 123, None, 1000), None) in impressions_called + assert _logger.mock_calls == [] + + # Test with client not ready + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property + impmanager.process_impressions.reset_mock() + assert client.get_treatments_with_config_by_flag_sets('some_key', ['set5'], {'some_attribute': 1}) == {'some_feature': ('control', None)} + assert mocker.call( + [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] + ) in impmanager.process_impressions.mock_calls + + # Test with exception: + ready_property.return_value = True + split_storage.get_change_number.return_value = -1 + + def _raise(*_): + raise Exception('something') + client._evaluator.evaluate_features = _raise + assert client.get_treatments_with_config_by_flag_sets('key', ['set1', 'set2']) == { + 'f1': ('control', None), + 'f2': ('control', None) + } + @mock.patch('splitio.client.factory.SplitFactory.destroy') def test_destroy(self, mocker): """Test that destroy/destroyed calls are forwarded to the factory.""" @@ -481,7 +852,7 @@ def test_telemetry_not_ready(self, mocker): @mock.patch('splitio.client.client.Client._evaluate_if_ready', side_effect=Exception()) def test_telemetry_record_treatment_exception(self, mocker): split_storage = InMemorySplitStorage() - split_storage.put(Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)) + split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], 123) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) @@ -525,7 +896,7 @@ def test_telemetry_record_treatment_exception(self, mocker): @mock.patch('splitio.client.client.Client._evaluate_features_if_ready', side_effect=Exception()) def test_telemetry_record_treatments_exception(self, mocker): split_storage = InMemorySplitStorage() - split_storage.put(Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)) + split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], 123) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) @@ -568,7 +939,7 @@ def test_telemetry_record_treatments_exception(self, mocker): def test_telemetry_method_latency(self, mocker): split_storage = InMemorySplitStorage() - split_storage.put(Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)) + split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], 123) segment_storage = mocker.Mock(spec=SegmentStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) diff --git a/tests/client/test_config.py b/tests/client/test_config.py index e9a1c284..d12c0ab8 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -72,20 +72,26 @@ def test_sanitize(self): def test_sanitize_flag_sets(self): """Test sanitization for flag sets.""" - flag_sets = config._sanitize_flag_sets([' set1', 'set2 ', 'set3']) + flag_sets = config.sanitize_flag_sets([' set1', 'set2 ', 'set3']) assert flag_sets == ['set1', 'set2', 'set3'] - flag_sets = config._sanitize_flag_sets(['1set', '_set2']) + flag_sets = config.sanitize_flag_sets(['1set', '_set2']) assert flag_sets == ['1set'] - flag_sets = config._sanitize_flag_sets(['Set1', 'SET2']) + flag_sets = config.sanitize_flag_sets(['Set1', 'SET2']) assert flag_sets == ['set1', 'set2'] - flag_sets = config._sanitize_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5']) + flag_sets = config.sanitize_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5']) assert flag_sets == [] - flag_sets = config._sanitize_flag_sets(['set4', 'set1', 'set3', 'set1']) + flag_sets = config.sanitize_flag_sets(['set4', 'set1', 'set3', 'set1']) assert flag_sets == ['set1', 'set3', 'set4'] - flag_sets = config._sanitize_flag_sets(['w' * 50, 's' * 51]) + flag_sets = config.sanitize_flag_sets(['w' * 50, 's' * 51]) assert flag_sets == ['w' * 50] + + flag_sets = config.sanitize_flag_sets('set1') + assert flag_sets == [] + + flag_sets = config.sanitize_flag_sets([12, 33]) + assert flag_sets == [] diff --git a/tests/models/test_telemetry_model.py b/tests/models/test_telemetry_model.py index 26e705a0..b33dbd4b 100644 --- a/tests/models/test_telemetry_model.py +++ b/tests/models/test_telemetry_model.py @@ -54,6 +54,14 @@ def test_method_latencies(self, mocker): assert(method_latencies._treatment_with_config[ModelTelemetry.get_latency_bucket_index(50)] == 1) elif method.value == 'treatments_with_config': assert(method_latencies._treatments_with_config[ModelTelemetry.get_latency_bucket_index(50)] == 1) + elif method.value == 'treatments_by_flag_set': + assert(method_latencies._treatments_by_flag_set[ModelTelemetry.get_latency_bucket_index(50)] == 1) + elif method.value == 'treatments_by_flag_sets': + assert(method_latencies._treatments_by_flag_sets[ModelTelemetry.get_latency_bucket_index(50)] == 1) + elif method.value == 'treatments_with_config_by_flag_set': + assert(method_latencies._treatments_with_config_by_flag_set[ModelTelemetry.get_latency_bucket_index(50)] == 1) + elif method.value == 'treatments_with_config_by_flag_sets': + assert(method_latencies._treatments_with_config_by_flag_sets[ModelTelemetry.get_latency_bucket_index(50)] == 1) elif method.value == 'track': assert(method_latencies._track[ModelTelemetry.get_latency_bucket_index(50)] == 1) method_latencies.add_latency(method, 50000000) @@ -65,6 +73,14 @@ def test_method_latencies(self, mocker): assert(method_latencies._treatment_with_config[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) if method.value == 'treatments_with_config': assert(method_latencies._treatments_with_config[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) + elif method.value == 'treatments_by_flag_set': + assert(method_latencies._treatments_by_flag_set[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) + elif method.value == 'treatments_by_flag_sets': + assert(method_latencies._treatments_by_flag_sets[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) + elif method.value == 'treatments_with_config_by_flag_set': + assert(method_latencies._treatments_with_config_by_flag_set[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) + elif method.value == 'treatments_with_config_by_flag_sets': + assert(method_latencies._treatments_with_config_by_flag_sets[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) if method.value == 'track': assert(method_latencies._track[ModelTelemetry.get_latency_bucket_index(50000000)] == 1) @@ -79,9 +95,23 @@ def test_method_latencies(self, mocker): [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS, 20) for i in range(2)] method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, 50) method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, 20) + [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, 20) for i in range(3)] + [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, 20) for i in range(4)] + [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, 20) for i in range(5)] + [method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, 20) for i in range(6)] method_latencies.add_latency(ModelTelemetry.MethodExceptionsAndLatencies.TRACK, 20) latencies = method_latencies.pop_all() - assert(latencies == {'methodLatencies': {'treatment': [1] + [0] * 22, 'treatments': [2] + [0] * 22, 'treatment_with_config': [1] + [0] * 22, 'treatments_with_config': [1] + [0] * 22, 'track': [1] + [0] * 22}}) + assert(latencies == {'methodLatencies': {'treatment': [1] + [0] * 22, + 'treatments': [2] + [0] * 22, + 'treatment_with_config': [1] + [0] * 22, + 'treatments_with_config': [1] + [0] * 22, + 'treatments_by_flag_set': [3] + [0] * 22, + 'treatments_by_flag_sets': [4] + [0] * 22, + 'treatments_with_config_by_flag_set': [5] + [0] * 22, + 'treatments_with_config_by_flag_sets': [6] + [0] * 22, + 'track': [1] + [0] * 22} + } + ) def test_http_latencies(self, mocker): http_latencies = HTTPLatencies() @@ -144,6 +174,10 @@ def test_method_exceptions(self, mocker): method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG) [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG) for i in range(5)] [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TRACK) for i in range(3)] + [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET) for i in range(6)] + [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS) for i in range(7)] + [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET) for i in range(8)] + [method_exception.add_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS) for i in range(9)] exceptions = method_exception.pop_all() assert(method_exception._treatment == 0) @@ -151,7 +185,18 @@ def test_method_exceptions(self, mocker): assert(method_exception._treatment_with_config == 0) assert(method_exception._treatments_with_config == 0) assert(method_exception._track == 0) - assert(exceptions == {'methodExceptions': {'treatment': 2, 'treatments': 1, 'treatment_with_config': 1, 'treatments_with_config': 5, 'track': 3}}) + assert(exceptions == {'methodExceptions': {'treatment': 2, + 'treatments': 1, + 'treatment_with_config': 1, + 'treatments_with_config': 5, + 'treatments_by_flag_set': 6, + 'treatments_by_flag_sets': 7, + 'treatments_with_config_by_flag_set': 8, + 'treatments_with_config_by_flag_sets': 9, + 'track': 3 + } + } + ) def test_http_errors(self, mocker): http_error = HTTPErrors() From 67438392e1bc881bedae11de0732437bb3165b49 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 30 Aug 2023 11:52:22 -0700 Subject: [PATCH 23/77] polish --- splitio/client/client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index e84df605..0bcb3939 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -323,8 +323,8 @@ def get_treatments_by_flag_set(self, key, flag_set, attributes=None): :param attributes: An optional dictionary of attributes :type attributes: dict - :return: The treatment for the key and feature flag - :rtype: str + :return: Dictionary with the result of all the feature flags provided + :rtype: dict """ return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes) @@ -342,7 +342,7 @@ def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): :param attributes: An optional dictionary of attributes :type attributes: dict - :return: The treatment for the key and feature flag + :return: Dictionary with the result of all the feature flags provided :rtype: dict """ return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes) @@ -361,8 +361,8 @@ def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None) :param attributes: An optional dictionary of attributes :type attributes: dict - :return: The treatment for the key and feature flag - :rtype: str + :return: Dictionary with the result of all the feature flags provided + :rtype: dict """ return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes) @@ -380,8 +380,8 @@ def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=Non :param attributes: An optional dictionary of attributes :type attributes: dict - :return: The treatment for the key and feature flag - :rtype: str + :return: Dictionary with the result of all the feature flags provided + :rtype: dict """ return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes) @@ -401,8 +401,8 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): :param attributes: An optional dictionary of attributes :type attributes: dict - :return: The treatment for the key and feature flag - :rtype: str + :return: Dictionary with the result of all the feature flags provided + :rtype: dict """ feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets) if feature_flags_names == []: From bf636145315aacc93e84fbce861e9bdac02fcb61 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 31 Aug 2023 10:00:25 -0700 Subject: [PATCH 24/77] added client.manager test, minor fix in models.splits and updated config param name --- splitio/client/config.py | 4 ++-- splitio/models/splits.py | 2 +- tests/client/test_manager.py | 41 +++++++++++++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index aa5c391b..02a2d696 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -60,7 +60,7 @@ 'storageWrapper': None, 'storagePrefix': None, 'storageType': None, - 'FlagSets': None + 'FlagSetsFilter': None } def _parse_operation_mode(sdk_key, config): @@ -179,6 +179,6 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 - processed['FlagSets'] = _sanitize_flag_sets(processed['FlagSets']) if processed['FlagSets'] is not None else None + processed['FlagSetsFilter'] = sanitize_flag_sets(processed['FlagSetsFilter']) if processed['FlagSetsFilter'] is not None else None return processed diff --git a/splitio/models/splits.py b/splitio/models/splits.py index cf6a3c7b..241650e8 100644 --- a/splitio/models/splits.py +++ b/splitio/models/splits.py @@ -200,7 +200,7 @@ def to_split_view(self): list(set(part.treatment for cond in self.conditions for part in cond.partitions)), self.change_number, self._configurations if self._configurations is not None else {}, - self._sets + self._sets if self._sets is not None else [] ) def local_kill(self, default_treatment, change_number): diff --git a/tests/client/test_manager.py b/tests/client/test_manager.py index 30916177..7079b31c 100644 --- a/tests/client/test_manager.py +++ b/tests/client/test_manager.py @@ -1,13 +1,15 @@ """SDK main manager test module.""" +import pytest from splitio.client.factory import SplitFactory from splitio.client.manager import SplitManager, _LOGGER as _logger from splitio.storage import SplitStorage, EventStorage, ImpressionStorage, SegmentStorage -from splitio.storage.inmemmory import InMemoryTelemetryStorage +from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemorySplitStorage +from splitio.models import splits from splitio.engine.impressions.impressions import Manager as ImpressionManager from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageConsumer from splitio.recorder.recorder import StandardRecorder - +from tests.models.test_splits import SplitTests class ManagerTests(object): # pylint: disable=too-few-public-methods """Split manager test cases.""" @@ -19,7 +21,6 @@ def test_evaluations_before_running_post_fork(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) recorder = StandardRecorder(impmanager, mocker.Mock(), mocker.Mock(), telemetry_producer.get_telemetry_evaluation_producer()) factory = SplitFactory(mocker.Mock(), {'splits': mocker.Mock(), @@ -55,3 +56,37 @@ def test_evaluations_before_running_post_fork(self, mocker): assert manager.splits() == [] assert _logger.error.mock_calls == expected_msg _logger.reset_mock() + + def test_manager_calls(self, mocker): + split_storage = InMemorySplitStorage() + split = splits.from_raw(SplitTests.raw) + split_storage.update([split], [], 123) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': mocker.Mock(), + 'impressions': mocker.Mock(), + 'events': mocker.Mock()}, + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + mocker.Mock(), + False + ) + manager = SplitManager(factory) + splits_view = manager.splits() + self._verify_split(splits_view[0]) + assert manager.split_names() == ['some_name'] + split_view = manager.split('some_name') + self._verify_split(split_view) + + def _verify_split(self, split): + assert split.name == 'some_name' + assert split.traffic_type == 'user' + assert split.killed == False + assert split.treatments == ['on', 'off'] + assert split.change_number == 123 + assert split.configs == {'on': '{"color": "blue", "size": 13}'} + assert split.sets == ['set1', 'set2'] From f2b9858711cd580ca70bc8615889a38afc3be1e8 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 31 Aug 2023 12:50:36 -0700 Subject: [PATCH 25/77] Added flagset support to factory --- splitio/client/factory.py | 2 +- splitio/models/telemetry.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index fede6ad0..c4b4d1a0 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -350,7 +350,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl } storages = { - 'splits': InMemorySplitStorage(), + 'splits': InMemorySplitStorage(cfg['FlagSetsFilter'] if cfg['FlagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(), 'impressions': InMemoryImpressionStorage(cfg['impressionsQueueSize'], telemetry_runtime_producer), 'events': InMemoryEventStorage(cfg['eventsQueueSize'], telemetry_runtime_producer), diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index c06a2bbd..bd57a506 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -41,6 +41,7 @@ class ConfigParams(Enum): EVENTS_QUEUE_SIZE = 'eventsQueueSize' IMPRESSIONS_MODE = 'impressionsMode' IMPRESSIONS_LISTENER = 'impressionListener' + FLAG_SETS = 'FlagSetsFilter' class ExtraConfig(Enum): """Extra config constants""" @@ -671,6 +672,8 @@ def pop_update_from_sse(self, event): :rtype: int """ with self._lock: + if self._update_from_sse.get(event.value) is None: + return 0 update_from_sse = self._update_from_sse[event.value] self._update_from_sse[event.value] = 0 return update_from_sse @@ -826,6 +829,7 @@ def record_config(self, config, extra_config): self._impressions_mode = self._get_impressions_mode(config[ConfigParams.IMPRESSIONS_MODE.value]) self._impression_listener = True if config[ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False self._http_proxy = self._check_if_proxy_detected() + self._flag_sets = len(config[ConfigParams.FLAG_SETS.value]) if config[ConfigParams.FLAG_SETS.value] is not None else 0 def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): with self._lock: From f62b88a656e92fc42cf7275516e8d0a25048f8fe Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 1 Sep 2023 08:33:09 -0700 Subject: [PATCH 26/77] added test --- tests/client/test_manager.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/client/test_manager.py b/tests/client/test_manager.py index 7079b31c..6e30837c 100644 --- a/tests/client/test_manager.py +++ b/tests/client/test_manager.py @@ -81,12 +81,19 @@ def test_manager_calls(self, mocker): assert manager.split_names() == ['some_name'] split_view = manager.split('some_name') self._verify_split(split_view) + split2 = SplitTests.raw.copy() + split2['sets'] = None + split2['name'] = 'no_sets_split' + split_storage.update([splits.from_raw(split2)], [], 123) + + split_view = manager.split('no_sets_split') + assert split_view.sets == [] def _verify_split(self, split): assert split.name == 'some_name' assert split.traffic_type == 'user' assert split.killed == False - assert split.treatments == ['on', 'off'] + assert sorted(split.treatments) == ['off', 'on'] assert split.change_number == 123 assert split.configs == {'on': '{"color": "blue", "size": 13}'} assert split.sets == ['set1', 'set2'] From f5c3a96de0ed47122208ed9c5a20fd481403b93f Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 1 Sep 2023 11:39:56 -0700 Subject: [PATCH 27/77] updated config param name --- splitio/client/config.py | 4 ++-- splitio/client/factory.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index aa5c391b..3576ecde 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -60,7 +60,7 @@ 'storageWrapper': None, 'storagePrefix': None, 'storageType': None, - 'FlagSets': None + 'flagSetsFilter': None } def _parse_operation_mode(sdk_key, config): @@ -179,6 +179,6 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 - processed['FlagSets'] = _sanitize_flag_sets(processed['FlagSets']) if processed['FlagSets'] is not None else None + processed['flagSetsFilter'] = sanitize_flag_sets(processed['flagSetsFilter']) if processed['flagSetsFilter'] is not None else None return processed diff --git a/splitio/client/factory.py b/splitio/client/factory.py index c4b4d1a0..d777fbf2 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -350,7 +350,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl } storages = { - 'splits': InMemorySplitStorage(cfg['FlagSetsFilter'] if cfg['FlagSetsFilter'] is not None else []), + 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(), 'impressions': InMemoryImpressionStorage(cfg['impressionsQueueSize'], telemetry_runtime_producer), 'events': InMemoryEventStorage(cfg['eventsQueueSize'], telemetry_runtime_producer), From 19f5f61145c6d36674ea8fb7d6e7ed658dd91178 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 1 Sep 2023 12:27:04 -0700 Subject: [PATCH 28/77] added flagsets to redis split storage --- splitio/client/factory.py | 4 +- splitio/storage/redis.py | 150 ++++++++++++++++++++++-------------- tests/storage/test_redis.py | 14 ++++ 3 files changed, 110 insertions(+), 58 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index fede6ad0..63c01ddd 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -350,7 +350,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl } storages = { - 'splits': InMemorySplitStorage(), + 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(), 'impressions': InMemoryImpressionStorage(cfg['impressionsQueueSize'], telemetry_runtime_producer), 'events': InMemoryEventStorage(cfg['eventsQueueSize'], telemetry_runtime_producer), @@ -440,7 +440,7 @@ def _build_redis_factory(api_key, cfg): cache_enabled = cfg.get('redisLocalCacheEnabled', False) cache_ttl = cfg.get('redisLocalCacheTTL', 5) storages = { - 'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl), + 'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': RedisSegmentStorage(redis_adapter), 'impressions': RedisImpressionsStorage(redis_adapter, sdk_metadata), 'events': RedisEventsStorage(redis_adapter, sdk_metadata), diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 9433fdd4..cfafa200 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -16,13 +16,14 @@ MAX_TAGS = 10 class RedisSplitStorage(SplitStorage): - """Redis-based storage for splits.""" + """Redis-based storage for feature flags.""" - _SPLIT_KEY = 'SPLITIO.split.{split_name}' - _SPLIT_TILL_KEY = 'SPLITIO.splits.till' + _FEATURE_FLAG_KEY = 'SPLITIO.split.{feature_flag_name}' + _FEATURE_FLAG_TILL_KEY = 'SPLITIO.splits.till' _TRAFFIC_TYPE_KEY = 'SPLITIO.trafficType.{traffic_type_name}' + _SET_KEY = 'SPLITIO.set.{flag_set}' - def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE): + def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, flag_sets=[]): """ Class constructor. @@ -30,87 +31,124 @@ def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE): :type redis_client: splitio.storage.adapters.redis.RedisAdapter """ self._redis = redis_client + self._flag_sets = flag_sets if enable_caching: self.get = add_cache(lambda *p, **_: p[0], max_age)(self.get) self.is_valid_traffic_type = add_cache(lambda *p, **_: p[0], max_age)(self.is_valid_traffic_type) # pylint: disable=line-too-long self.fetch_many = add_cache(lambda *p, **_: frozenset(p[0]), max_age)(self.fetch_many) - def _get_key(self, split_name): + def _get_key(self, feature_flag_name): """ - Use the provided split_name to build the appropriate redis key. + Use the provided feature_flag_name to build the appropriate redis key. - :param split_name: Name of the split to interact with in redis. - :type split_name: str + :param feature_flag_name: Name of the feature flag to interact with in redis. + :type feature_flag_name: str :return: Redis key. :rtype: str. """ - return self._SPLIT_KEY.format(split_name=split_name) + return self._FEATURE_FLAG_KEY.format(feature_flag_name=feature_flag_name) def _get_traffic_type_key(self, traffic_type_name): """ - Use the provided split_name to build the appropriate redis key. + Use the provided traffic_type_name to build the appropriate redis key. - :param split_name: Name of the split to interact with in redis. - :type split_name: str + :param trafic_type_name: Name of the traffic type to interact with in redis. + :type traffic_type_name: str :return: Redis key. :rtype: str. """ return self._TRAFFIC_TYPE_KEY.format(traffic_type_name=traffic_type_name) - def get(self, split_name): # pylint: disable=method-hidden + def _get_set_key(self, flag_set): """ - Retrieve a split. + Use the provided flag set to build the appropriate redis key. + + :param flag_set: Name of the flag set to interact with in redis. + :type flag_set: str - :param split_name: Name of the feature to fetch. - :type split_name: str + :return: Redis key. + :rtype: str. + """ + return self._SET_KEY.format(flag_set=flag_set) + + def get(self, feature_flag_name): # pylint: disable=method-hidden + """ + Retrieve a feature flag. + + :param feature_flag_name: Name of the feature to fetch. + :type feature_flag_name: str :return: A split object parsed from redis if the key exists. None otherwise :rtype: splitio.models.splits.Split """ try: - raw = self._redis.get(self._get_key(split_name)) - _LOGGER.debug("Fetchting Split [%s] from redis" % split_name) + raw = self._redis.get(self._get_key(feature_flag_name)) + _LOGGER.debug("Fetchting Feature flag [%s] from redis" % feature_flag_name) _LOGGER.debug(raw) return splits.from_raw(json.loads(raw)) if raw is not None else None except RedisAdapterException: - _LOGGER.error('Error fetching split from storage') + _LOGGER.error('Error fetching feature flag from storage') + _LOGGER.debug('Error: ', exc_info=True) + return None + + def get_feature_flags_by_set(self, flag_set): + """ + Retrieve feature flags by flag set. + + :param feature_flag_names: Names of the features to fetch. + :type feature_flag_name: list(str) + + :return: A dict with split objects parsed from redis. + :rtype: dict(split_name, splitio.models.splits.Split) + """ + try: + if flag_set not in self._flag_sets and len(self._flag_sets) > 0: + _LOGGER.warning("Flag set %s used is not part of the configured flag set list, ignoring the request." % (flag_set)) + return [] + + keys = list(self._redis.smembers(self._get_set_key(flag_set))) + _LOGGER.debug("Fetchting Feature flags by set [%s] from redis" % (flag_set)) + _LOGGER.debug(keys) + return keys if keys is not None else [] + except RedisAdapterException: + _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def fetch_many(self, split_names): + def fetch_many(self, feature_flag_names): """ - Retrieve splits. + Retrieve feature flags. - :param split_names: Names of the features to fetch. - :type split_name: list(str) + :param feature_flag_names: Names of the features to fetch. + :type feature_flag_name: list(str) :return: A dict with split objects parsed from redis. :rtype: dict(split_name, splitio.models.splits.Split) """ to_return = dict() try: - keys = [self._get_key(split_name) for split_name in split_names] - raw_splits = self._redis.mget(keys) - _LOGGER.debug("Fetchting Splits [%s] from redis" % split_names) - _LOGGER.debug(raw_splits) - for i in range(len(split_names)): - split = None + keys = [self._get_key(feature_flag_name) for feature_flag_name in feature_flag_names] + raw_feature_flags = self._redis.mget(keys) + _LOGGER.debug("Fetchting feature flags [%s] from redis" % feature_flag_names) + _LOGGER.debug(raw_feature_flags) + for i in range(len(feature_flag_names)): + feature_flag = None try: - split = splits.from_raw(json.loads(raw_splits[i])) + feature_flag = splits.from_raw(json.loads(raw_feature_flags[i])) except (ValueError, TypeError): - _LOGGER.error('Could not parse split.') - _LOGGER.debug("Raw split that failed parsing attempt: %s", raw_splits[i]) - to_return[split_names[i]] = split + _LOGGER.error('Could not parse feature flag.') + _LOGGER.debug("Raw feature flag that failed parsing attempt: %s", raw_feature_flags[i]) + to_return[feature_flag_names[i]] = feature_flag except RedisAdapterException: - _LOGGER.error('Error fetching splits from storage') + _LOGGER.error('Error fetching feature flags from storage') _LOGGER.debug('Error: ', exc_info=True) return to_return def is_valid_traffic_type(self, traffic_type_name): # pylint: disable=method-hidden """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -124,7 +162,7 @@ def is_valid_traffic_type(self, traffic_type_name): # pylint: disable=method-hi _LOGGER.debug("Fetching TrafficType [%s] count in redis: %s" % (traffic_type_name, count)) return count > 0 except RedisAdapterException: - _LOGGER.error('Error fetching split from storage') + _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return False @@ -143,32 +181,32 @@ def update(self, to_add, to_delete, new_change_number): def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ try: - stored_value = self._redis.get(self._SPLIT_TILL_KEY) - _LOGGER.debug("Fetching Split Change Number from redis: %s" % stored_value) + stored_value = self._redis.get(self._FEATURE_FLAG_TILL_KEY) + _LOGGER.debug("Fetching feature flag Change Number from redis: %s" % stored_value) return json.loads(stored_value) if stored_value is not None else None except RedisAdapterException: - _LOGGER.error('Error fetching split change number from storage') + _LOGGER.error('Error fetching feature flag change number from storage') _LOGGER.debug('Error: ', exc_info=True) return None def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ try: keys = self._redis.keys(self._get_key('*')) - _LOGGER.debug("Fetchting Split names from redis: %s" % keys) + _LOGGER.debug("Fetchting feature flag names from redis: %s" % keys) return [key.replace(self._get_key(''), '') for key in keys] except RedisAdapterException: - _LOGGER.error('Error fetching split names from storage') + _LOGGER.error('Error fetching feature flag names from storage') _LOGGER.debug('Error: ', exc_info=True) return [] @@ -182,33 +220,33 @@ def get_splits_count(self): def get_all_splits(self): """ - Return all the splits in cache. - :return: List of all splits in cache. + Return all the feature flags in cache. + :return: List of all feature flags in cache. :rtype: list(splitio.models.splits.Split) """ keys = self._redis.keys(self._get_key('*')) to_return = [] try: - _LOGGER.debug("Fetchting all Splits from redis: %s" % keys) - raw_splits = self._redis.mget(keys) - _LOGGER.debug(raw_splits) - for raw in raw_splits: + _LOGGER.debug("Fetchting all feature flags from redis: %s" % keys) + raw_feature_flags = self._redis.mget(keys) + _LOGGER.debug(raw_feature_flags) + for raw in raw_feature_flags: try: to_return.append(splits.from_raw(json.loads(raw))) except (ValueError, TypeError): - _LOGGER.error('Could not parse split. Skipping') - _LOGGER.debug("Raw split that failed parsing attempt: %s", raw) + _LOGGER.error('Could not parse feature flag. Skipping') + _LOGGER.debug("Raw feature flag that failed parsing attempt: %s", raw) except RedisAdapterException: _LOGGER.error('Error fetching all splits from storage') _LOGGER.debug('Error: ', exc_info=True) return to_return - def kill_locally(self, split_name, default_treatment, change_number): + def kill_locally(self, feature_flag_name, default_treatment, change_number): """ - Local kill for split + Local kill for feature flag - :param split_name: name of the split to perform kill - :type split_name: str + :param feature_flag_name: name of the feature flag to perform kill + :type feature_flag_name: str :param default_treatment: name of the default treatment to return :type default_treatment: str :param change_number: change_number diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 33fef5a6..63b02425 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -172,6 +172,20 @@ def test_is_valid_traffic_type_with_cache(self, mocker): time.sleep(1) assert storage.is_valid_traffic_type('any') is False + def test_flag_sets(self, mocker): + """Test Flag sets scenarios.""" + adapter = mocker.Mock(spec=RedisAdapter) + adapter.smembers.return_value = set({'split1', 'split2'}) + storage = RedisSplitStorage(adapter, True, 1) + assert storage._flag_sets == [] + assert sorted(storage.get_feature_flags_by_set('set1')) == ['split1', 'split2'] + + storage._flag_sets = ['set2', 'set3'] + assert storage.get_feature_flags_by_set('set1') == [] + assert sorted(storage.get_feature_flags_by_set('set2')) == ['split1', 'split2'] + + storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) + assert storage2._flag_sets == ['set2', 'set3'] class RedisSegmentStorageTests(object): """Redis segment storage test cases.""" From 0c93d555202094bf58921cc96d3c57f4982ff379 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 1 Sep 2023 12:36:58 -0700 Subject: [PATCH 29/77] polish --- splitio/storage/redis.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index cfafa200..bf815f1d 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -97,15 +97,15 @@ def get_feature_flags_by_set(self, flag_set): """ Retrieve feature flags by flag set. - :param feature_flag_names: Names of the features to fetch. - :type feature_flag_name: list(str) + :param flag_set: Names of the flag set to fetch. + :type flag_set: str - :return: A dict with split objects parsed from redis. - :rtype: dict(split_name, splitio.models.splits.Split) + :return: Feature flag names that are tagged with the flag set + :rtype: listt(str) """ try: if flag_set not in self._flag_sets and len(self._flag_sets) > 0: - _LOGGER.warning("Flag set %s used is not part of the configured flag set list, ignoring the request." % (flag_set)) + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) return [] keys = list(self._redis.smembers(self._get_set_key(flag_set))) From 198f5b2d9286f85e4358750afa25ef2e8a608563 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 1 Sep 2023 14:55:20 -0700 Subject: [PATCH 30/77] Updated fetching splits by flag set to batch fetching for redis and memory --- splitio/client/client.py | 5 +--- splitio/storage/adapters/redis.py | 4 +++ splitio/storage/inmemmory.py | 19 +++++++++++-- splitio/storage/redis.py | 25 ++++++++++++----- tests/storage/test_inmemory_storage.py | 39 +++++++++++++------------- tests/storage/test_redis.py | 10 +++---- 6 files changed, 64 insertions(+), 38 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 0bcb3939..c952b418 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -429,10 +429,7 @@ def _get_feature_flag_names_by_flag_sets(self, flag_sets): :rtype: list """ sanitized_flag_sets = config.sanitize_flag_sets(flag_sets) - feature_flags = [] - [feature_flags.extend(self._split_storage.get_feature_flags_by_set(flag_set)) for flag_set in sanitized_flag_sets] - feature_flags_names = [] - [feature_flags_names.append(feature_flag) for feature_flag in feature_flags] + feature_flags_names = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets) return feature_flags_names def _build_impression( # pylint: disable=too-many-arguments diff --git a/splitio/storage/adapters/redis.py b/splitio/storage/adapters/redis.py index de3026b3..8657b317 100644 --- a/splitio/storage/adapters/redis.py +++ b/splitio/storage/adapters/redis.py @@ -339,6 +339,10 @@ def execute(self): except RedisError as exc: raise RedisAdapterException('Error executing pipeline operation') from exc + def smembers(self, name): + """Mimic original redis function but using user custom prefix.""" + self._pipe.smembers(self._prefix_helper.add_prefix(name)) + def _build_default_client(config): # pylint: disable=too-many-locals """ diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index b8a621a6..39fe6f3e 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -122,7 +122,7 @@ def _remove_from_flag_sets(self, feature_flag): if len(self._sets_feature_flag_map[flag_set]) == 0 and self.config_flag_sets_used == 0: del self._sets_feature_flag_map[flag_set] - def get_feature_flags_by_set(self, set): + def get_feature_flags_by_sets(self, sets): """ Get list of feature flag names associated to a set, if it does not exist will return empty list @@ -133,9 +133,22 @@ def get_feature_flags_by_set(self, set): :rtype: list """ with self._lock: - if set not in self._sets_feature_flag_map: + sets_to_fetch = [] + for flag_set in sets: + if flag_set not in self._sets_feature_flag_map.keys(): + if self.config_flag_sets_used > 0: + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) + continue + else: + self._sets_feature_flag_map[flag_set] = set() + sets_to_fetch.append(flag_set) + + if sets_to_fetch == []: return [] - return list(self._sets_feature_flag_map[set]) + + to_return = set() + [to_return.update(self._sets_feature_flag_map[flag_set]) for flag_set in sets_to_fetch] + return list(to_return) def get_change_number(self): """ diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index bf815f1d..55d609f7 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -93,7 +93,7 @@ def get(self, feature_flag_name): # pylint: disable=method-hidden _LOGGER.debug('Error: ', exc_info=True) return None - def get_feature_flags_by_set(self, flag_set): + def get_feature_flags_by_sets(self, flag_sets): """ Retrieve feature flags by flag set. @@ -104,14 +104,25 @@ def get_feature_flags_by_set(self, flag_set): :rtype: listt(str) """ try: - if flag_set not in self._flag_sets and len(self._flag_sets) > 0: - _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) + sets_to_fetch = [] + for flag_set in flag_sets: + if flag_set not in self._flag_sets and len(self._flag_sets) > 0: + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) + continue + sets_to_fetch.append(flag_set) + + if sets_to_fetch == []: return [] - keys = list(self._redis.smembers(self._get_set_key(flag_set))) - _LOGGER.debug("Fetchting Feature flags by set [%s] from redis" % (flag_set)) - _LOGGER.debug(keys) - return keys if keys is not None else [] + keys = [self._get_set_key(feature_flag_name) for feature_flag_name in sets_to_fetch] + pipe = self._redis.pipeline() + [pipe.smembers(key) for key in keys] + result_sets = pipe.execute() + _LOGGER.debug("Fetchting Feature flags by set [%s] from redis" % (keys)) + _LOGGER.debug(result_sets) + to_return = set() + [to_return.update(result_set) for result_set in result_sets] + return list(to_return) except RedisAdapterException: _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index a0e7fff3..df5449c6 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -225,34 +225,35 @@ def test_flag_sets_with_config_sets(self): split3 = Split('split3', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set04', 'set05']) storage.update([split1], [], 1) - assert storage.get_feature_flags_by_set('set10') == ['split1'] - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set10']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02', 'set10']) == ['split1'] assert storage.is_flag_set_exist('set10') assert storage.is_flag_set_exist('set02') assert not storage.is_flag_set_exist('set03') storage.update([split2], [], 1) - assert storage.get_feature_flags_by_set('set05') == ['split2'] - assert sorted(storage.get_feature_flags_by_set('set02')) == ['split1', 'split2'] + assert storage.get_feature_flags_by_sets(['set05']) == ['split2'] + assert sorted(storage.get_feature_flags_by_sets(['set02', 'set05'])) == ['split1', 'split2'] assert storage.is_flag_set_exist('set05') storage.update([], [split2.name], 1) assert storage.is_flag_set_exist('set05') - assert storage.get_feature_flags_by_set('set02') == ['split1'] - assert storage.get_feature_flags_by_set('set05') == [] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set05']) == [] split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set02']) storage.update([split1], [], 1) assert storage.is_flag_set_exist('set10') - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] storage.update([], [split1.name], 1) - assert storage.get_feature_flags_by_set('set02') == [] + assert storage.get_feature_flags_by_sets(['set02']) == [] assert storage._sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} storage.update([split3], [], 1) - assert storage.get_feature_flags_by_set('set05') == ['split3'] + assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] assert not storage.is_flag_set_exist('set04') def test_flag_sets_withut_config_sets(self): @@ -267,34 +268,34 @@ def test_flag_sets_withut_config_sets(self): split3 = Split('split3', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set04', 'set05']) storage.update([split1], [], 1) - assert storage.get_feature_flags_by_set('set10') == ['split1'] - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set10']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] assert storage.is_flag_set_exist('set10') assert storage.is_flag_set_exist('set02') assert not storage.is_flag_set_exist('set03') storage.update([split2], [], 1) - assert storage.get_feature_flags_by_set('set05') == ['split2'] - assert sorted(storage.get_feature_flags_by_set('set02')) == ['split1', 'split2'] + assert storage.get_feature_flags_by_sets(['set05']) == ['split2'] + assert sorted(storage.get_feature_flags_by_sets(['set02', 'set05'])) == ['split1', 'split2'] assert storage.is_flag_set_exist('set05') storage.update([], [split2.name], 1) assert not storage.is_flag_set_exist('set05') - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set02']) storage.update([split1], [], 1) assert not storage.is_flag_set_exist('set10') - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] storage.update([], [split1.name], 1) - assert storage.get_feature_flags_by_set('set02') == [] - assert storage._sets_feature_flag_map == {} + assert storage.get_feature_flags_by_sets(['set02']) == [] + assert storage._sets_feature_flag_map == {'set02': set()} storage.update([split3], [], 1) - assert storage.get_feature_flags_by_set('set05') == ['split3'] - assert storage.get_feature_flags_by_set('set04') == ['split3'] + assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] + assert storage.get_feature_flags_by_sets(['set04', 'set05']) == ['split3'] class InMemorySegmentStorageTests(object): diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 63b02425..22c40d7a 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -172,17 +172,17 @@ def test_is_valid_traffic_type_with_cache(self, mocker): time.sleep(1) assert storage.is_valid_traffic_type('any') is False + @mock.patch('splitio.storage.adapters.redis.RedisPipelineAdapter.execute', return_value = [{'split1', 'split2'}]) def test_flag_sets(self, mocker): """Test Flag sets scenarios.""" - adapter = mocker.Mock(spec=RedisAdapter) - adapter.smembers.return_value = set({'split1', 'split2'}) + adapter = build({}) storage = RedisSplitStorage(adapter, True, 1) assert storage._flag_sets == [] - assert sorted(storage.get_feature_flags_by_set('set1')) == ['split1', 'split2'] + assert sorted(storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] storage._flag_sets = ['set2', 'set3'] - assert storage.get_feature_flags_by_set('set1') == [] - assert sorted(storage.get_feature_flags_by_set('set2')) == ['split1', 'split2'] + assert storage.get_feature_flags_by_sets(['set1']) == [] + assert sorted(storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) assert storage2._flag_sets == ['set2', 'set3'] From 44447f698acc9af952f2e3997ac6a9e38233a9da Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 1 Sep 2023 15:10:45 -0700 Subject: [PATCH 31/77] polish --- splitio/storage/redis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 55d609f7..90d7ac13 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -32,6 +32,7 @@ def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, """ self._redis = redis_client self._flag_sets = flag_sets + self._pipe = self._redis.pipeline if enable_caching: self.get = add_cache(lambda *p, **_: p[0], max_age)(self.get) self.is_valid_traffic_type = add_cache(lambda *p, **_: p[0], max_age)(self.is_valid_traffic_type) # pylint: disable=line-too-long @@ -115,7 +116,7 @@ def get_feature_flags_by_sets(self, flag_sets): return [] keys = [self._get_set_key(feature_flag_name) for feature_flag_name in sets_to_fetch] - pipe = self._redis.pipeline() + pipe = self._pipe() [pipe.smembers(key) for key in keys] result_sets = pipe.execute() _LOGGER.debug("Fetchting Feature flags by set [%s] from redis" % (keys)) From a2998828ce9a8a7c6ab6a0a20de9a7e85acceb01 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 5 Sep 2023 10:37:09 -0700 Subject: [PATCH 32/77] polish --- splitio/models/telemetry.py | 82 ++++++++++++++-------------- tests/models/test_telemetry_model.py | 3 +- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index bd57a506..b15f15e7 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -27,7 +27,7 @@ class CounterConstants(Enum): EVENTS_QUEUED = 'eventsQueued' EVENTS_DROPPED = 'eventsDropped' -class ConfigParams(Enum): +class _ConfigParams(Enum): """Config parameters constants""" SPLITS_REFRESH_RATE = 'featuresRefreshRate' SEGMENTS_REFRESH_RATE = 'segmentsRefreshRate' @@ -41,9 +41,9 @@ class ConfigParams(Enum): EVENTS_QUEUE_SIZE = 'eventsQueueSize' IMPRESSIONS_MODE = 'impressionsMode' IMPRESSIONS_LISTENER = 'impressionListener' - FLAG_SETS = 'FlagSetsFilter' + FLAG_SETS = 'flagSetsFilter' -class ExtraConfig(Enum): +class _ExtraConfig(Enum): """Extra config constants""" ACTIVE_FACTORY_COUNT = 'activeFactoryCount' REDUNDANT_FACTORY_COUNT = 'redundantFactoryCount' @@ -54,7 +54,7 @@ class ExtraConfig(Enum): HTTP_PROXY = 'httpProxy' HTTPS_PROXY_ENV = 'HTTPS_PROXY' -class ApiURLs(Enum): +class _ApiURLs(Enum): """Api URL constants""" SDK_URL = 'sdk_url' EVENTS_URL = 'events_url' @@ -89,7 +89,7 @@ class MethodExceptionsAndLatencies(Enum): TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 'treatments_with_config_by_flag_sets' TRACK = 'track' -class LastSynchronizationConstants(Enum): +class _LastSynchronizationConstants(Enum): """Last sync constants""" LAST_SYNCHRONIZATIONS = 'lastSynchronizations' @@ -109,7 +109,7 @@ class SSESyncMode(Enum): STREAMING = 0 POLLING = 1 -class StreamingEventsConstant(Enum): +class _StreamingEventsConstant(Enum): """Storage types constant""" STREAMING_EVENTS = 'streamingEvents' @@ -427,7 +427,7 @@ def get_all(self): :rtype: dict """ with self._lock: - return {LastSynchronizationConstants.LAST_SYNCHRONIZATIONS.value: {HTTPExceptionsAndLatencies.SPLIT.value: self._split, HTTPExceptionsAndLatencies.SEGMENT.value: self._segment, HTTPExceptionsAndLatencies.IMPRESSION.value: self._impression, + return {_LastSynchronizationConstants.LAST_SYNCHRONIZATIONS.value: {HTTPExceptionsAndLatencies.SPLIT.value: self._split, HTTPExceptionsAndLatencies.SEGMENT.value: self._segment, HTTPExceptionsAndLatencies.IMPRESSION.value: self._impression, HTTPExceptionsAndLatencies.IMPRESSION_COUNT.value: self._impression_count, HTTPExceptionsAndLatencies.EVENT.value: self._event, HTTPExceptionsAndLatencies.TELEMETRY.value: self._telemetry, HTTPExceptionsAndLatencies.TOKEN.value: self._token} } @@ -761,7 +761,7 @@ def pop_streaming_events(self): with self._lock: streaming_events = self._streaming_events self._streaming_events = [] - return {StreamingEventsConstant.STREAMING_EVENTS.value: [{'e': streaming_event.type, 'd': streaming_event.data, + return {_StreamingEventsConstant.STREAMING_EVENTS.value: [{'e': streaming_event.type, 'd': streaming_event.data, 't': streaming_event.time} for streaming_event in streaming_events]} class TelemetryConfig(object): @@ -783,10 +783,10 @@ def _reset_all(self): self._operation_mode = None self._storage_type = None self._streaming_enabled = None - self._refresh_rate = {ConfigParams.SPLITS_REFRESH_RATE.value: 0, ConfigParams.SEGMENTS_REFRESH_RATE.value: 0, - ConfigParams.IMPRESSIONS_REFRESH_RATE.value: 0, ConfigParams.EVENTS_REFRESH_RATE.value: 0, ConfigParams.TELEMETRY_REFRESH_RATE.value: 0} - self._url_override = {ApiURLs.SDK_URL.value: False, ApiURLs.EVENTS_URL.value: False, ApiURLs.AUTH_URL.value: False, - ApiURLs.STREAMING_URL.value: False, ApiURLs.TELEMETRY_URL.value: False} + self._refresh_rate = {_ConfigParams.SPLITS_REFRESH_RATE.value: 0, _ConfigParams.SEGMENTS_REFRESH_RATE.value: 0, + _ConfigParams.IMPRESSIONS_REFRESH_RATE.value: 0, _ConfigParams.EVENTS_REFRESH_RATE.value: 0, _ConfigParams.TELEMETRY_REFRESH_RATE.value: 0} + self._url_override = {_ApiURLs.SDK_URL.value: False, _ApiURLs.EVENTS_URL.value: False, _ApiURLs.AUTH_URL.value: False, + _ApiURLs.STREAMING_URL.value: False, _ApiURLs.TELEMETRY_URL.value: False} self._impressions_queue_size = 0 self._events_queue_size = 0 self._impressions_mode = None @@ -819,17 +819,17 @@ def record_config(self, config, extra_config): :type config: dict """ with self._lock: - self._operation_mode = self._get_operation_mode(config[ConfigParams.OPERATION_MODE.value]) - self._storage_type = self._get_storage_type(config[ConfigParams.OPERATION_MODE.value], config[ConfigParams.STORAGE_TYPE.value]) - self._streaming_enabled = config[ConfigParams.STREAMING_ENABLED.value] + self._operation_mode = self._get_operation_mode(config[_ConfigParams.OPERATION_MODE.value]) + self._storage_type = self._get_storage_type(config[_ConfigParams.OPERATION_MODE.value], config[_ConfigParams.STORAGE_TYPE.value]) + self._streaming_enabled = config[_ConfigParams.STREAMING_ENABLED.value] self._refresh_rate = self._get_refresh_rates(config) self._url_override = self._get_url_overrides(extra_config) - self._impressions_queue_size = config[ConfigParams.IMPRESSIONS_QUEUE_SIZE.value] - self._events_queue_size = config[ConfigParams.EVENTS_QUEUE_SIZE.value] - self._impressions_mode = self._get_impressions_mode(config[ConfigParams.IMPRESSIONS_MODE.value]) - self._impression_listener = True if config[ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False + self._impressions_queue_size = config[_ConfigParams.IMPRESSIONS_QUEUE_SIZE.value] + self._events_queue_size = config[_ConfigParams.EVENTS_QUEUE_SIZE.value] + self._impressions_mode = self._get_impressions_mode(config[_ConfigParams.IMPRESSIONS_MODE.value]) + self._impression_listener = True if config[_ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False self._http_proxy = self._check_if_proxy_detected() - self._flag_sets = len(config[ConfigParams.FLAG_SETS.value]) if config[ConfigParams.FLAG_SETS.value] is not None else 0 + self._flag_sets = len(config[_ConfigParams.FLAG_SETS.value]) if config[_ConfigParams.FLAG_SETS.value] is not None else 0 def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): with self._lock: @@ -915,16 +915,16 @@ def get_stats(self): 'oM': self._operation_mode, 'sT': self._storage_type, 'sE': self._streaming_enabled, - 'rR': {'sp': self._refresh_rate[ConfigParams.SPLITS_REFRESH_RATE.value], - 'se': self._refresh_rate[ConfigParams.SEGMENTS_REFRESH_RATE.value], - 'im': self._refresh_rate[ConfigParams.IMPRESSIONS_REFRESH_RATE.value], - 'ev': self._refresh_rate[ConfigParams.EVENTS_REFRESH_RATE.value], - 'te': self._refresh_rate[ConfigParams.TELEMETRY_REFRESH_RATE.value]}, - 'uO': {'s': self._url_override[ApiURLs.SDK_URL.value], - 'e': self._url_override[ApiURLs.EVENTS_URL.value], - 'a': self._url_override[ApiURLs.AUTH_URL.value], - 'st': self._url_override[ApiURLs.STREAMING_URL.value], - 't': self._url_override[ApiURLs.TELEMETRY_URL.value]}, + 'rR': {'sp': self._refresh_rate[_ConfigParams.SPLITS_REFRESH_RATE.value], + 'se': self._refresh_rate[_ConfigParams.SEGMENTS_REFRESH_RATE.value], + 'im': self._refresh_rate[_ConfigParams.IMPRESSIONS_REFRESH_RATE.value], + 'ev': self._refresh_rate[_ConfigParams.EVENTS_REFRESH_RATE.value], + 'te': self._refresh_rate[_ConfigParams.TELEMETRY_REFRESH_RATE.value]}, + 'uO': {'s': self._url_override[_ApiURLs.SDK_URL.value], + 'e': self._url_override[_ApiURLs.EVENTS_URL.value], + 'a': self._url_override[_ApiURLs.AUTH_URL.value], + 'st': self._url_override[_ApiURLs.STREAMING_URL.value], + 't': self._url_override[_ApiURLs.TELEMETRY_URL.value]}, 'iQ': self._impressions_queue_size, 'eQ': self._events_queue_size, 'iM': self._impressions_mode, @@ -983,11 +983,11 @@ def _get_refresh_rates(self, config): """ with self._lock: return { - ConfigParams.SPLITS_REFRESH_RATE.value: config[ConfigParams.SPLITS_REFRESH_RATE.value], - ConfigParams.SEGMENTS_REFRESH_RATE.value: config[ConfigParams.SEGMENTS_REFRESH_RATE.value], - ConfigParams.IMPRESSIONS_REFRESH_RATE.value: config[ConfigParams.IMPRESSIONS_REFRESH_RATE.value], - ConfigParams.EVENTS_REFRESH_RATE.value: config[ConfigParams.EVENTS_REFRESH_RATE.value], - ConfigParams.TELEMETRY_REFRESH_RATE.value: config[ConfigParams.TELEMETRY_REFRESH_RATE.value] + _ConfigParams.SPLITS_REFRESH_RATE.value: config[_ConfigParams.SPLITS_REFRESH_RATE.value], + _ConfigParams.SEGMENTS_REFRESH_RATE.value: config[_ConfigParams.SEGMENTS_REFRESH_RATE.value], + _ConfigParams.IMPRESSIONS_REFRESH_RATE.value: config[_ConfigParams.IMPRESSIONS_REFRESH_RATE.value], + _ConfigParams.EVENTS_REFRESH_RATE.value: config[_ConfigParams.EVENTS_REFRESH_RATE.value], + _ConfigParams.TELEMETRY_REFRESH_RATE.value: config[_ConfigParams.TELEMETRY_REFRESH_RATE.value] } def _get_url_overrides(self, config): @@ -1002,11 +1002,11 @@ def _get_url_overrides(self, config): """ with self._lock: return { - ApiURLs.SDK_URL.value: True if ApiURLs.SDK_URL.value in config else False, - ApiURLs.EVENTS_URL.value: True if ApiURLs.EVENTS_URL.value in config else False, - ApiURLs.AUTH_URL.value: True if ApiURLs.AUTH_URL.value in config else False, - ApiURLs.STREAMING_URL.value: True if ApiURLs.STREAMING_URL.value in config else False, - ApiURLs.TELEMETRY_URL.value: True if ApiURLs.TELEMETRY_URL.value in config else False + _ApiURLs.SDK_URL.value: True if _ApiURLs.SDK_URL.value in config else False, + _ApiURLs.EVENTS_URL.value: True if _ApiURLs.EVENTS_URL.value in config else False, + _ApiURLs.AUTH_URL.value: True if _ApiURLs.AUTH_URL.value in config else False, + _ApiURLs.STREAMING_URL.value: True if _ApiURLs.STREAMING_URL.value in config else False, + _ApiURLs.TELEMETRY_URL.value: True if _ApiURLs.TELEMETRY_URL.value in config else False } def _get_impressions_mode(self, imp_mode): @@ -1036,6 +1036,6 @@ def _check_if_proxy_detected(self): """ with self._lock: for x in os.environ: - if x.upper() == ExtraConfig.HTTPS_PROXY_ENV.value: + if x.upper() == _ExtraConfig.HTTPS_PROXY_ENV.value: return True return False \ No newline at end of file diff --git a/tests/models/test_telemetry_model.py b/tests/models/test_telemetry_model.py index b33dbd4b..d5dda172 100644 --- a/tests/models/test_telemetry_model.py +++ b/tests/models/test_telemetry_model.py @@ -297,7 +297,8 @@ def test_telemetry_config(self): 'impressionsRefreshRate': 60, 'eventsPushRate': 60, 'metricsRefreshRate': 10, - 'storageType': None + 'storageType': None, + 'flagSetsFilter': None } telemetry_config.record_config(config, {}) assert(telemetry_config.get_stats() == {'oM': 0, From e6a78c8aa818cd07211d6f773b0642228413f564 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 5 Sep 2023 11:56:17 -0700 Subject: [PATCH 33/77] polish --- splitio/storage/redis.py | 6 +++--- tests/storage/test_redis.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 90d7ac13..2b2017b9 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -23,7 +23,7 @@ class RedisSplitStorage(SplitStorage): _TRAFFIC_TYPE_KEY = 'SPLITIO.trafficType.{traffic_type_name}' _SET_KEY = 'SPLITIO.set.{flag_set}' - def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, flag_sets=[]): + def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, config_flag_sets=[]): """ Class constructor. @@ -31,7 +31,7 @@ def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, :type redis_client: splitio.storage.adapters.redis.RedisAdapter """ self._redis = redis_client - self._flag_sets = flag_sets + self._config_flag_sets = config_flag_sets self._pipe = self._redis.pipeline if enable_caching: self.get = add_cache(lambda *p, **_: p[0], max_age)(self.get) @@ -107,7 +107,7 @@ def get_feature_flags_by_sets(self, flag_sets): try: sets_to_fetch = [] for flag_set in flag_sets: - if flag_set not in self._flag_sets and len(self._flag_sets) > 0: + if flag_set not in self._config_flag_sets and len(self._config_flag_sets) > 0: _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) continue sets_to_fetch.append(flag_set) diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 22c40d7a..7ee00ca8 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -177,15 +177,15 @@ def test_flag_sets(self, mocker): """Test Flag sets scenarios.""" adapter = build({}) storage = RedisSplitStorage(adapter, True, 1) - assert storage._flag_sets == [] + assert storage._config_flag_sets == [] assert sorted(storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] - storage._flag_sets = ['set2', 'set3'] + storage._config_flag_sets = ['set2', 'set3'] assert storage.get_feature_flags_by_sets(['set1']) == [] assert sorted(storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) - assert storage2._flag_sets == ['set2', 'set3'] + assert storage2._config_flag_sets == ['set2', 'set3'] class RedisSegmentStorageTests(object): """Redis segment storage test cases.""" From c86273a9d17ef5cfc6d2eb73d16a15fd0fb3a658 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 5 Sep 2023 15:23:41 -0700 Subject: [PATCH 34/77] Updated push.splitworker and sync.split --- splitio/push/splitworker.py | 15 ++++--- splitio/sync/split.py | 23 +++------- splitio/util/storage_helper.py | 49 ++++++++++++++++++++ tests/push/test_split_worker.py | 53 ++++++++++------------ tests/util/test_storage_helper.py | 75 +++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 52 deletions(-) create mode 100644 splitio/util/storage_helper.py create mode 100644 tests/util/test_storage_helper.py diff --git a/splitio/push/splitworker.py b/splitio/push/splitworker.py index 96654040..cc48cd7b 100644 --- a/splitio/push/splitworker.py +++ b/splitio/push/splitworker.py @@ -10,7 +10,7 @@ from splitio.models.splits import from_raw, Status from splitio.models.telemetry import UpdateFromSSE from splitio.push.parser import UpdateType - +from splitio.util.storage_helper import update_feature_flag_storage _LOGGER = logging.getLogger(__name__) @@ -88,17 +88,20 @@ def _run(self): try: if self._check_instant_ff_update(event): try: - new_split = from_raw(json.loads(self._get_feature_flag_definition(event))) + new_feature_flag = from_raw(json.loads(self._get_feature_flag_definition(event))) + segment_list = update_feature_flag_storage(self._feature_flag_storage, [new_feature_flag], event.change_number) + ''' if new_split.status == Status.ACTIVE: self._feature_flag_storage.put(new_split) _LOGGER.debug('Feature flag %s is updated', new_split.name) - for segment_name in new_split.get_segment_names(): - if self._segment_storage.get(segment_name) is None: - _LOGGER.debug('Fetching new segment %s', segment_name) - self._segment_handler(segment_name, event.change_number) else: self._feature_flag_storage.remove(new_split.name) self._feature_flag_storage.set_change_number(event.change_number) + ''' + for segment_name in segment_list: + if self._segment_storage.get(segment_name) is None: + _LOGGER.debug('Fetching new segment %s', segment_name) + self._segment_handler(segment_name, event.change_number) self._telemetry_runtime_producer.record_update_from_sse(UpdateFromSSE.SPLIT_UPDATE) continue except Exception as e: diff --git a/splitio/sync/split.py b/splitio/sync/split.py index c904d9d1..eadd75b4 100644 --- a/splitio/sync/split.py +++ b/splitio/sync/split.py @@ -13,6 +13,7 @@ from splitio.models import splits from splitio.util.backoff import Backoff from splitio.util.time import get_current_epoch_time_ms +from splitio.util.storage_helper import update_feature_flag_storage from splitio.sync import util _LEGACY_COMMENT_LINE_RE = re.compile(r'^#.*$') @@ -79,7 +80,10 @@ def _fetch_until(self, fetch_options, till=None): _LOGGER.error('Exception raised while fetching feature flags') _LOGGER.debug('Exception information: ', exc_info=True) raise exc - + fetched_feature_flags = [] + [fetched_feature_flags.append(splits.from_raw(feature_flag)) for feature_flag in feature_flag_changes.get('splits', [])] + segment_list = update_feature_flag_storage(self._feature_flag_storage, fetched_feature_flags, feature_flag_changes['till']) + ''' to_add = [] to_delete = [] for feature_flag in feature_flag_changes.get('splits', []): @@ -93,25 +97,10 @@ def _fetch_until(self, fetch_options, till=None): to_delete.append(feature_flag['name']) self._feature_flag_storage.update(to_add, to_delete, feature_flag_changes['till']) + ''' if feature_flag_changes['till'] == feature_flag_changes['since']: return feature_flag_changes['till'], segment_list - def _check_flag_sets(self, feature_flag): - """ - Check all flag sets in a feature flag, return True if any of sets exist in storage - - :param feature_flag: Flag set to validate. - :type feature_flag: json - - :return: True if any of its flag_set exist. False otherwise. - :rtype: bool - """ - for flag_set in feature_flag['sets']: - if self._feature_flag_storage.is_flag_set_exist(flag_set): - return True - return False - - def _attempt_feature_flag_sync(self, fetch_options, till=None): """ Hit endpoint, update storage and return True if sync is complete. diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py new file mode 100644 index 00000000..fb07e70c --- /dev/null +++ b/splitio/util/storage_helper.py @@ -0,0 +1,49 @@ +"""Storage Helper.""" + +from splitio.models import splits + +def update_feature_flag_storage(feature_flag_storage, feature_flags, change_number): + """ + Update feature flag storage from given list of feature flags while checking the flag set logic + + :param feature_flag_storage: Feature flag storage instance + :type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage + :param feature_flag: Feature flag instance to validate. + :type feature_flag: splitio.models.splits.Split + :param: last change number + :type: int + + :return: segments list from feature flags list + :rtype: list(str) + """ + segment_list = set() + to_add = [] + to_delete = [] + for feature_flag in feature_flags: + if (feature_flag_storage.config_flag_sets_used == 0 and feature_flag.status == splits.Status.ACTIVE) or \ + (feature_flag.status == splits.Status.ACTIVE and _check_flag_sets(feature_flag_storage, feature_flag)): + to_add.append(feature_flag) + segment_list.update(set(feature_flag.get_segment_names())) + else: + if feature_flag_storage.get(feature_flag.name) is not None: + to_delete.append(feature_flag.name) + + feature_flag_storage.update(to_add, to_delete, change_number) + return segment_list + +def _check_flag_sets(feature_flag_storage, feature_flag): + """ + Check all flag sets in a feature flag, return True if any of sets exist in storage + + :param feature_flag_storage: Feature flag storage instance + :type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage + :param feature_flag: Feature flag instance to validate. + :type feature_flag: splitio.models.splits.Split + + :return: True if any of its flag_set exist. False otherwise. + :rtype: bool + """ + for flag_set in feature_flag.sets: + if feature_flag_storage.is_flag_set_exist(flag_set): + return True + return False diff --git a/tests/push/test_split_worker.py b/tests/push/test_split_worker.py index 09ede0bb..23831bc5 100644 --- a/tests/push/test_split_worker.py +++ b/tests/push/test_split_worker.py @@ -55,18 +55,13 @@ def test_handler(self, mocker): def get_change_number(): return 2345 - - self._feature_flag = None - def put(feature_flag): - self._feature_flag = feature_flag + split_worker._feature_flag_storage.get_change_number = get_change_number self.new_change_number = 0 - def set_change_number(new_change_number): - self.new_change_number = new_change_number - - split_worker._feature_flag_storage.get_change_number = get_change_number - split_worker._feature_flag_storage.set_change_number = set_change_number - split_worker._feature_flag_storage.put = put + def update(to_add, to_delete, change_number): + self.new_change_number = change_number + split_worker._feature_flag_storage.update = update + split_worker._feature_flag_storage.config_flag_sets_used = 0 # should call the handler q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 12345, "{}", 1)) @@ -98,45 +93,45 @@ def test_compression(self, mocker): split_worker.start() def get_change_number(): return 2345 - - def put(feature_flag): - self._feature_flag = feature_flag - - def remove(feature_flag): - self._feature_flag_delete = feature_flag - split_worker._feature_flag_storage.get_change_number = get_change_number - split_worker._feature_flag_storage.put = put - split_worker._feature_flag_storage.remove = remove + + self._feature_flag_added = None + self._feature_flag_deleted = None + def update(feature_flag_add, feature_flag_delete, change_number): + self._feature_flag_added = feature_flag_add + self._feature_flag_deleted = feature_flag_delete + split_worker._feature_flag_storage.update = update + split_worker._feature_flag_storage.config_flag_sets_used = 0 # compression 0 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiIzM2VhZmE1MC0xYTY1LTExZWQtOTBkZi1mYTMwZDk2OTA0NDUiLCJuYW1lIjoiYmlsYWxfc3BsaXQiLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOi0xMzY0MTE5MjgyLCJzZWVkIjotNjA1OTM4ODQzLCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib2ZmIiwiY2hhbmdlTnVtYmVyIjoxNjg0MzQwOTA4NDc1LCJhbGdvIjoyLCJjb25maWd1cmF0aW9ucyI6e30sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoidXNlciJ9LCJtYXRjaGVyVHlwZSI6IklOX1NFR01FTlQiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6eyJzZWdtZW50TmFtZSI6ImJpbGFsX3NlZ21lbnQifX1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjEwMH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgYmlsYWxfc2VnbWVudCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfV0sImxhYmVsIjoiZGVmYXVsdCBydWxlIn1dfQ==', 0)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' +# pytest.set_trace() + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 1 # compression 2 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eJzEUtFq20AQ/JUwz2c4WZZr3ZupTQh1FKjcQinGrKU95cjpZE6nh9To34ssJ3FNX0sfd3Zm53b2TgietDbF9vXIGdUMha5lDwFTQiGOmTQlchLRPJlEEZeTVJZ6oimWZTpP5WyWQMCNyoOxZPft0ZoA8TZ5aW1TUDCNg4qk/AueM5dQkyiez6IonS6mAu0IzWWSxovFLBZoA4WuhcLy8/bh+xoCL8bagaXJtixQsqbOhq1nCjW7AIVGawgUz+Qqzrr6wB4qmi9m00/JIk7TZCpAtmqgpgJF47SpOn9+UQt16s9YaS71z9NHOYQFha9Pm83Tty0EagrFM/t733RHqIFZH4wb7LDMVh+Ecc4Lv+ZsuQiNH8hXF3hLv39XXNCHbJ+v7x/X2eDmuKLA74sPihVr47jMuRpWfxy1Kwo0GLQjmv1xpBFD3+96gSP5cLVouM7QQaA1vxhK9uKmd853bEZS9jsBSwe2UDDu7mJxd2Mo/muQy81m/2X9I7+N8R/FcPmUd76zjH7X/w4AAP//90glTw==', 2)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 2 # compression 1 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'H4sIAAkVZWQC/8WST0+DQBDFv0qzZ0ig/BF6a2xjGismUk2MaZopzOKmy9Isy0EbvrtDwbY2Xo233Tdv5se85cCMBs5FtvrYYwIlsglratTMYiKns+chcAgc24UwsF0Xczt2cm5z8Jw8DmPH9wPyqr5zKyTITb2XwpA4TJ5KWWVgRKXYxHWcX/QUkVi264W+68bjaGyxupdCJ4i9KPI9UgyYpibI9Ha1eJnT/J2QsnNxkDVaLEcOjTQrjWBKVIasFefky95BFZg05Zb2mrhh5I9vgsiL44BAIIuKTeiQVYqLotHHLyLOoT1quRjub4fztQuLxj89LpePzytClGCyd9R3umr21ErOcitUh2PTZHY29HN2+JGixMxUujNfvMB3+u2pY1AXySad3z3Mk46msACDp8W7jhly4uUpFt3qD33vDAx0gLpXkx+P1GusbdcE24M2F4uaywwVEWvxSa1Oa13Vjvn2RXradm0xCVuUVBJqNCBGV0DrX4OcLpeb+/lreh3jH8Uw/JQj3UhkxPgCCurdEnADAAA=', 1)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 3 # should call delete split - self._feature_flag = None - self._feature_flag_delete = None + self._feature_flag_added = None + self._feature_flag_deleted = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eyJ0cmFmZmljVHlwZU5hbWUiOiAidXNlciIsICJpZCI6ICIzM2VhZmE1MC0xYTY1LTExZWQtOTBkZi1mYTMwZDk2OTA0NDUiLCAibmFtZSI6ICJiaWxhbF9zcGxpdCIsICJ0cmFmZmljQWxsb2NhdGlvbiI6IDEwMCwgInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6IC0xMzY0MTE5MjgyLCAic2VlZCI6IC02MDU5Mzg4NDMsICJzdGF0dXMiOiAiQVJDSElWRUQiLCAia2lsbGVkIjogZmFsc2UsICJkZWZhdWx0VHJlYXRtZW50IjogIm9mZiIsICJjaGFuZ2VOdW1iZXIiOiAxNjg0Mjc1ODM5OTUyLCAiYWxnbyI6IDIsICJjb25maWd1cmF0aW9ucyI6IHt9LCAiY29uZGl0aW9ucyI6IFt7ImNvbmRpdGlvblR5cGUiOiAiUk9MTE9VVCIsICJtYXRjaGVyR3JvdXAiOiB7ImNvbWJpbmVyIjogIkFORCIsICJtYXRjaGVycyI6IFt7ImtleVNlbGVjdG9yIjogeyJ0cmFmZmljVHlwZSI6ICJ1c2VyIn0sICJtYXRjaGVyVHlwZSI6ICJJTl9TRUdNRU5UIiwgIm5lZ2F0ZSI6IGZhbHNlLCAidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOiB7InNlZ21lbnROYW1lIjogImJpbGFsX3NlZ21lbnQifX1dfSwgInBhcnRpdGlvbnMiOiBbeyJ0cmVhdG1lbnQiOiAib24iLCAic2l6ZSI6IDB9LCB7InRyZWF0bWVudCI6ICJvZmYiLCAic2l6ZSI6IDEwMH1dLCAibGFiZWwiOiAiaW4gc2VnbWVudCBiaWxhbF9zZWdtZW50In0sIHsiY29uZGl0aW9uVHlwZSI6ICJST0xMT1VUIiwgIm1hdGNoZXJHcm91cCI6IHsiY29tYmluZXIiOiAiQU5EIiwgIm1hdGNoZXJzIjogW3sia2V5U2VsZWN0b3IiOiB7InRyYWZmaWNUeXBlIjogInVzZXIifSwgIm1hdGNoZXJUeXBlIjogIkFMTF9LRVlTIiwgIm5lZ2F0ZSI6IGZhbHNlfV19LCAicGFydGl0aW9ucyI6IFt7InRyZWF0bWVudCI6ICJvbiIsICJzaXplIjogMH0sIHsidHJlYXRtZW50IjogIm9mZiIsICJzaXplIjogMTAwfV0sICJsYWJlbCI6ICJkZWZhdWx0IHJ1bGUifV19', 0)) time.sleep(0.1) - assert self._feature_flag_delete == 'bilal_split' - assert self._feature_flag == None + assert self._feature_flag_deleted[0] == 'bilal_split' + self._feature_flag_added = None def test_edge_cases(self, mocker): q = queue.Queue() diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py new file mode 100644 index 00000000..e6537580 --- /dev/null +++ b/tests/util/test_storage_helper.py @@ -0,0 +1,75 @@ +"""Storage Helper tests.""" + +from splitio.util.storage_helper import update_feature_flag_storage +from splitio.storage.inmemmory import InMemorySplitStorage +from splitio.models import splits +from tests.sync.test_splits_synchronizer import splits as split_sample + +class StorageHelperTests(object): + + def test_helper_scenarios(self, mocker): + storage = mocker.Mock(spec=InMemorySplitStorage) + split = splits.from_raw(split_sample[0]) + + self.added = [] + self.deleted = [] + self.change_number = 0 + def update(to_add, to_delete, change_number): + self.added = to_add + self.deleted = to_delete + self.change_number = change_number + storage.update = update + + def is_flag_set_exist(flag_set): + return False + storage.is_flag_set_exist = is_flag_set_exist + + storage.config_flag_sets_used = 0 + update_feature_flag_storage(storage, [split], 123) + assert self.added[0] == split + assert self.deleted == [] + assert self.change_number == 123 + + storage.config_flag_sets_used = 2 + update_feature_flag_storage(storage, [split], 123) + assert self.added == [] + assert self.deleted[0] == split.name + + def is_flag_set_exist2(flag_set): + return True + storage.is_flag_set_exist = is_flag_set_exist2 + update_feature_flag_storage(storage, [split], 123) + assert self.added[0] == split + assert self.deleted == [] + + split_json = split_sample[0] + split_json['conditions'].append({ + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": False, + "userDefinedSegmentMatcherData": { + "segmentName": "segment1" + }, + "whitelistMatcherData": None + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 30 + }, + { + "treatment": "off", + "size": 70 + } + ] + } + ) + + split = splits.from_raw(split_json) + storage.config_flag_sets_used = 0 + assert update_feature_flag_storage(storage, [split], 123) == {'segment1'} From 823e5b303af377f3b92090e3e1c6e88521caf2b3 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 6 Sep 2023 08:35:23 -0700 Subject: [PATCH 35/77] polishing --- splitio/storage/redis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 2b2017b9..4a2cf349 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -122,7 +122,10 @@ def get_feature_flags_by_sets(self, flag_sets): _LOGGER.debug("Fetchting Feature flags by set [%s] from redis" % (keys)) _LOGGER.debug(result_sets) to_return = set() - [to_return.update(result_set) for result_set in result_sets] + for result_set in result_sets: + if isinstance(result_set, set) and len(result_set) > 0: + to_return.update(result_set) + return list(to_return) except RedisAdapterException: _LOGGER.error('Error fetching feature flag from storage') From 9a63a90275c1f7ed97d2ff522120ea099135361f Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 6 Sep 2023 09:01:55 -0700 Subject: [PATCH 36/77] polish --- splitio/storage/redis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 4a2cf349..2f118aac 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -115,7 +115,7 @@ def get_feature_flags_by_sets(self, flag_sets): if sets_to_fetch == []: return [] - keys = [self._get_set_key(feature_flag_name) for feature_flag_name in sets_to_fetch] + keys = [self._get_set_key(flag_set) for flag_set in sets_to_fetch] pipe = self._pipe() [pipe.smembers(key) for key in keys] result_sets = pipe.execute() From fd94eea75d5a8583b5c6183263c3fb497b6b47f7 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 6 Sep 2023 09:35:15 -0700 Subject: [PATCH 37/77] added helper for checking flag sets in config flag sets --- splitio/storage/redis.py | 5 +- splitio/util/storage_helper.py | 73 ++++++++++++++++++++++++ tests/util/test_storage_helper.py | 95 +++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 splitio/util/storage_helper.py create mode 100644 tests/util/test_storage_helper.py diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 2f118aac..b7c685d4 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -10,6 +10,7 @@ ImpressionPipelinedStorage, TelemetryStorage from splitio.storage.adapters.redis import RedisAdapterException from splitio.storage.adapters.cache_trait import decorate as add_cache, DEFAULT_MAX_AGE +from splitio.util.storage_helper import get_valid_flag_sets _LOGGER = logging.getLogger(__name__) @@ -105,13 +106,15 @@ def get_feature_flags_by_sets(self, flag_sets): :rtype: listt(str) """ try: + ''' sets_to_fetch = [] for flag_set in flag_sets: if flag_set not in self._config_flag_sets and len(self._config_flag_sets) > 0: _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) continue sets_to_fetch.append(flag_set) - + ''' + sets_to_fetch = get_valid_flag_sets(flag_sets, self._config_flag_sets) if sets_to_fetch == []: return [] diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py new file mode 100644 index 00000000..e303ccef --- /dev/null +++ b/splitio/util/storage_helper.py @@ -0,0 +1,73 @@ +"""Storage Helper.""" +import logging + +from splitio.models import splits + +_LOGGER = logging.getLogger(__name__) + +def update_feature_flag_storage(feature_flag_storage, feature_flags, change_number): + """ + Update feature flag storage from given list of feature flags while checking the flag set logic + + :param feature_flag_storage: Feature flag storage instance + :type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage + :param feature_flag: Feature flag instance to validate. + :type feature_flag: splitio.models.splits.Split + :param: last change number + :type: int + + :return: segments list from feature flags list + :rtype: list(str) + """ + segment_list = set() + to_add = [] + to_delete = [] + for feature_flag in feature_flags: + if (feature_flag_storage.config_flag_sets_used == 0 and feature_flag.status == splits.Status.ACTIVE) or \ + (feature_flag.status == splits.Status.ACTIVE and _check_flag_sets(feature_flag_storage, feature_flag)): + to_add.append(feature_flag) + segment_list.update(set(feature_flag.get_segment_names())) + else: + if feature_flag_storage.get(feature_flag.name) is not None: + to_delete.append(feature_flag.name) + + feature_flag_storage.update(to_add, to_delete, change_number) + return segment_list + +def _check_flag_sets(feature_flag_storage, feature_flag): + """ + Check all flag sets in a feature flag, return True if any of sets exist in storage + + :param feature_flag_storage: Feature flag storage instance + :type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage + :param feature_flag: Feature flag instance to validate. + :type feature_flag: splitio.models.splits.Split + + :return: True if any of its flag_set exist. False otherwise. + :rtype: bool + """ + for flag_set in feature_flag.sets: + if feature_flag_storage.is_flag_set_exist(flag_set): + return True + return False + +def get_valid_flag_sets(flag_sets, config_flag_sets): + """ + 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 + + :param flag_sets: Flag sets array + :type flag_sets: list(str) + :param config_flag_sets: Config flag sets array + :type config_flag_sets: list(str) + + :return: array of flag sets + :rtype: list(str) + """ + sets_to_fetch = [] + for flag_set in flag_sets: + if flag_set not in config_flag_sets and len(config_flag_sets) > 0: + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) + continue + sets_to_fetch.append(flag_set) + + return sets_to_fetch diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py new file mode 100644 index 00000000..c8df71bb --- /dev/null +++ b/tests/util/test_storage_helper.py @@ -0,0 +1,95 @@ +"""Storage Helper tests.""" + +from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets +from splitio.storage.inmemmory import InMemorySplitStorage +from splitio.models import splits +from tests.sync.test_splits_synchronizer import splits as split_sample + +class StorageHelperTests(object): + + def test_update_feature_flag_storage(self, mocker): + storage = mocker.Mock(spec=InMemorySplitStorage) + split = splits.from_raw(split_sample[0]) + + self.added = [] + self.deleted = [] + self.change_number = 0 + def update(to_add, to_delete, change_number): + self.added = to_add + self.deleted = to_delete + self.change_number = change_number + storage.update = update + + def is_flag_set_exist(flag_set): + return False + storage.is_flag_set_exist = is_flag_set_exist + + storage.config_flag_sets_used = 0 + update_feature_flag_storage(storage, [split], 123) + assert self.added[0] == split + assert self.deleted == [] + assert self.change_number == 123 + + storage.config_flag_sets_used = 2 + update_feature_flag_storage(storage, [split], 123) + assert self.added == [] + assert self.deleted[0] == split.name + + def is_flag_set_exist2(flag_set): + return True + storage.is_flag_set_exist = is_flag_set_exist2 + update_feature_flag_storage(storage, [split], 123) + assert self.added[0] == split + assert self.deleted == [] + + split_json = split_sample[0] + split_json['conditions'].append({ + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": False, + "userDefinedSegmentMatcherData": { + "segmentName": "segment1" + }, + "whitelistMatcherData": None + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 30 + }, + { + "treatment": "off", + "size": 70 + } + ] + } + ) + + split = splits.from_raw(split_json) + storage.config_flag_sets_used = 0 + assert update_feature_flag_storage(storage, [split], 123) == {'segment1'} + + def test_get_valid_flag_sets(self, mocker): + flag_sets = ['set1', 'set2'] + config_flag_sets = [] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1', 'set2'] + + config_flag_sets = ['set1'] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1'] + + flag_sets = ['set2', 'set3'] + config_flag_sets = ['set1', 'set2'] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set2'] + + flag_sets = ['set3', 'set4'] + config_flag_sets = ['set1', 'set2'] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] + + flag_sets = [] + config_flag_sets = ['set1', 'set2'] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] From 4b157e099b462d3ca8e6dbb4ffd7cb7e67cd02e1 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 6 Sep 2023 09:50:12 -0700 Subject: [PATCH 38/77] added helper function to combine valid sets into one set --- splitio/storage/redis.py | 9 ++------- splitio/util/storage_helper.py | 16 ++++++++++++++++ tests/util/test_storage_helper.py | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index b7c685d4..b703d8f7 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -10,7 +10,7 @@ ImpressionPipelinedStorage, TelemetryStorage from splitio.storage.adapters.redis import RedisAdapterException from splitio.storage.adapters.cache_trait import decorate as add_cache, DEFAULT_MAX_AGE -from splitio.util.storage_helper import get_valid_flag_sets +from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets _LOGGER = logging.getLogger(__name__) @@ -124,12 +124,7 @@ def get_feature_flags_by_sets(self, flag_sets): result_sets = pipe.execute() _LOGGER.debug("Fetchting Feature flags by set [%s] from redis" % (keys)) _LOGGER.debug(result_sets) - to_return = set() - for result_set in result_sets: - if isinstance(result_set, set) and len(result_set) > 0: - to_return.update(result_set) - - return list(to_return) + return list(combine_valid_flag_sets(result_sets)) except RedisAdapterException: _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index e303ccef..61e15fc9 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -71,3 +71,19 @@ def get_valid_flag_sets(flag_sets, config_flag_sets): sets_to_fetch.append(flag_set) return sets_to_fetch + +def combine_valid_flag_sets(result_sets): + """ + Check each flag set in given array of sets, combine all flag sets in one unique set + + :param result_sets: Flag sets set + :type flag_sets: list(set) + + :return: flag sets set + :rtype: set + """ + to_return = set() + for result_set in result_sets: + if isinstance(result_set, set) and len(result_set) > 0: + to_return.update(result_set) + return to_return diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index c8df71bb..8c148942 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -1,6 +1,6 @@ """Storage Helper tests.""" -from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets +from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets from splitio.storage.inmemmory import InMemorySplitStorage from splitio.models import splits from tests.sync.test_splits_synchronizer import splits as split_sample @@ -74,7 +74,7 @@ def is_flag_set_exist2(flag_set): storage.config_flag_sets_used = 0 assert update_feature_flag_storage(storage, [split], 123) == {'segment1'} - def test_get_valid_flag_sets(self, mocker): + def test_get_valid_flag_sets(self): flag_sets = ['set1', 'set2'] config_flag_sets = [] assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1', 'set2'] @@ -93,3 +93,13 @@ def test_get_valid_flag_sets(self, mocker): flag_sets = [] config_flag_sets = ['set1', 'set2'] assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] + + def test_combine_valid_flag_sets(self): + results_set = [{'set1', 'set2'}, {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set1', 'set2', 'set3'} + + results_set = [{}, {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} + + results_set = ['set1', {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} From a5a872e79da3f07292fc264fd7a5b1873a1a8bad Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 6 Sep 2023 10:22:53 -0700 Subject: [PATCH 39/77] polish --- splitio/storage/redis.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index b703d8f7..d39d6054 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -106,14 +106,6 @@ def get_feature_flags_by_sets(self, flag_sets): :rtype: listt(str) """ try: - ''' - sets_to_fetch = [] - for flag_set in flag_sets: - if flag_set not in self._config_flag_sets and len(self._config_flag_sets) > 0: - _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) - continue - sets_to_fetch.append(flag_set) - ''' sets_to_fetch = get_valid_flag_sets(flag_sets, self._config_flag_sets) if sets_to_fetch == []: return [] From f01618e9205490a598b596d07d4e1873e21a9889 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 6 Sep 2023 10:24:14 -0700 Subject: [PATCH 40/77] updted storage.pluggable class --- splitio/client/factory.py | 2 +- splitio/storage/pluggable.py | 106 ++++++++++++++++++++------------ tests/storage/test_pluggable.py | 22 +++---- 3 files changed, 79 insertions(+), 51 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index fede6ad0..cc15b0eb 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -523,7 +523,7 @@ def _build_pluggable_factory(api_key, cfg): pluggable_adapter = cfg.get('storageWrapper') storage_prefix = cfg.get('storagePrefix') storages = { - 'splits': PluggableSplitStorage(pluggable_adapter, storage_prefix), + 'splits': PluggableSplitStorage(pluggable_adapter, storage_prefix, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': PluggableSegmentStorage(pluggable_adapter, storage_prefix), 'impressions': PluggableImpressionsStorage(pluggable_adapter, sdk_metadata, storage_prefix), 'events': PluggableEventsStorage(pluggable_adapter, sdk_metadata, storage_prefix), diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 2be0f6d3..2ad79f7b 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -8,15 +8,16 @@ from splitio.models.impressions import Impression from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MAX_TAGS, get_latency_bucket_index from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage +from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets _LOGGER = logging.getLogger(__name__) class PluggableSplitStorage(SplitStorage): - """InMemory implementation of a split storage.""" + """InMemory implementation of feature flag storage.""" - _SPLIT_NAME_LENGTH = 12 + _FEATURE_FLAG_NAME_LENGTH = 19 - def __init__(self, pluggable_adapter, prefix=None): + def __init__(self, pluggable_adapter, prefix=None, config_flag_sets=[]): """ Class constructor. @@ -26,51 +27,78 @@ def __init__(self, pluggable_adapter, prefix=None): :type prefix: str """ self._pluggable_adapter = pluggable_adapter - self._prefix = "SPLITIO.split.{split_name}" + self._config_flag_sets = config_flag_sets + self._prefix = "SPLITIO.split.{feature_flag_name}" self._traffic_type_prefix = "SPLITIO.trafficType.{traffic_type_name}" - self._split_till_prefix = "SPLITIO.splits.till" + self._feature_flag_till_prefix = "SPLITIO.splits.till" + self._feature_flag_set_prefix = 'SPLITIO.set.{flag_set}' if prefix is not None: self._prefix = prefix + "." + self._prefix self._traffic_type_prefix = prefix + "." + self._traffic_type_prefix - self._split_till_prefix = prefix + "." + self._split_till_prefix + self._feature_flag_till_prefix = prefix + "." + self._feature_flag_till_prefix + self._feature_flag_set_prefix = prefix + "." + self._feature_flag_till_prefix - def get(self, split_name): + def get(self, feature_flag_name): """ - Retrieve a split. + Retrieve a feature flag. - :param split_name: Name of the feature to fetch. - :type split_name: str + :param feature_flag_name: Name of the feature to fetch. + :type feature_flag_name: str :rtype: splitio.models.splits.Split """ try: - split = self._pluggable_adapter.get(self._prefix.format(split_name=split_name)) - if not split: + feature_flag = self._pluggable_adapter.get(self._prefix.format(feature_flag_name=feature_flag_name)) + if not feature_flag: return None - return splits.from_raw(split) + return splits.from_raw(feature_flag) except Exception: - _LOGGER.error('Error getting split from storage') + _LOGGER.error('Error getting feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def fetch_many(self, split_names): + def fetch_many(self, feature_flag_names): """ - Retrieve splits. + Retrieve feature flags. - :param split_names: Names of the features to fetch. - :type split_name: list(str) + :param feature_flag_names: Names of the features to fetch. + :type feature_flag_name: list(str) :return: A dict with split objects parsed from queue. :rtype: dict(split_name, splitio.models.splits.Split) """ try: - prefix_added = [self._prefix.format(split_name=split_name) for split_name in split_names] - return {split['name']: splits.from_raw(split) for split in self._pluggable_adapter.get_many(prefix_added)} + prefix_added = [self._prefix.format(feature_flag_name=feature_flag_name) for feature_flag_name in feature_flag_names] + return {feature_flag['name']: splits.from_raw(feature_flag) for feature_flag in self._pluggable_adapter.get_many(prefix_added)} except Exception: - _LOGGER.error('Error getting split from storage') + _LOGGER.error('Error getting feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None + def get_feature_flags_by_sets(self, flag_sets): + """ + Retrieve feature flags by flag set. + + :param flag_set: Names of the flag set to fetch. + :type flag_set: str + + :return: Feature flag names that are tagged with the flag set + :rtype: listt(str) + """ + try: + sets_to_fetch = get_valid_flag_sets(flag_sets, self._config_flag_sets) + if sets_to_fetch == []: + return [] + + keys = [self._prefix(feature_flag_name) for feature_flag_name in sets_to_fetch] + result_sets = self._pluggable_adapter.get_many(keys) + return list(combine_valid_flag_sets(result_sets)) + except Exception: + _LOGGER.error('Error fetching feature flag from storage') + _LOGGER.debug('Error: ', exc_info=True) + return None + + # TODO: To be added when producer mode is supported # def put_many(self, splits, change_number): # """ @@ -127,14 +155,14 @@ def update(self, to_add, to_delete, new_change_number): def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ try: - return self._pluggable_adapter.get(self._split_till_prefix) + return self._pluggable_adapter.get(self._feature_flag_till_prefix) except Exception: - _LOGGER.error('Error getting change number in split storage') + _LOGGER.error('Error getting change number in feature flag storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -156,35 +184,35 @@ def get_change_number(self): def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ try: - return [split.name for split in self.get_all()] + return [feature_flag.name for feature_flag in self.get_all()] except Exception: - _LOGGER.error('Error getting split names from storage') + _LOGGER.error('Error getting feature flag names from storage') _LOGGER.debug('Error: ', exc_info=True) return None def get_all(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ try: - return [splits.from_raw(self._pluggable_adapter.get(key)) for key in self._pluggable_adapter.get_keys_by_prefix(self._prefix[:-self._SPLIT_NAME_LENGTH])] + return [splits.from_raw(self._pluggable_adapter.get(key)) for key in self._pluggable_adapter.get_keys_by_prefix(self._prefix[:-self._FEATURE_FLAG_NAME_LENGTH])] except Exception: - _LOGGER.error('Error getting split keys from storage') + _LOGGER.error('Error getting feature flag keys from storage') _LOGGER.debug('Error: ', exc_info=True) return None def traffic_type_exists(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -195,7 +223,7 @@ def traffic_type_exists(self, traffic_type_name): try: return self._pluggable_adapter.get(self._traffic_type_prefix.format(traffic_type_name=traffic_type_name)) != None except Exception: - _LOGGER.error('Error getting split info from storage') + _LOGGER.error('Error getting traffic type info from storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -264,21 +292,21 @@ def kill_locally(self, split_name, default_treatment, change_number): def get_all_splits(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ try: return self.get_all() except Exception: - _LOGGER.error('Error fetching splits from storage') + _LOGGER.error('Error fetching feature flags from storage') _LOGGER.debug('Error: ', exc_info=True) return None def is_valid_traffic_type(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -289,7 +317,7 @@ def is_valid_traffic_type(self, traffic_type_name): try: return self.traffic_type_exists(traffic_type_name) except Exception: - _LOGGER.error('Error getting split info from storage') + _LOGGER.error('Error getting traffic type info from storage') _LOGGER.debug('Error: ', exc_info=True) return None diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index 38a5b511..50db52f0 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -138,9 +138,9 @@ def test_init(self): prefix = 'myprefix.' else: prefix = '' - assert(pluggable_split_storage._prefix == prefix + "SPLITIO.split.{split_name}") + assert(pluggable_split_storage._prefix == prefix + "SPLITIO.split.{feature_flag_name}") assert(pluggable_split_storage._traffic_type_prefix == prefix + "SPLITIO.trafficType.{traffic_type_name}") - assert(pluggable_split_storage._split_till_prefix == prefix + "SPLITIO.splits.till") + assert(pluggable_split_storage._feature_flag_till_prefix == prefix + "SPLITIO.splits.till") # TODO: To be added when producer mode is aupported # def test_put_many(self): @@ -163,10 +163,10 @@ def test_get(self): pluggable_split_storage = PluggableSplitStorage(self.mock_adapter, prefix=sprefix) split1 = splits.from_raw(splits_json['splitChange1_2']['splits'][0]) - split_name = splits_json['splitChange1_2']['splits'][0]['name'] + feature_flag_name = splits_json['splitChange1_2']['splits'][0]['name'] - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split_name), split1.to_json()) - assert(pluggable_split_storage.get(split_name).to_json() == splits.from_raw(splits_json['splitChange1_2']['splits'][0]).to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=feature_flag_name), split1.to_json()) + assert(pluggable_split_storage.get(feature_flag_name).to_json() == splits.from_raw(splits_json['splitChange1_2']['splits'][0]).to_json()) assert(pluggable_split_storage.get('not_existing') == None) def test_fetch_many(self): @@ -178,8 +178,8 @@ def test_fetch_many(self): split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) fetched = pluggable_split_storage.fetch_many([split1.name, split2.name]) assert(fetched[split1.name].to_json() == split1.to_json()) assert(fetched[split2.name].to_json() == split2.to_json()) @@ -217,8 +217,8 @@ def test_get_split_names(self): split2_temp = splits_json['splitChange1_2']['splits'][0].copy() split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) assert(pluggable_split_storage.get_split_names() == [split1.name, split2.name]) def test_get_all(self): @@ -230,8 +230,8 @@ def test_get_all(self): split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) all_splits = pluggable_split_storage.get_all() assert([all_splits[0].to_json(), all_splits[1].to_json()] == [split1.to_json(), split2.to_json()]) From 76cde5da305d45ad482eb0d0cbf9dfc72215bc28 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 6 Sep 2023 16:14:15 -0700 Subject: [PATCH 41/77] updated e2e inmemory tests --- tests/integration/files/splitChanges.json | 12 +- tests/integration/test_client_e2e.py | 787 ++++++++++++---------- 2 files changed, 429 insertions(+), 370 deletions(-) diff --git a/tests/integration/files/splitChanges.json b/tests/integration/files/splitChanges.json index d5401c93..f77ce97e 100644 --- a/tests/integration/files/splitChanges.json +++ b/tests/integration/files/splitChanges.json @@ -58,7 +58,8 @@ } ] } - ] + ], + "sets": ["set1", "set2"] }, { "orgId": null, @@ -95,7 +96,8 @@ } ] } - ] + ], + "sets": ["set4"] }, { "orgId": null, @@ -136,7 +138,8 @@ } ] } - ] + ], + "sets": ["set3"] }, { "orgId": null, @@ -199,7 +202,8 @@ } ] } - ] + ], + "sets": ["set1"] }, { "orgId": null, diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 02e61051..8d4b150b 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -34,6 +34,344 @@ from tests.integration import splits_json from tests.storage.test_pluggable import StorageMockAdapter +def _validate_last_impressions(client, *to_validate): + """Validate the last N impressions are present disregarding the order.""" + imp_storage = client._factory._get_storage('impressions') + impressions = imp_storage.pop_many(len(to_validate)) + as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) + assert as_tup_set == set(to_validate) + +def _validate_last_events(client, *to_validate): + """Validate the last N impressions are present disregarding the order.""" + event_storage = client._factory._get_storage('events') + events = event_storage.pop_many(len(to_validate)) + as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) + assert as_tup_set == set(to_validate) + +def _get_treatment(factory): + """Test client.get_treatment().""" + try: + client = factory.client() + except: + pass + + assert client.get_treatment('user1', 'sample_feature') == 'on' + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + assert client.get_treatment('invalidKey', 'sample_feature') == 'off' + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' + _validate_last_impressions(client) # No impressions should be present + + # testing a killed feature. No matter what the key, must return default treatment + assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + assert client.get_treatment('invalidKey', 'all_feature') == 'on' + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + + # testing WHITELIST matcher + assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' + _validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) + assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' + _validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) + + # testing INVALID matcher + assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' + _validate_last_impressions(client) # No impressions should be present + + # testing Dependency matcher + assert client.get_treatment('somekey', 'dependency_test') == 'off' + _validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) + + # testing boolean matcher + assert client.get_treatment('True', 'boolean_test') == 'on' + _validate_last_impressions(client, ('boolean_test', 'True', 'on')) + + # testing regex matcher + assert client.get_treatment('abc4', 'regex_test') == 'on' + _validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + +def _get_treatment_with_config(factory): + """Test client.get_treatment_with_config().""" + try: + client = factory.client() + except: + pass + result = client.get_treatment_with_config('user1', 'sample_feature') + assert result == ('on', '{"size":15,"test":20}') + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatment_with_config('invalidKey', 'sample_feature') + assert result == ('off', None) + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatment_with_config('invalidKey', 'invalid_feature') + assert result == ('control', None) + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatment_with_config('invalidKey', 'killed_feature') + assert ('defTreatment', '{"size":15,"defTreatment":true}') == result + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatment_with_config('invalidKey', 'all_feature') + assert result == ('on', None) + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments(factory): + """Test client.get_treatments().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments('user1', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == 'on' + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatments('invalidKey', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == 'off' + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatments('invalidKey', ['invalid_feature']) + assert len(result) == 1 + assert result['invalid_feature'] == 'control' + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments('invalidKey', ['killed_feature']) + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments('invalidKey', ['all_feature']) + assert len(result) == 1 + assert result['all_feature'] == 'on' + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_with_config(factory): + """Test client.get_treatments_with_config().""" + try: + client = factory.client() + except: + pass + + result = client.get_treatments_with_config('user1', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == ('on', '{"size":15,"test":20}') + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatments_with_config('invalidKey', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == ('off', None) + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) + assert len(result) == 1 + assert result['invalid_feature'] == ('control', None) + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config('invalidKey', ['killed_feature']) + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config('invalidKey', ['all_feature']) + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + + # testing multiple splitNames + result = client.get_treatments_with_config('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == ('on', None) + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + assert result['invalid_feature'] == ('control', None) + assert result['sample_feature'] == ('off', None) + _validate_last_impressions( + client, + ('all_feature', 'invalidKey', 'on'), + ('killed_feature', 'invalidKey', 'defTreatment'), + ('sample_feature', 'invalidKey', 'off'), + ) + +def _get_treatments_by_flag_set(factory): + """Test client.get_treatments_by_flag_set().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_by_flag_set('user1', 'set1') + assert len(result) == 2 + assert result == {'sample_feature': 'on', 'whitelist_feature': 'off'} + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_by_flag_set('invalidKey', 'invalid_set') + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_by_flag_set('invalidKey', 'set3') + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_by_flag_set('invalidKey', 'set4') + assert len(result) == 1 + assert result['all_feature'] == 'on' + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_by_flag_sets(factory): + """Test client.get_treatments_by_flag_sets().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_by_flag_sets('user1', ['set1']) + assert len(result) == 2 + assert result == {'sample_feature': 'on', 'whitelist_feature': 'off'} + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_by_flag_sets('invalidKey', ['invalid_set']) + assert len(result) == 0 + assert result == {} + + result = client.get_treatments_by_flag_sets('invalidKey', []) + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_by_flag_sets('invalidKey', ['set3']) + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_by_flag_sets('user1', ['set4']) + assert len(result) == 1 + assert result['all_feature'] == 'on' + _validate_last_impressions(client, ('all_feature', 'user1', 'on')) + +def _get_treatments_with_config_by_flag_set(factory): + """Test client.get_treatments_with_config_by_flag_set().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_with_config_by_flag_set('user1', 'set1') + assert len(result) == 2 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), 'whitelist_feature': ('off', None)} + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'invalid_set') + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'set3') + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'set4') + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_with_config_by_flag_sets(factory): + """Test client.get_treatments_with_config_by_flag_sets().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1']) + assert len(result) == 2 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), 'whitelist_feature': ('off', None)} + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_with_config_by_flag_sets('invalidKey', ['invalid_set']) + assert len(result) == 0 + assert result == {} + + result = client.get_treatments_with_config_by_flag_sets('invalidKey', []) + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config_by_flag_sets('invalidKey', ['set3']) + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config_by_flag_sets('user1', ['set4']) + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + _validate_last_impressions(client, ('all_feature', 'user1', 'on')) + +def _track(factory): + """Test client.track().""" + try: + client = factory.client() + except: + pass + assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) + assert(not client.track(None, 'user', 'conversion')) + assert(not client.track('user1', None, 'conversion')) + assert(not client.track('user1', 'user', None)) + _validate_last_events( + client, + ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") + ) + +def _manager_methods(factory): + """Test manager.split/splits.""" + try: + manager = factory.manager() + except: + pass + result = manager.split('all_feature') + assert result.name == 'all_feature' + assert result.traffic_type is None + assert result.killed is False + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs == {} + + result = manager.split('killed_feature') + assert result.name == 'killed_feature' + assert result.traffic_type is None + assert result.killed is True + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' + assert result.configs['off'] == '{"size":15,"test":20}' + + result = manager.split('sample_feature') + assert result.name == 'sample_feature' + assert result.traffic_type is None + assert result.killed is False + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs['on'] == '{"size":15,"test":20}' + + assert len(manager.split_names()) == 7 + assert len(manager.splits()) == 7 class InMemoryIntegrationTests(object): """Inmemory storage-based integration tests.""" @@ -47,7 +385,7 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - split_storage.put(splits.from_raw(split)) + split_storage.update([splits.from_raw(split)], [], 0) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -61,7 +399,6 @@ def setup_method(self): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) -# telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() @@ -92,128 +429,18 @@ def teardown_method(self): self.factory.destroy(event) event.wait() - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions = imp_storage.pop_many(len(to_validate)) - as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) - assert as_tup_set == set(to_validate) - - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events = event_storage.pop_many(len(to_validate)) - as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - try: - client = self.factory.client() - except: - pass - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - try: - client = self.factory.client() - except: - pass - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): - """Test client.get_treatments().""" - try: - client = self.factory.client() - except: - pass - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing multiple splitNames + _get_treatments(self.factory) + # testing multiple splitNames + client = self.factory.client() result = client.get_treatments('invalidKey', [ 'all_feature', 'killed_feature', @@ -225,7 +452,7 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), @@ -234,105 +461,54 @@ def test_get_treatments(self): def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - try: - client = self.factory.client() - except: - pass - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + _get_treatments_with_config(self.factory) - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) - # testing multiple splitNames - result = client.get_treatments_with_config('invalidKey', [ - 'all_feature', - 'killed_feature', - 'invalid_feature', - 'sample_feature' - ]) - assert len(result) == 4 - assert result['all_feature'] == ('on', None) - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - assert result['invalid_feature'] == ('control', None) - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( - client, - ('all_feature', 'invalidKey', 'on'), - ('killed_feature', 'invalidKey', 'defTreatment'), - ('sample_feature', 'invalidKey', 'off'), - ) + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) def test_track(self): """Test client.track().""" - try: - client = self.factory.client() - except: - pass - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) class InMemoryOptimizedIntegrationTests(object): @@ -347,7 +523,7 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - split_storage.put(splits.from_raw(split)) + split_storage.update([splits.from_raw(split)], [], 0) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -361,7 +537,6 @@ def setup_method(self): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() @@ -398,85 +573,13 @@ def _validate_last_events(self, client, *to_validate): def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - client.get_treatment('user1', 'sample_feature') - client.get_treatment('user1', 'sample_feature') - client.get_treatment('user1', 'sample_feature') - - # Only one impression was added, and popped when validating, the rest were ignored - assert self.factory._storages['impressions']._impressions.qsize() == 0 - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" - client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments('invalidKey', [ 'all_feature', 'killed_feature', @@ -492,92 +595,48 @@ def test_get_treatments(self): def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + _get_treatment_with_config(self.factory) - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + self._validate_last_impressions(client, ) + assert self.factory._storages['impressions']._impressions.qsize() == 0 - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) - # testing multiple splitNames - result = client.get_treatments_with_config('invalidKey', [ - 'all_feature', - 'killed_feature', - 'invalid_feature', - 'sample_feature' - ]) - assert len(result) == 4 - - assert result['all_feature'] == ('on', None) - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - assert result['invalid_feature'] == ('control', None) - assert result['sample_feature'] == ('off', None) - assert self.factory._storages['impressions']._impressions.qsize() == 0 + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + self._validate_last_impressions(client, ) def test_manager_methods(self): """Test manager.split/splits.""" - manager = self.factory.manager() - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) class RedisIntegrationTests(object): """Redis storage-based integration tests.""" @@ -610,7 +669,6 @@ def setup_method(self): telemetry_redis_storage = RedisTelemetryStorage(redis_client, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_redis_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_redis_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { @@ -927,7 +985,6 @@ def setup_method(self): telemetry_redis_storage = RedisTelemetryStorage(redis_client, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_redis_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_redis_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -1466,7 +1523,6 @@ def setup_method(self): telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { @@ -1803,7 +1859,6 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) self.client = self.factory.client() - def _validate_last_events(self, client, *to_validate): """Validate the last N impressions are present disregarding the order.""" event_storage = client._factory._get_storage('events') From c3392802b8e973585d9d539bfbdf096df8aa0b44 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 7 Sep 2023 08:37:58 -0700 Subject: [PATCH 42/77] Added none check for fetched splits by flag set in client. --- splitio/client/client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 0bcb3939..ab29065c 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -430,7 +430,12 @@ def _get_feature_flag_names_by_flag_sets(self, flag_sets): """ sanitized_flag_sets = config.sanitize_flag_sets(flag_sets) feature_flags = [] - [feature_flags.extend(self._split_storage.get_feature_flags_by_set(flag_set)) for flag_set in sanitized_flag_sets] + for flag_set in sanitized_flag_sets: + feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(flag_set) + if feature_flags_by_set is None: + _LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_set)) + continue + feature_flags.extend(feature_flags_by_set) feature_flags_names = [] [feature_flags_names.append(feature_flag) for feature_flag in feature_flags] return feature_flags_names From 837e57182f249c7cecdbc6bdc5a7c7156ce4354d Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 7 Sep 2023 16:56:10 -0700 Subject: [PATCH 43/77] added e2e tests, remved set validation from pluggable since there is no smember key type --- splitio/storage/pluggable.py | 105 ++- tests/integration/test_client_e2e.py | 1170 ++++++++++---------------- tests/storage/test_pluggable.py | 5 +- tests/storage/test_redis.py | 14 + 4 files changed, 529 insertions(+), 765 deletions(-) diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 2be0f6d3..3e572569 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -8,15 +8,16 @@ from splitio.models.impressions import Impression from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MAX_TAGS, get_latency_bucket_index from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage +from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets _LOGGER = logging.getLogger(__name__) class PluggableSplitStorage(SplitStorage): - """InMemory implementation of a split storage.""" + """InMemory implementation of feature flag storage.""" - _SPLIT_NAME_LENGTH = 12 + _FEATURE_FLAG_NAME_LENGTH = 19 - def __init__(self, pluggable_adapter, prefix=None): + def __init__(self, pluggable_adapter, prefix=None, config_flag_sets=[]): """ Class constructor. @@ -26,51 +27,77 @@ def __init__(self, pluggable_adapter, prefix=None): :type prefix: str """ self._pluggable_adapter = pluggable_adapter - self._prefix = "SPLITIO.split.{split_name}" + self._config_flag_sets = config_flag_sets + self._prefix = "SPLITIO.split.{feature_flag_name}" self._traffic_type_prefix = "SPLITIO.trafficType.{traffic_type_name}" - self._split_till_prefix = "SPLITIO.splits.till" + self._feature_flag_till_prefix = "SPLITIO.splits.till" + self._feature_flag_set_prefix = 'SPLITIO.set.{flag_set}' if prefix is not None: self._prefix = prefix + "." + self._prefix self._traffic_type_prefix = prefix + "." + self._traffic_type_prefix - self._split_till_prefix = prefix + "." + self._split_till_prefix + self._feature_flag_till_prefix = prefix + "." + self._feature_flag_till_prefix + self._feature_flag_set_prefix = prefix + "." + self._feature_flag_till_prefix - def get(self, split_name): + def get(self, feature_flag_name): """ - Retrieve a split. + Retrieve a feature flag. - :param split_name: Name of the feature to fetch. - :type split_name: str + :param feature_flag_name: Name of the feature to fetch. + :type feature_flag_name: str :rtype: splitio.models.splits.Split """ try: - split = self._pluggable_adapter.get(self._prefix.format(split_name=split_name)) - if not split: + feature_flag = self._pluggable_adapter.get(self._prefix.format(feature_flag_name=feature_flag_name)) + if not feature_flag: return None - return splits.from_raw(split) + return splits.from_raw(feature_flag) except Exception: - _LOGGER.error('Error getting split from storage') + _LOGGER.error('Error getting feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def fetch_many(self, split_names): + def fetch_many(self, feature_flag_names): """ - Retrieve splits. + Retrieve feature flags. - :param split_names: Names of the features to fetch. - :type split_name: list(str) + :param feature_flag_names: Names of the features to fetch. + :type feature_flag_name: list(str) :return: A dict with split objects parsed from queue. :rtype: dict(split_name, splitio.models.splits.Split) """ try: - prefix_added = [self._prefix.format(split_name=split_name) for split_name in split_names] - return {split['name']: splits.from_raw(split) for split in self._pluggable_adapter.get_many(prefix_added)} + prefix_added = [self._prefix.format(feature_flag_name=feature_flag_name) for feature_flag_name in feature_flag_names] + return {feature_flag['name']: splits.from_raw(feature_flag) for feature_flag in self._pluggable_adapter.get_many(prefix_added)} except Exception: - _LOGGER.error('Error getting split from storage') + _LOGGER.error('Error getting feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None + def get_feature_flags_by_sets(self, flag_sets): + """ + Retrieve feature flags by flag set. + + :param flag_set: Names of the flag set to fetch. + :type flag_set: str + + :return: Feature flag names that are tagged with the flag set + :rtype: listt(str) + """ + try: + sets_to_fetch = get_valid_flag_sets(flag_sets, self._config_flag_sets) + if sets_to_fetch == []: + return [] + + keys = [self._feature_flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] + return self._pluggable_adapter.get_many(keys) + except Exception: + _LOGGER.error('Error fetching feature flag from storage') + _LOGGER.debug('Error: ', exc_info=True) + return None + + # TODO: To be added when producer mode is supported # def put_many(self, splits, change_number): # """ @@ -127,14 +154,14 @@ def update(self, to_add, to_delete, new_change_number): def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ try: - return self._pluggable_adapter.get(self._split_till_prefix) + return self._pluggable_adapter.get(self._feature_flag_till_prefix) except Exception: - _LOGGER.error('Error getting change number in split storage') + _LOGGER.error('Error getting change number in feature flag storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -156,35 +183,35 @@ def get_change_number(self): def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ try: - return [split.name for split in self.get_all()] + return [feature_flag.name for feature_flag in self.get_all()] except Exception: - _LOGGER.error('Error getting split names from storage') + _LOGGER.error('Error getting feature flag names from storage') _LOGGER.debug('Error: ', exc_info=True) return None def get_all(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ try: - return [splits.from_raw(self._pluggable_adapter.get(key)) for key in self._pluggable_adapter.get_keys_by_prefix(self._prefix[:-self._SPLIT_NAME_LENGTH])] + return [splits.from_raw(self._pluggable_adapter.get(key)) for key in self._pluggable_adapter.get_keys_by_prefix(self._prefix[:-self._FEATURE_FLAG_NAME_LENGTH])] except Exception: - _LOGGER.error('Error getting split keys from storage') + _LOGGER.error('Error getting feature flag keys from storage') _LOGGER.debug('Error: ', exc_info=True) return None def traffic_type_exists(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -195,7 +222,7 @@ def traffic_type_exists(self, traffic_type_name): try: return self._pluggable_adapter.get(self._traffic_type_prefix.format(traffic_type_name=traffic_type_name)) != None except Exception: - _LOGGER.error('Error getting split info from storage') + _LOGGER.error('Error getting traffic type info from storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -264,21 +291,21 @@ def kill_locally(self, split_name, default_treatment, change_number): def get_all_splits(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ try: return self.get_all() except Exception: - _LOGGER.error('Error fetching splits from storage') + _LOGGER.error('Error fetching feature flags from storage') _LOGGER.debug('Error: ', exc_info=True) return None def is_valid_traffic_type(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -289,7 +316,7 @@ def is_valid_traffic_type(self, traffic_type_name): try: return self.traffic_type_exists(traffic_type_name) except Exception: - _LOGGER.error('Error getting split info from storage') + _LOGGER.error('Error getting traffic type info from storage') _LOGGER.debug('Error: ', exc_info=True) return None diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 8d4b150b..117d29ba 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -37,16 +37,57 @@ def _validate_last_impressions(client, *to_validate): """Validate the last N impressions are present disregarding the order.""" imp_storage = client._factory._get_storage('impressions') - impressions = imp_storage.pop_many(len(to_validate)) - as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) - assert as_tup_set == set(to_validate) + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage) or isinstance(client._factory._get_storage('splits'), PluggableSplitStorage): + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage): + redis_client = imp_storage._redis + impressions_raw = [ + json.loads(redis_client.lpop(imp_storage.IMPRESSIONS_QUEUE_KEY)) + for _ in to_validate + ] + else: + pluggable_adapter = imp_storage._pluggable_adapter + results = pluggable_adapter.pop_items(imp_storage._impressions_queue_key) + results = [] if results == None else results + impressions_raw = [ + json.loads(i) + for i in results + ] + as_tup_set = set( + (i['i']['f'], i['i']['k'], i['i']['t']) + for i in impressions_raw + ) + assert as_tup_set == set(to_validate) + time.sleep(0.2) # delay for redis to sync + else: + impressions = imp_storage.pop_many(len(to_validate)) + as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) + assert as_tup_set == set(to_validate) def _validate_last_events(client, *to_validate): """Validate the last N impressions are present disregarding the order.""" event_storage = client._factory._get_storage('events') - events = event_storage.pop_many(len(to_validate)) - as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) - assert as_tup_set == set(to_validate) + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage) or isinstance(client._factory._get_storage('splits'), PluggableSplitStorage): + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage): + redis_client = event_storage._redis + events_raw = [ + json.loads(redis_client.lpop(event_storage._EVENTS_KEY_TEMPLATE)) + for _ in to_validate + ] + else: + pluggable_adapter = event_storage._pluggable_adapter + events_raw = [ + json.loads(i) + for i in pluggable_adapter.pop_items(event_storage._events_queue_key) + ] + as_tup_set = set( + (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) + for i in events_raw + ) + assert as_tup_set == set(to_validate) + else: + events = event_storage.pop_many(len(to_validate)) + as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) + assert as_tup_set == set(to_validate) def _get_treatment(factory): """Test client.get_treatment().""" @@ -56,43 +97,54 @@ def _get_treatment(factory): pass assert client.get_treatment('user1', 'sample_feature') == 'on' - _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - _validate_last_impressions(client) # No impressions should be present + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present # testing a killed feature. No matter what the key, must return default treatment assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) # testing ALL matcher assert client.get_treatment('invalidKey', 'all_feature') == 'on' - _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) # testing WHITELIST matcher assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - _validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - _validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) # testing INVALID matcher assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - _validate_last_impressions(client) # No impressions should be present + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present # testing Dependency matcher assert client.get_treatment('somekey', 'dependency_test') == 'off' - _validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) # testing boolean matcher assert client.get_treatment('True', 'boolean_test') == 'on' - _validate_last_impressions(client, ('boolean_test', 'True', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('boolean_test', 'True', 'on')) # testing regex matcher assert client.get_treatment('abc4', 'regex_test') == 'on' - _validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('regex_test', 'abc4', 'on')) def _get_treatment_with_config(factory): """Test client.get_treatment_with_config().""" @@ -102,25 +154,30 @@ def _get_treatment_with_config(factory): pass result = client.get_treatment_with_config('user1', 'sample_feature') assert result == ('on', '{"size":15,"test":20}') - _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) result = client.get_treatment_with_config('invalidKey', 'sample_feature') assert result == ('off', None) - _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) result = client.get_treatment_with_config('invalidKey', 'invalid_feature') assert result == ('control', None) - _validate_last_impressions(client) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # testing a killed feature. No matter what the key, must return default treatment result = client.get_treatment_with_config('invalidKey', 'killed_feature') assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) # testing ALL matcher result = client.get_treatment_with_config('invalidKey', 'all_feature') assert result == ('on', None) - _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) def _get_treatments(factory): """Test client.get_treatments().""" @@ -131,29 +188,34 @@ def _get_treatments(factory): result = client.get_treatments('user1', ['sample_feature']) assert len(result) == 1 assert result['sample_feature'] == 'on' - _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) result = client.get_treatments('invalidKey', ['sample_feature']) assert len(result) == 1 assert result['sample_feature'] == 'off' - _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) result = client.get_treatments('invalidKey', ['invalid_feature']) assert len(result) == 1 assert result['invalid_feature'] == 'control' - _validate_last_impressions(client) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # testing a killed feature. No matter what the key, must return default treatment result = client.get_treatments('invalidKey', ['killed_feature']) assert len(result) == 1 assert result['killed_feature'] == 'defTreatment' - _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) # testing ALL matcher result = client.get_treatments('invalidKey', ['all_feature']) assert len(result) == 1 assert result['all_feature'] == 'on' - _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) def _get_treatments_with_config(factory): """Test client.get_treatments_with_config().""" @@ -165,48 +227,34 @@ def _get_treatments_with_config(factory): result = client.get_treatments_with_config('user1', ['sample_feature']) assert len(result) == 1 assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) result = client.get_treatments_with_config('invalidKey', ['sample_feature']) assert len(result) == 1 assert result['sample_feature'] == ('off', None) - _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) assert len(result) == 1 assert result['invalid_feature'] == ('control', None) - _validate_last_impressions(client) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # testing a killed feature. No matter what the key, must return default treatment result = client.get_treatments_with_config('invalidKey', ['killed_feature']) assert len(result) == 1 assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) # testing ALL matcher result = client.get_treatments_with_config('invalidKey', ['all_feature']) assert len(result) == 1 assert result['all_feature'] == ('on', None) - _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing multiple splitNames - result = client.get_treatments_with_config('invalidKey', [ - 'all_feature', - 'killed_feature', - 'invalid_feature', - 'sample_feature' - ]) - assert len(result) == 4 - assert result['all_feature'] == ('on', None) - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - assert result['invalid_feature'] == ('control', None) - assert result['sample_feature'] == ('off', None) - _validate_last_impressions( - client, - ('all_feature', 'invalidKey', 'on'), - ('killed_feature', 'invalidKey', 'defTreatment'), - ('sample_feature', 'invalidKey', 'off'), - ) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) def _get_treatments_by_flag_set(factory): """Test client.get_treatments_by_flag_set().""" @@ -217,7 +265,8 @@ def _get_treatments_by_flag_set(factory): result = client.get_treatments_by_flag_set('user1', 'set1') assert len(result) == 2 assert result == {'sample_feature': 'on', 'whitelist_feature': 'off'} - _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) result = client.get_treatments_by_flag_set('invalidKey', 'invalid_set') assert len(result) == 0 @@ -227,13 +276,15 @@ def _get_treatments_by_flag_set(factory): result = client.get_treatments_by_flag_set('invalidKey', 'set3') assert len(result) == 1 assert result['killed_feature'] == 'defTreatment' - _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) # testing ALL matcher result = client.get_treatments_by_flag_set('invalidKey', 'set4') assert len(result) == 1 assert result['all_feature'] == 'on' - _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) def _get_treatments_by_flag_sets(factory): """Test client.get_treatments_by_flag_sets().""" @@ -244,7 +295,8 @@ def _get_treatments_by_flag_sets(factory): result = client.get_treatments_by_flag_sets('user1', ['set1']) assert len(result) == 2 assert result == {'sample_feature': 'on', 'whitelist_feature': 'off'} - _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) result = client.get_treatments_by_flag_sets('invalidKey', ['invalid_set']) assert len(result) == 0 @@ -258,13 +310,15 @@ def _get_treatments_by_flag_sets(factory): result = client.get_treatments_by_flag_sets('invalidKey', ['set3']) assert len(result) == 1 assert result['killed_feature'] == 'defTreatment' - _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) # testing ALL matcher result = client.get_treatments_by_flag_sets('user1', ['set4']) assert len(result) == 1 assert result['all_feature'] == 'on' - _validate_last_impressions(client, ('all_feature', 'user1', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'user1', 'on')) def _get_treatments_with_config_by_flag_set(factory): """Test client.get_treatments_with_config_by_flag_set().""" @@ -275,7 +329,8 @@ def _get_treatments_with_config_by_flag_set(factory): result = client.get_treatments_with_config_by_flag_set('user1', 'set1') assert len(result) == 2 assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), 'whitelist_feature': ('off', None)} - _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) result = client.get_treatments_with_config_by_flag_set('invalidKey', 'invalid_set') assert len(result) == 0 @@ -285,13 +340,15 @@ def _get_treatments_with_config_by_flag_set(factory): result = client.get_treatments_with_config_by_flag_set('invalidKey', 'set3') assert len(result) == 1 assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) # testing ALL matcher result = client.get_treatments_with_config_by_flag_set('invalidKey', 'set4') assert len(result) == 1 assert result['all_feature'] == ('on', None) - _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) def _get_treatments_with_config_by_flag_sets(factory): """Test client.get_treatments_with_config_by_flag_sets().""" @@ -302,7 +359,8 @@ def _get_treatments_with_config_by_flag_sets(factory): result = client.get_treatments_with_config_by_flag_sets('user1', ['set1']) assert len(result) == 2 assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), 'whitelist_feature': ('off', None)} - _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) result = client.get_treatments_with_config_by_flag_sets('invalidKey', ['invalid_set']) assert len(result) == 0 @@ -316,13 +374,15 @@ def _get_treatments_with_config_by_flag_sets(factory): result = client.get_treatments_with_config_by_flag_sets('invalidKey', ['set3']) assert len(result) == 1 assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) # testing ALL matcher result = client.get_treatments_with_config_by_flag_sets('user1', ['set4']) assert len(result) == 1 assert result['all_feature'] == ('on', None) - _validate_last_impressions(client, ('all_feature', 'user1', 'on')) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'user1', 'on')) def _track(factory): """Test client.track().""" @@ -462,6 +522,25 @@ def test_get_treatments(self): def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" _get_treatments_with_config(self.factory) + # testing multiple splitNames + client = self.factory.client() + result = client.get_treatments_with_config('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == ('on', None) + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + assert result['invalid_feature'] == ('control', None) + assert result['sample_feature'] == ('off', None) + _validate_last_impressions( + client, + ('all_feature', 'invalidKey', 'on'), + ('killed_feature', 'invalidKey', 'defTreatment'), + ('sample_feature', 'invalidKey', 'off'), + ) def test_get_treatments_by_flag_set(self): """Test client.get_treatments_by_flag_set().""" @@ -482,7 +561,6 @@ def test_get_treatments_by_flag_sets(self): ('all_feature', 'user1', 'on') ) - def test_get_treatments_with_config_by_flag_set(self): """Test client.get_treatments_with_config_by_flag_set().""" _get_treatments_with_config_by_flag_set(self.factory) @@ -557,20 +635,6 @@ def setup_method(self): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), ) # pylint:disable=attribute-defined-outside-init - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions = imp_storage.pop_many(len(to_validate)) - as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) - assert as_tup_set == set(to_validate) - - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events = event_storage.pop_many(len(to_validate)) - as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" _get_treatment(self.factory) @@ -595,7 +659,21 @@ def test_get_treatments(self): def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - _get_treatment_with_config(self.factory) + _get_treatments_with_config(self.factory) + # testing multiple splitNames + client = self.factory.client() + result = client.get_treatments_with_config('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == ('on', None) + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + assert result['invalid_feature'] == ('control', None) + assert result['sample_feature'] == ('off', None) + _validate_last_impressions(client,) def test_get_treatments_by_flag_set(self): """Test client.get_treatments_by_flag_set().""" @@ -611,7 +689,7 @@ def test_get_treatments_by_flag_sets(self): 'whitelist_feature': 'off', 'all_feature': 'on' } - self._validate_last_impressions(client, ) + _validate_last_impressions(client, ) assert self.factory._storages['impressions']._impressions.qsize() == 0 def test_get_treatments_with_config_by_flag_set(self): @@ -628,7 +706,7 @@ def test_get_treatments_with_config_by_flag_sets(self): 'whitelist_feature': ('off', None), 'all_feature': ('on', None) } - self._validate_last_impressions(client, ) + _validate_last_impressions(client, ) def test_manager_methods(self): """Test manager.split/splits.""" @@ -653,7 +731,10 @@ def setup_method(self): data = json.loads(flo.read()) for split in data['splits']: redis_client.set(split_storage._get_key(split['name']), json.dumps(split)) - redis_client.set(split_storage._SPLIT_TILL_KEY, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + redis_client.sadd(split_storage._get_set_key(flag_set), split['name']) + redis_client.set(split_storage._FEATURE_FLAG_TILL_KEY, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -688,135 +769,18 @@ def setup_method(self): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), ) # pylint:disable=attribute-defined-outside-init - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - redis_client = event_storage._redis - events_raw = [ - json.loads(redis_client.lpop(event_storage._EVENTS_KEY_TEMPLATE)) - for _ in to_validate - ] - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - redis_client = imp_storage._redis - impressions_raw = [ - json.loads(redis_client.lpop(imp_storage.IMPRESSIONS_QUEUE_KEY)) - for _ in to_validate - ] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - client = self.factory.client() - - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" + _get_treatments(self.factory) client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments('invalidKey', [ 'all_feature', @@ -829,44 +793,21 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off') ) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) + def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" + _get_treatments_with_config(self.factory) client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments_with_config('invalidKey', [ 'all_feature', @@ -879,58 +820,58 @@ def test_get_treatments_with_config(self): assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off'), ) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def teardown_method(self): """Clear redis cache.""" @@ -946,14 +887,17 @@ def teardown_method(self): "SPLITIO.split.regex_test", "SPLITIO.segment.human_beigns.till", "SPLITIO.split.boolean_test", - "SPLITIO.split.dependency_test" + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" ] redis_client = RedisAdapter(StrictRedis()) for key in keys_to_delete: redis_client.delete(key) - class RedisWithCacheIntegrationTests(RedisIntegrationTests): """Run the same tests as RedisIntegratioTests but with LRU/Expirable cache overlay.""" @@ -969,7 +913,7 @@ def setup_method(self): data = json.loads(flo.read()) for split in data['splits']: redis_client.set(split_storage._get_key(split['name']), json.dumps(split)) - redis_client.set(split_storage._SPLIT_TILL_KEY, data['till']) + redis_client.set(split_storage._FEATURE_FLAG_TILL_KEY, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1021,8 +965,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_1") == 'off' # Tests 1 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange1_1']) self._synchronize_now() @@ -1045,8 +989,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 3 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange3_1']) self._synchronize_now() @@ -1060,8 +1004,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'off' # Tests 4 - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange4_1']) self._synchronize_now() @@ -1084,9 +1028,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 5 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1', 'SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange5_1']) self._synchronize_now() @@ -1100,8 +1043,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 6 - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange6_1']) self._synchronize_now() @@ -1198,12 +1141,11 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { @@ -1233,8 +1175,12 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1248,134 +1194,18 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added'])) self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions_raw = [] - stored_impressions = self.pluggable_storage_adapter.pop_items(imp_storage._impressions_queue_key) - if stored_impressions is not None: - impressions_raw = [json.loads(im) for im in stored_impressions] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - client = self.factory.client() - - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" + _get_treatments(self.factory) client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments('invalidKey', [ 'all_feature', @@ -1388,44 +1218,21 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off') ) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) + def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" + _get_treatments_with_config(self.factory) client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments_with_config('invalidKey', [ 'all_feature', @@ -1438,58 +1245,58 @@ def test_get_treatments_with_config(self): assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off'), ) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def teardown_method(self): """Clear pluggable cache.""" @@ -1505,9 +1312,12 @@ def teardown_method(self): "SPLITIO.split.regex_test", "SPLITIO.segment.human_beigns.till", "SPLITIO.split.boolean_test", - "SPLITIO.split.dependency_test" + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" ] - for key in keys_to_delete: self.pluggable_storage_adapter.delete(key) @@ -1518,18 +1328,18 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { 'splits': split_storage, 'segments': segment_storage, - 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), - 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), + 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata), + 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata), 'telemetry': telemetry_pluggable_storage } @@ -1552,8 +1362,11 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1567,161 +1380,34 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added'])) self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions_raw = [] - stored_impressions = self.pluggable_storage_adapter.pop_items(imp_storage._impressions_queue_key) - if stored_impressions is not None: - impressions_raw = [json.loads(im) for im in stored_impressions] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" + _get_treatment(self.factory) client = self.factory.client() assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) client.get_treatment('user1', 'sample_feature') client.get_treatment('user1', 'sample_feature') client.get_treatment('user1', 'sample_feature') + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] - # Only one impression was added, and popped when validating, the rest were ignored -# pytest.set_trace() - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" - client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatments(self.factory) - # testing multiple splitNames - result = client.get_treatments('invalidKey', [ - 'all_feature', - 'killed_feature', - 'invalid_feature', - 'sample_feature' - ]) - assert len(result) == 4 - assert result['all_feature'] == 'on' - assert result['killed_feature'] == 'defTreatment' - assert result['invalid_feature'] == 'control' - assert result['sample_feature'] == 'off' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments_with_config(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments_with_config('invalidKey', [ 'all_feature', 'killed_feature', @@ -1729,55 +1415,74 @@ def test_get_treatments_with_config(self): 'sample_feature' ]) assert len(result) == 4 - assert result['all_feature'] == ('on', None) assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + _validate_last_impressions(client,) - def test_manager_methods(self): - """Test manager.split/splits.""" - manager = self.factory.manager() - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ) def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) + + def test_manager_methods(self): + """Test manager.split/splits.""" + _manager_methods(self.factory) + + def teardown_method(self): + """Clear pluggable cache.""" + keys_to_delete = [ + "SPLITIO.segment.human_beigns", + "SPLITIO.segment.employees.till", + "SPLITIO.split.sample_feature", + "SPLITIO.splits.till", + "SPLITIO.split.killed_feature", + "SPLITIO.split.all_feature", + "SPLITIO.split.whitelist_feature", + "SPLITIO.segment.employees", + "SPLITIO.split.regex_test", + "SPLITIO.segment.human_beigns.till", + "SPLITIO.split.boolean_test", + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" + ] + for key in keys_to_delete: + self.pluggable_storage_adapter.delete(key) class PluggableNoneIntegrationTests(object): """Pluggable storage-based integration tests.""" @@ -1786,25 +1491,24 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { 'splits': split_storage, 'segments': segment_storage, - 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), - 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), + 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata), + 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata), 'telemetry': telemetry_pluggable_storage } unique_keys_synchronizer, clear_filter_sync, unique_keys_task, \ clear_filter_task, impressions_count_sync, impressions_count_task, \ - imp_strategy = set_classes('PLUGGABLE', ImpressionsMode.NONE, self.pluggable_storage_adapter, 'myprefix') + imp_strategy = set_classes('PLUGGABLE', ImpressionsMode.NONE, self.pluggable_storage_adapter) impmanager = ImpressionsManager(imp_strategy, telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], @@ -1843,8 +1547,11 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1859,66 +1566,79 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) self.client = self.factory.client() - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - assert self.client.get_treatment('user1', 'sample_feature') == 'on' - assert self.client.get_treatment('invalidKey', 'sample_feature') == 'off' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + _get_treatment(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_get_treatments(self): """Test client.get_treatments().""" - result = self.client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - - result = self.client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 + _get_treatments(self.factory) + result = self.client.get_treatments('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == 'on' + assert result['killed_feature'] == 'defTreatment' + assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - result = self.client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - result = self.client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - - result = self.client.get_treatments_with_config('invalidKey2', ['sample_feature']) - assert len(result) == 1 + _get_treatments_with_config(self.factory) + result = self.client.get_treatments_with_config('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == ('on', None) + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] - result = self.client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + result = self.client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + result = self.client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_track(self): """Test client.track().""" - assert(self.client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not self.client.track(None, 'user', 'conversion')) - assert(not self.client.track('user1', None, 'conversion')) - assert(not self.client.track('user1', 'user', None)) - self._validate_last_events( - self.client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_mtk(self): self.client.get_treatment('user1', 'sample_feature') @@ -1928,6 +1648,6 @@ def test_mtk(self): event = threading.Event() self.factory.destroy(event) event.wait() - assert(json.loads(self.pluggable_storage_adapter._keys['myprefix.SPLITIO.uniquekeys'][0])["f"] =="sample_feature") - assert(json.loads(self.pluggable_storage_adapter._keys['myprefix.SPLITIO.uniquekeys'][0])["ks"].sort() == + assert(json.loads(self.pluggable_storage_adapter._keys['SPLITIO.uniquekeys'][0])["f"] =="sample_feature") + assert(json.loads(self.pluggable_storage_adapter._keys['SPLITIO.uniquekeys'][0])["ks"].sort() == ["invalidKey2", "invalidKey", "user1"].sort()) \ No newline at end of file diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index 38a5b511..bcfde8f9 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -85,7 +85,10 @@ def get_many(self, keys): returned_keys = [] for key in self._keys: if key in keys: - returned_keys.append(self._keys[key]) + if isinstance(self._keys[key], list): + returned_keys.extend(self._keys[key]) + else: + returned_keys.append(self._keys[key]) return returned_keys def add_items(self, key, added_items): diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 33fef5a6..7ee00ca8 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -172,6 +172,20 @@ def test_is_valid_traffic_type_with_cache(self, mocker): time.sleep(1) assert storage.is_valid_traffic_type('any') is False + @mock.patch('splitio.storage.adapters.redis.RedisPipelineAdapter.execute', return_value = [{'split1', 'split2'}]) + def test_flag_sets(self, mocker): + """Test Flag sets scenarios.""" + adapter = build({}) + storage = RedisSplitStorage(adapter, True, 1) + assert storage._config_flag_sets == [] + assert sorted(storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] + + storage._config_flag_sets = ['set2', 'set3'] + assert storage.get_feature_flags_by_sets(['set1']) == [] + assert sorted(storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] + + storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) + assert storage2._config_flag_sets == ['set2', 'set3'] class RedisSegmentStorageTests(object): """Redis segment storage test cases.""" From 9e94dcdb6c3a9b062a9e8e7b32d4ccdbbbfa07ba Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 8 Sep 2023 14:40:21 -0700 Subject: [PATCH 44/77] added flagset and flagsetfilter classes and updated other classes --- splitio/client/client.py | 7 +- splitio/models/splits.py | 2 +- splitio/push/splitworker.py | 19 +- splitio/storage/inmemmory.py | 102 +- splitio/storage/pluggable.py | 104 +- splitio/sync/split.py | 34 +- splitio/util/storage_helper.py | 20 +- tests/integration/test_client_e2e.py | 1777 +++++++++++------------- tests/push/test_split_worker.py | 53 +- tests/storage/test_inmemory_storage.py | 77 +- tests/storage/test_pluggable.py | 5 +- tests/sync/test_splits_synchronizer.py | 20 +- 12 files changed, 1051 insertions(+), 1169 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index c952b418..b8368d28 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -429,8 +429,11 @@ def _get_feature_flag_names_by_flag_sets(self, flag_sets): :rtype: list """ sanitized_flag_sets = config.sanitize_flag_sets(flag_sets) - feature_flags_names = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets) - return feature_flags_names + feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets) + if feature_flags_by_set is None: + _LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_sets)) + return [] + return feature_flags_by_set def _build_impression( # pylint: disable=too-many-arguments self, diff --git a/splitio/models/splits.py b/splitio/models/splits.py index 241650e8..a6913cf5 100644 --- a/splitio/models/splits.py +++ b/splitio/models/splits.py @@ -93,7 +93,7 @@ def __init__( # pylint: disable=too-many-arguments self._algo = HashAlgorithm.LEGACY self._configurations = configurations - self._sets = sets + self._sets = set(sets) if sets is not None else set() @property def name(self): diff --git a/splitio/push/splitworker.py b/splitio/push/splitworker.py index 96654040..00329c44 100644 --- a/splitio/push/splitworker.py +++ b/splitio/push/splitworker.py @@ -10,7 +10,7 @@ from splitio.models.splits import from_raw, Status from splitio.models.telemetry import UpdateFromSSE from splitio.push.parser import UpdateType - +from splitio.util.storage_helper import update_feature_flag_storage _LOGGER = logging.getLogger(__name__) @@ -88,17 +88,12 @@ def _run(self): try: if self._check_instant_ff_update(event): try: - new_split = from_raw(json.loads(self._get_feature_flag_definition(event))) - if new_split.status == Status.ACTIVE: - self._feature_flag_storage.put(new_split) - _LOGGER.debug('Feature flag %s is updated', new_split.name) - for segment_name in new_split.get_segment_names(): - if self._segment_storage.get(segment_name) is None: - _LOGGER.debug('Fetching new segment %s', segment_name) - self._segment_handler(segment_name, event.change_number) - else: - self._feature_flag_storage.remove(new_split.name) - self._feature_flag_storage.set_change_number(event.change_number) + new_feature_flag = from_raw(json.loads(self._get_feature_flag_definition(event))) + segment_list = update_feature_flag_storage(self._feature_flag_storage, [new_feature_flag], event.change_number) + for segment_name in segment_list: + if self._segment_storage.get(segment_name) is None: + _LOGGER.debug('Fetching new segment %s', segment_name) + self._segment_handler(segment_name, event.change_number) self._telemetry_runtime_producer.record_update_from_sse(UpdateFromSSE.SPLIT_UPDATE) continue except Exception as e: diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 39fe6f3e..bd7326b8 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -13,9 +13,70 @@ _LOGGER = logging.getLogger(__name__) +class FlagSetsFilter(object): + """Config Flagsets Filter storage.""" + + def __init__(self, flag_sets=[]): + self.flag_sets = set(flag_sets) + self.should_filter = any(flag_sets) + + def set_exist(self, flag_set): + if not self.should_filter: + return True + if not isinstance(flag_set, str) or flag_set == '': + return False + + return any(self.flag_sets.intersection(set([flag_set]))) + + def intersect(self, flag_sets): + if not self.should_filter: + return True + if not isinstance(flag_sets, set) or len(flag_sets) == 0: + return False + return any(self.flag_sets.intersection(flag_sets)) + + +class FlagSets(object): + """InMemory Flagsets storage.""" + + def __init__(self, flag_sets=[]): + self._lock = threading.RLock() + self.sets_feature_flag_map = {} + for flag_set in flag_sets: + self.sets_feature_flag_map[flag_set] = set() + + def flag_set_exist(self, flag_set): + with self._lock: + return flag_set in self.sets_feature_flag_map.keys() + + def get_flag_set(self, flag_set): + with self._lock: + if self.flag_set_exist(flag_set): + return self.sets_feature_flag_map[flag_set] + + def add_flag_set(self, flag_set): + with self._lock: + if not self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set] = set() + + def remove_flag_set(self, flag_set): + with self._lock: + if self.flag_set_exist(flag_set): + del self.sets_feature_flag_map[flag_set] + + def add_feature_flag_to_flag_set(self, flag_set, feature_flag): + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].add(feature_flag) + + def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].remove(feature_flag) + class InMemorySplitStorage(SplitStorage): - """InMemory implementation of a split storage.""" + """InMemory implementation of a feature flag storage.""" def __init__(self, flag_sets=[]): """Constructor.""" @@ -23,10 +84,8 @@ def __init__(self, flag_sets=[]): self._splits = {} self._change_number = -1 self._traffic_types = Counter() - self._sets_feature_flag_map = {} - self.config_flag_sets_used = len(flag_sets) - for flag_set in flag_sets: - self._sets_feature_flag_map[flag_set] = set() + self.flag_set = FlagSets(flag_sets) + self.flag_set_filter = FlagSetsFilter(flag_sets) def get(self, split_name): """ @@ -82,11 +141,11 @@ def _put(self, split): self._increase_traffic_type_count(split.traffic_type_name) if split.sets is not None: for flag_set in split.sets: - if flag_set not in self._sets_feature_flag_map.keys(): - if self.config_flag_sets_used > 0: + if not self.flag_set.flag_set_exist(flag_set): + if self.flag_set_filter.should_filter: continue - self._sets_feature_flag_map[flag_set] = set() - self._sets_feature_flag_map[flag_set].add(split.name) + self.flag_set.add_flag_set(flag_set) + self.flag_set.add_feature_flag_to_flag_set(flag_set, split.name) def _remove(self, split_name): """ @@ -118,9 +177,9 @@ def _remove_from_flag_sets(self, feature_flag): """ if feature_flag.sets is not None: for flag_set in feature_flag.sets: - self._sets_feature_flag_map[flag_set].remove(feature_flag.name) - if len(self._sets_feature_flag_map[flag_set]) == 0 and self.config_flag_sets_used == 0: - del self._sets_feature_flag_map[flag_set] + self.flag_set.remove_feature_flag_to_flag_set(flag_set, feature_flag.name) + if len(self.flag_set.get_flag_set(flag_set)) == 0 and not self.flag_set_filter.should_filter: + self.flag_set.remove_flag_set(flag_set) def get_feature_flags_by_sets(self, sets): """ @@ -135,19 +194,13 @@ def get_feature_flags_by_sets(self, sets): with self._lock: sets_to_fetch = [] for flag_set in sets: - if flag_set not in self._sets_feature_flag_map.keys(): - if self.config_flag_sets_used > 0: - _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) - continue - else: - self._sets_feature_flag_map[flag_set] = set() + if not self.flag_set.flag_set_exist(flag_set): + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring it." % (flag_set)) + continue sets_to_fetch.append(flag_set) - if sets_to_fetch == []: - return [] - to_return = set() - [to_return.update(self._sets_feature_flag_map[flag_set]) for flag_set in sets_to_fetch] + [to_return.update(self.flag_set.get_flag_set(flag_set)) for flag_set in sets_to_fetch] return list(to_return) def get_change_number(self): @@ -260,10 +313,7 @@ def is_flag_set_exist(self, flag_set): :return: True if the flag_set exist. False otherwise. :rtype: bool """ - if flag_set in self._sets_feature_flag_map.keys(): - return True - return False - + return self.flag_set.flag_set_exist(flag_set) class InMemorySegmentStorage(SegmentStorage): """In-memory implementation of a segment storage.""" diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 2be0f6d3..4a9db0b9 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -8,15 +8,16 @@ from splitio.models.impressions import Impression from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MAX_TAGS, get_latency_bucket_index from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage +from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets _LOGGER = logging.getLogger(__name__) class PluggableSplitStorage(SplitStorage): - """InMemory implementation of a split storage.""" + """InMemory implementation of feature flag storage.""" - _SPLIT_NAME_LENGTH = 12 + _FEATURE_FLAG_NAME_LENGTH = 19 - def __init__(self, pluggable_adapter, prefix=None): + def __init__(self, pluggable_adapter, prefix=None, config_flag_sets=[]): """ Class constructor. @@ -26,48 +27,73 @@ def __init__(self, pluggable_adapter, prefix=None): :type prefix: str """ self._pluggable_adapter = pluggable_adapter - self._prefix = "SPLITIO.split.{split_name}" + self._config_flag_sets = config_flag_sets + self._prefix = "SPLITIO.split.{feature_flag_name}" self._traffic_type_prefix = "SPLITIO.trafficType.{traffic_type_name}" - self._split_till_prefix = "SPLITIO.splits.till" + self._feature_flag_till_prefix = "SPLITIO.splits.till" + self._feature_flag_set_prefix = 'SPLITIO.set.{flag_set}' if prefix is not None: self._prefix = prefix + "." + self._prefix self._traffic_type_prefix = prefix + "." + self._traffic_type_prefix - self._split_till_prefix = prefix + "." + self._split_till_prefix + self._feature_flag_till_prefix = prefix + "." + self._feature_flag_till_prefix + self._feature_flag_set_prefix = prefix + "." + self._feature_flag_till_prefix - def get(self, split_name): + def get(self, feature_flag_name): """ - Retrieve a split. + Retrieve a feature flag. - :param split_name: Name of the feature to fetch. - :type split_name: str + :param feature_flag_name: Name of the feature to fetch. + :type feature_flag_name: str :rtype: splitio.models.splits.Split """ try: - split = self._pluggable_adapter.get(self._prefix.format(split_name=split_name)) - if not split: + feature_flag = self._pluggable_adapter.get(self._prefix.format(feature_flag_name=feature_flag_name)) + if not feature_flag: return None - return splits.from_raw(split) + return splits.from_raw(feature_flag) except Exception: - _LOGGER.error('Error getting split from storage') + _LOGGER.error('Error getting feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def fetch_many(self, split_names): + def fetch_many(self, feature_flag_names): """ - Retrieve splits. + Retrieve feature flags. - :param split_names: Names of the features to fetch. - :type split_name: list(str) + :param feature_flag_names: Names of the features to fetch. + :type feature_flag_name: list(str) :return: A dict with split objects parsed from queue. :rtype: dict(split_name, splitio.models.splits.Split) """ try: - prefix_added = [self._prefix.format(split_name=split_name) for split_name in split_names] - return {split['name']: splits.from_raw(split) for split in self._pluggable_adapter.get_many(prefix_added)} + prefix_added = [self._prefix.format(feature_flag_name=feature_flag_name) for feature_flag_name in feature_flag_names] + return {feature_flag['name']: splits.from_raw(feature_flag) for feature_flag in self._pluggable_adapter.get_many(prefix_added)} + except Exception: + _LOGGER.error('Error getting feature flag from storage') + _LOGGER.debug('Error: ', exc_info=True) + return None + + def get_feature_flags_by_sets(self, flag_sets): + """ + Retrieve feature flags by flag set. + + :param flag_set: Names of the flag set to fetch. + :type flag_set: str + + :return: Feature flag names that are tagged with the flag set + :rtype: listt(str) + """ + try: + sets_to_fetch = get_valid_flag_sets(flag_sets, self._config_flag_sets) + if sets_to_fetch == []: + return [] + + keys = [self._feature_flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] + return self._pluggable_adapter.get_many(keys) except Exception: - _LOGGER.error('Error getting split from storage') + _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -127,14 +153,14 @@ def update(self, to_add, to_delete, new_change_number): def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ try: - return self._pluggable_adapter.get(self._split_till_prefix) + return self._pluggable_adapter.get(self._feature_flag_till_prefix) except Exception: - _LOGGER.error('Error getting change number in split storage') + _LOGGER.error('Error getting change number in feature flag storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -156,35 +182,35 @@ def get_change_number(self): def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ try: - return [split.name for split in self.get_all()] + return [feature_flag.name for feature_flag in self.get_all()] except Exception: - _LOGGER.error('Error getting split names from storage') + _LOGGER.error('Error getting feature flag names from storage') _LOGGER.debug('Error: ', exc_info=True) return None def get_all(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ try: - return [splits.from_raw(self._pluggable_adapter.get(key)) for key in self._pluggable_adapter.get_keys_by_prefix(self._prefix[:-self._SPLIT_NAME_LENGTH])] + return [splits.from_raw(self._pluggable_adapter.get(key)) for key in self._pluggable_adapter.get_keys_by_prefix(self._prefix[:-self._FEATURE_FLAG_NAME_LENGTH])] except Exception: - _LOGGER.error('Error getting split keys from storage') + _LOGGER.error('Error getting feature flag keys from storage') _LOGGER.debug('Error: ', exc_info=True) return None def traffic_type_exists(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -195,7 +221,7 @@ def traffic_type_exists(self, traffic_type_name): try: return self._pluggable_adapter.get(self._traffic_type_prefix.format(traffic_type_name=traffic_type_name)) != None except Exception: - _LOGGER.error('Error getting split info from storage') + _LOGGER.error('Error getting traffic type info from storage') _LOGGER.debug('Error: ', exc_info=True) return None @@ -264,21 +290,21 @@ def kill_locally(self, split_name, default_treatment, change_number): def get_all_splits(self): """ - Return all the splits. + Return all the feature flags. - :return: List of all the splits. + :return: List of all the feature flags. :rtype: list """ try: return self.get_all() except Exception: - _LOGGER.error('Error fetching splits from storage') + _LOGGER.error('Error fetching feature flags from storage') _LOGGER.debug('Error: ', exc_info=True) return None def is_valid_traffic_type(self, traffic_type_name): """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -289,7 +315,7 @@ def is_valid_traffic_type(self, traffic_type_name): try: return self.traffic_type_exists(traffic_type_name) except Exception: - _LOGGER.error('Error getting split info from storage') + _LOGGER.error('Error getting traffic type info from storage') _LOGGER.debug('Error: ', exc_info=True) return None diff --git a/splitio/sync/split.py b/splitio/sync/split.py index c904d9d1..3fb4e4bf 100644 --- a/splitio/sync/split.py +++ b/splitio/sync/split.py @@ -13,6 +13,7 @@ from splitio.models import splits from splitio.util.backoff import Backoff from splitio.util.time import get_current_epoch_time_ms +from splitio.util.storage_helper import update_feature_flag_storage from splitio.sync import util _LEGACY_COMMENT_LINE_RE = re.compile(r'^#.*$') @@ -79,39 +80,12 @@ def _fetch_until(self, fetch_options, till=None): _LOGGER.error('Exception raised while fetching feature flags') _LOGGER.debug('Exception information: ', exc_info=True) raise exc - - to_add = [] - to_delete = [] - for feature_flag in feature_flag_changes.get('splits', []): - if (self._feature_flag_storage.config_flag_sets_used == 0 and feature_flag['status'] == splits.Status.ACTIVE.value) or \ - (feature_flag['status'] == splits.Status.ACTIVE.value and self._check_flag_sets(feature_flag)): - parsed = splits.from_raw(feature_flag) - to_add.append(parsed) - segment_list.update(set(parsed.get_segment_names())) - else: - if self._feature_flag_storage.get(feature_flag['name']) is not None: - to_delete.append(feature_flag['name']) - - self._feature_flag_storage.update(to_add, to_delete, feature_flag_changes['till']) + fetched_feature_flags = [] + [fetched_feature_flags.append(splits.from_raw(feature_flag)) for feature_flag in feature_flag_changes.get('splits', [])] + segment_list = update_feature_flag_storage(self._feature_flag_storage, fetched_feature_flags, feature_flag_changes['till']) if feature_flag_changes['till'] == feature_flag_changes['since']: return feature_flag_changes['till'], segment_list - def _check_flag_sets(self, feature_flag): - """ - Check all flag sets in a feature flag, return True if any of sets exist in storage - - :param feature_flag: Flag set to validate. - :type feature_flag: json - - :return: True if any of its flag_set exist. False otherwise. - :rtype: bool - """ - for flag_set in feature_flag['sets']: - if self._feature_flag_storage.is_flag_set_exist(flag_set): - return True - return False - - def _attempt_feature_flag_sync(self, fetch_options, till=None): """ Hit endpoint, update storage and return True if sync is complete. diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index 61e15fc9..c8667da2 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -23,8 +23,7 @@ def update_feature_flag_storage(feature_flag_storage, feature_flags, change_numb to_add = [] to_delete = [] for feature_flag in feature_flags: - if (feature_flag_storage.config_flag_sets_used == 0 and feature_flag.status == splits.Status.ACTIVE) or \ - (feature_flag.status == splits.Status.ACTIVE and _check_flag_sets(feature_flag_storage, feature_flag)): + if feature_flag_storage.flag_set_filter.intersect(feature_flag.sets) and feature_flag.status == splits.Status.ACTIVE: to_add.append(feature_flag) segment_list.update(set(feature_flag.get_segment_names())) else: @@ -34,23 +33,6 @@ 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 _check_flag_sets(feature_flag_storage, feature_flag): - """ - Check all flag sets in a feature flag, return True if any of sets exist in storage - - :param feature_flag_storage: Feature flag storage instance - :type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage - :param feature_flag: Feature flag instance to validate. - :type feature_flag: splitio.models.splits.Split - - :return: True if any of its flag_set exist. False otherwise. - :rtype: bool - """ - for flag_set in feature_flag.sets: - if feature_flag_storage.is_flag_set_exist(flag_set): - return True - return False - def get_valid_flag_sets(flag_sets, config_flag_sets): """ 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/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 02e61051..117d29ba 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -34,6 +34,404 @@ from tests.integration import splits_json from tests.storage.test_pluggable import StorageMockAdapter +def _validate_last_impressions(client, *to_validate): + """Validate the last N impressions are present disregarding the order.""" + imp_storage = client._factory._get_storage('impressions') + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage) or isinstance(client._factory._get_storage('splits'), PluggableSplitStorage): + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage): + redis_client = imp_storage._redis + impressions_raw = [ + json.loads(redis_client.lpop(imp_storage.IMPRESSIONS_QUEUE_KEY)) + for _ in to_validate + ] + else: + pluggable_adapter = imp_storage._pluggable_adapter + results = pluggable_adapter.pop_items(imp_storage._impressions_queue_key) + results = [] if results == None else results + impressions_raw = [ + json.loads(i) + for i in results + ] + as_tup_set = set( + (i['i']['f'], i['i']['k'], i['i']['t']) + for i in impressions_raw + ) + assert as_tup_set == set(to_validate) + time.sleep(0.2) # delay for redis to sync + else: + impressions = imp_storage.pop_many(len(to_validate)) + as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) + assert as_tup_set == set(to_validate) + +def _validate_last_events(client, *to_validate): + """Validate the last N impressions are present disregarding the order.""" + event_storage = client._factory._get_storage('events') + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage) or isinstance(client._factory._get_storage('splits'), PluggableSplitStorage): + if isinstance(client._factory._get_storage('splits'), RedisSplitStorage): + redis_client = event_storage._redis + events_raw = [ + json.loads(redis_client.lpop(event_storage._EVENTS_KEY_TEMPLATE)) + for _ in to_validate + ] + else: + pluggable_adapter = event_storage._pluggable_adapter + events_raw = [ + json.loads(i) + for i in pluggable_adapter.pop_items(event_storage._events_queue_key) + ] + as_tup_set = set( + (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) + for i in events_raw + ) + assert as_tup_set == set(to_validate) + else: + events = event_storage.pop_many(len(to_validate)) + as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) + assert as_tup_set == set(to_validate) + +def _get_treatment(factory): + """Test client.get_treatment().""" + try: + client = factory.client() + except: + pass + + assert client.get_treatment('user1', 'sample_feature') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + assert client.get_treatment('invalidKey', 'sample_feature') == 'off' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + + # testing a killed feature. No matter what the key, must return default treatment + assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + assert client.get_treatment('invalidKey', 'all_feature') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + + # testing WHITELIST matcher + assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) + assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) + + # testing INVALID matcher + assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + + # testing Dependency matcher + assert client.get_treatment('somekey', 'dependency_test') == 'off' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) + + # testing boolean matcher + assert client.get_treatment('True', 'boolean_test') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('boolean_test', 'True', 'on')) + + # testing regex matcher + assert client.get_treatment('abc4', 'regex_test') == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + +def _get_treatment_with_config(factory): + """Test client.get_treatment_with_config().""" + try: + client = factory.client() + except: + pass + result = client.get_treatment_with_config('user1', 'sample_feature') + assert result == ('on', '{"size":15,"test":20}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatment_with_config('invalidKey', 'sample_feature') + assert result == ('off', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatment_with_config('invalidKey', 'invalid_feature') + assert result == ('control', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatment_with_config('invalidKey', 'killed_feature') + assert ('defTreatment', '{"size":15,"defTreatment":true}') == result + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatment_with_config('invalidKey', 'all_feature') + assert result == ('on', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments(factory): + """Test client.get_treatments().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments('user1', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatments('invalidKey', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == 'off' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatments('invalidKey', ['invalid_feature']) + assert len(result) == 1 + assert result['invalid_feature'] == 'control' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments('invalidKey', ['killed_feature']) + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments('invalidKey', ['all_feature']) + assert len(result) == 1 + assert result['all_feature'] == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_with_config(factory): + """Test client.get_treatments_with_config().""" + try: + client = factory.client() + except: + pass + + result = client.get_treatments_with_config('user1', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == ('on', '{"size":15,"test":20}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + + result = client.get_treatments_with_config('invalidKey', ['sample_feature']) + assert len(result) == 1 + assert result['sample_feature'] == ('off', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) + + result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) + assert len(result) == 1 + assert result['invalid_feature'] == ('control', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config('invalidKey', ['killed_feature']) + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config('invalidKey', ['all_feature']) + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_by_flag_set(factory): + """Test client.get_treatments_by_flag_set().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_by_flag_set('user1', 'set1') + assert len(result) == 2 + assert result == {'sample_feature': 'on', 'whitelist_feature': 'off'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_by_flag_set('invalidKey', 'invalid_set') + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_by_flag_set('invalidKey', 'set3') + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_by_flag_set('invalidKey', 'set4') + assert len(result) == 1 + assert result['all_feature'] == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_by_flag_sets(factory): + """Test client.get_treatments_by_flag_sets().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_by_flag_sets('user1', ['set1']) + assert len(result) == 2 + assert result == {'sample_feature': 'on', 'whitelist_feature': 'off'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_by_flag_sets('invalidKey', ['invalid_set']) + assert len(result) == 0 + assert result == {} + + result = client.get_treatments_by_flag_sets('invalidKey', []) + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_by_flag_sets('invalidKey', ['set3']) + assert len(result) == 1 + assert result['killed_feature'] == 'defTreatment' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_by_flag_sets('user1', ['set4']) + assert len(result) == 1 + assert result['all_feature'] == 'on' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'user1', 'on')) + +def _get_treatments_with_config_by_flag_set(factory): + """Test client.get_treatments_with_config_by_flag_set().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_with_config_by_flag_set('user1', 'set1') + assert len(result) == 2 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), 'whitelist_feature': ('off', None)} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'invalid_set') + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'set3') + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config_by_flag_set('invalidKey', 'set4') + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + +def _get_treatments_with_config_by_flag_sets(factory): + """Test client.get_treatments_with_config_by_flag_sets().""" + try: + client = factory.client() + except: + pass + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1']) + assert len(result) == 2 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), 'whitelist_feature': ('off', None)} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), ('whitelist_feature', 'user1', 'off')) + + result = client.get_treatments_with_config_by_flag_sets('invalidKey', ['invalid_set']) + assert len(result) == 0 + assert result == {} + + result = client.get_treatments_with_config_by_flag_sets('invalidKey', []) + assert len(result) == 0 + assert result == {} + + # testing a killed feature. No matter what the key, must return default treatment + result = client.get_treatments_with_config_by_flag_sets('invalidKey', ['set3']) + assert len(result) == 1 + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) + + # testing ALL matcher + result = client.get_treatments_with_config_by_flag_sets('user1', ['set4']) + assert len(result) == 1 + assert result['all_feature'] == ('on', None) + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client, ('all_feature', 'user1', 'on')) + +def _track(factory): + """Test client.track().""" + try: + client = factory.client() + except: + pass + assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) + assert(not client.track(None, 'user', 'conversion')) + assert(not client.track('user1', None, 'conversion')) + assert(not client.track('user1', 'user', None)) + _validate_last_events( + client, + ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") + ) + +def _manager_methods(factory): + """Test manager.split/splits.""" + try: + manager = factory.manager() + except: + pass + result = manager.split('all_feature') + assert result.name == 'all_feature' + assert result.traffic_type is None + assert result.killed is False + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs == {} + + result = manager.split('killed_feature') + assert result.name == 'killed_feature' + assert result.traffic_type is None + assert result.killed is True + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' + assert result.configs['off'] == '{"size":15,"test":20}' + + result = manager.split('sample_feature') + assert result.name == 'sample_feature' + assert result.traffic_type is None + assert result.killed is False + assert len(result.treatments) == 2 + assert result.change_number == 123 + assert result.configs['on'] == '{"size":15,"test":20}' + + assert len(manager.split_names()) == 7 + assert len(manager.splits()) == 7 class InMemoryIntegrationTests(object): """Inmemory storage-based integration tests.""" @@ -47,7 +445,7 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - split_storage.put(splits.from_raw(split)) + split_storage.update([splits.from_raw(split)], [], 0) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -61,7 +459,6 @@ def setup_method(self): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) -# telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() @@ -92,128 +489,18 @@ def teardown_method(self): self.factory.destroy(event) event.wait() - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions = imp_storage.pop_many(len(to_validate)) - as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) - assert as_tup_set == set(to_validate) - - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events = event_storage.pop_many(len(to_validate)) - as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - try: - client = self.factory.client() - except: - pass - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - try: - client = self.factory.client() - except: - pass - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): - """Test client.get_treatments().""" - try: - client = self.factory.client() - except: - pass - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing multiple splitNames + _get_treatments(self.factory) + # testing multiple splitNames + client = self.factory.client() result = client.get_treatments('invalidKey', [ 'all_feature', 'killed_feature', @@ -225,7 +512,7 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), @@ -234,39 +521,9 @@ def test_get_treatments(self): def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - try: - client = self.factory.client() - except: - pass - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments_with_config(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments_with_config('invalidKey', [ 'all_feature', 'killed_feature', @@ -278,61 +535,58 @@ def test_get_treatments_with_config(self): assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off'), ) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + def test_track(self): """Test client.track().""" - try: - client = self.factory.client() - except: - pass - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) class InMemoryOptimizedIntegrationTests(object): @@ -347,7 +601,7 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - split_storage.put(splits.from_raw(split)) + split_storage.update([splits.from_raw(split)], [], 0) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -361,7 +615,6 @@ def setup_method(self): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() @@ -382,101 +635,15 @@ def setup_method(self): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), ) # pylint:disable=attribute-defined-outside-init - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions = imp_storage.pop_many(len(to_validate)) - as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) - assert as_tup_set == set(to_validate) - - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events = event_storage.pop_many(len(to_validate)) - as_tup_set = set((i.key, i.traffic_type_name, i.event_type_id, i.value, str(i.properties)) for i in events) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - client.get_treatment('user1', 'sample_feature') - client.get_treatment('user1', 'sample_feature') - client.get_treatment('user1', 'sample_feature') - - # Only one impression was added, and popped when validating, the rest were ignored - assert self.factory._storages['impressions']._impressions.qsize() == 0 - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" - client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments('invalidKey', [ 'all_feature', 'killed_feature', @@ -492,36 +659,9 @@ def test_get_treatments(self): def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments_with_config(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments_with_config('invalidKey', [ 'all_feature', 'killed_feature', @@ -529,55 +669,52 @@ def test_get_treatments_with_config(self): 'sample_feature' ]) assert len(result) == 4 - assert result['all_feature'] == ('on', None) assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) + _validate_last_impressions(client,) + + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ) assert self.factory._storages['impressions']._impressions.qsize() == 0 + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ) + def test_manager_methods(self): """Test manager.split/splits.""" - manager = self.factory.manager() - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) class RedisIntegrationTests(object): """Redis storage-based integration tests.""" @@ -594,7 +731,10 @@ def setup_method(self): data = json.loads(flo.read()) for split in data['splits']: redis_client.set(split_storage._get_key(split['name']), json.dumps(split)) - redis_client.set(split_storage._SPLIT_TILL_KEY, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + redis_client.sadd(split_storage._get_set_key(flag_set), split['name']) + redis_client.set(split_storage._FEATURE_FLAG_TILL_KEY, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -610,7 +750,6 @@ def setup_method(self): telemetry_redis_storage = RedisTelemetryStorage(redis_client, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_redis_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_redis_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { @@ -630,135 +769,18 @@ def setup_method(self): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), ) # pylint:disable=attribute-defined-outside-init - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - redis_client = event_storage._redis - events_raw = [ - json.loads(redis_client.lpop(event_storage._EVENTS_KEY_TEMPLATE)) - for _ in to_validate - ] - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - redis_client = imp_storage._redis - impressions_raw = [ - json.loads(redis_client.lpop(imp_storage.IMPRESSIONS_QUEUE_KEY)) - for _ in to_validate - ] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - client = self.factory.client() - - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" + _get_treatments(self.factory) client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments('invalidKey', [ 'all_feature', @@ -771,44 +793,21 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off') ) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) + def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" + _get_treatments_with_config(self.factory) client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments_with_config('invalidKey', [ 'all_feature', @@ -821,58 +820,58 @@ def test_get_treatments_with_config(self): assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off'), ) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def teardown_method(self): """Clear redis cache.""" @@ -888,14 +887,17 @@ def teardown_method(self): "SPLITIO.split.regex_test", "SPLITIO.segment.human_beigns.till", "SPLITIO.split.boolean_test", - "SPLITIO.split.dependency_test" + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" ] redis_client = RedisAdapter(StrictRedis()) for key in keys_to_delete: redis_client.delete(key) - class RedisWithCacheIntegrationTests(RedisIntegrationTests): """Run the same tests as RedisIntegratioTests but with LRU/Expirable cache overlay.""" @@ -911,7 +913,7 @@ def setup_method(self): data = json.loads(flo.read()) for split in data['splits']: redis_client.set(split_storage._get_key(split['name']), json.dumps(split)) - redis_client.set(split_storage._SPLIT_TILL_KEY, data['till']) + redis_client.set(split_storage._FEATURE_FLAG_TILL_KEY, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -927,7 +929,6 @@ def setup_method(self): telemetry_redis_storage = RedisTelemetryStorage(redis_client, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_redis_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_redis_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -964,8 +965,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_1") == 'off' # Tests 1 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange1_1']) self._synchronize_now() @@ -988,8 +989,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 3 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange3_1']) self._synchronize_now() @@ -1003,8 +1004,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'off' # Tests 4 - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange4_1']) self._synchronize_now() @@ -1027,9 +1028,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 5 - self.factory._storages['splits'].remove('SPLIT_1') - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_1', 'SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange5_1']) self._synchronize_now() @@ -1043,8 +1043,8 @@ def test_localhost_json_e2e(self): assert client.get_treatment("key", "SPLIT_2", None) == 'on' # Tests 6 - self.factory._storages['splits'].remove('SPLIT_2') - self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) + self.factory._storages['splits'].update([], ['SPLIT_2'], -1) +# self.factory._sync_manager._synchronizer._split_synchronizers._feature_flag_sync._feature_flag_storage.set_change_number(-1) self._update_temp_file(splits_json['splitChange6_1']) self._synchronize_now() @@ -1141,12 +1141,11 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { @@ -1176,8 +1175,12 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1191,134 +1194,18 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added'])) self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions_raw = [] - stored_impressions = self.pluggable_storage_adapter.pop_items(imp_storage._impressions_queue_key) - if stored_impressions is not None: - impressions_raw = [json.loads(im) for im in stored_impressions] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - client = self.factory.client() - - assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + _get_treatment(self.factory) def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" - client = self.factory.client() - - result = client.get_treatment_with_config('user1', 'sample_feature') - assert result == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatment_with_config('invalidKey', 'sample_feature') - assert result == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatment_with_config('invalidKey', 'invalid_feature') - assert result == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatment_with_config('invalidKey', 'killed_feature') - assert ('defTreatment', '{"size":15,"defTreatment":true}') == result - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatment_with_config('invalidKey', 'all_feature') - assert result == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" + _get_treatments(self.factory) client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments('invalidKey', [ 'all_feature', @@ -1331,44 +1218,21 @@ def test_get_treatments(self): assert result['killed_feature'] == 'defTreatment' assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off') ) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) + def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" + _get_treatments_with_config(self.factory) client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - # testing multiple splitNames result = client.get_treatments_with_config('invalidKey', [ 'all_feature', @@ -1381,58 +1245,58 @@ def test_get_treatments_with_config(self): assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - self._validate_last_impressions( + _validate_last_impressions( client, ('all_feature', 'invalidKey', 'on'), ('killed_feature', 'invalidKey', 'defTreatment'), ('sample_feature', 'invalidKey', 'off'), ) + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ('sample_feature', 'user1', 'on'), + ('whitelist_feature', 'user1', 'off'), + ('all_feature', 'user1', 'on') + ) + def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_manager_methods(self): """Test manager.split/splits.""" - try: - manager = self.factory.manager() - except: - pass - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + _manager_methods(self.factory) def teardown_method(self): """Clear pluggable cache.""" @@ -1448,9 +1312,12 @@ def teardown_method(self): "SPLITIO.split.regex_test", "SPLITIO.segment.human_beigns.till", "SPLITIO.split.boolean_test", - "SPLITIO.split.dependency_test" + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" ] - for key in keys_to_delete: self.pluggable_storage_adapter.delete(key) @@ -1461,19 +1328,18 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { 'splits': split_storage, 'segments': segment_storage, - 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), - 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), + 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata), + 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata), 'telemetry': telemetry_pluggable_storage } @@ -1496,8 +1362,11 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1511,161 +1380,34 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added'])) self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - - def _validate_last_impressions(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - imp_storage = client._factory._get_storage('impressions') - impressions_raw = [] - stored_impressions = self.pluggable_storage_adapter.pop_items(imp_storage._impressions_queue_key) - if stored_impressions is not None: - impressions_raw = [json.loads(im) for im in stored_impressions] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) - - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" + _get_treatment(self.factory) client = self.factory.client() assert client.get_treatment('user1', 'sample_feature') == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) client.get_treatment('user1', 'sample_feature') client.get_treatment('user1', 'sample_feature') client.get_treatment('user1', 'sample_feature') + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] - # Only one impression was added, and popped when validating, the rest were ignored -# pytest.set_trace() - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] - - assert client.get_treatment('invalidKey', 'sample_feature') == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - assert client.get_treatment('invalidKey', 'invalid_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing a killed feature. No matter what the key, must return default treatment - assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - assert client.get_treatment('invalidKey', 'all_feature') == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - - # testing WHITELIST matcher - assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on' - self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on')) - assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off' - self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off')) - - # testing INVALID matcher - assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control' - self._validate_last_impressions(client) # No impressions should be present - - # testing Dependency matcher - assert client.get_treatment('somekey', 'dependency_test') == 'off' - self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off')) - - # testing boolean matcher - assert client.get_treatment('True', 'boolean_test') == 'on' - self._validate_last_impressions(client, ('boolean_test', 'True', 'on')) - - # testing regex matcher - assert client.get_treatment('abc4', 'regex_test') == 'on' - self._validate_last_impressions(client, ('regex_test', 'abc4', 'on')) + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) def test_get_treatments(self): """Test client.get_treatments().""" - client = self.factory.client() - - result = client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'off' - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == 'defTreatment' - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == 'on' - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + _get_treatments(self.factory) - # testing multiple splitNames - result = client.get_treatments('invalidKey', [ - 'all_feature', - 'killed_feature', - 'invalid_feature', - 'sample_feature' - ]) - assert len(result) == 4 - assert result['all_feature'] == 'on' - assert result['killed_feature'] == 'defTreatment' - assert result['invalid_feature'] == 'control' - assert result['sample_feature'] == 'off' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + def test_get_treatment_with_config(self): + """Test client.get_treatment_with_config().""" + _get_treatment_with_config(self.factory) def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - client = self.factory.client() - - result = client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - self._validate_last_impressions(client, ('sample_feature', 'user1', 'on')) - - result = client.get_treatments_with_config('invalidKey', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off')) - - result = client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == ('control', None) - self._validate_last_impressions(client) - - # testing a killed feature. No matter what the key, must return default treatment - result = client.get_treatments_with_config('invalidKey', ['killed_feature']) - assert len(result) == 1 - assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') - self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment')) - - # testing ALL matcher - result = client.get_treatments_with_config('invalidKey', ['all_feature']) - assert len(result) == 1 - assert result['all_feature'] == ('on', None) - self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) - + _get_treatments_with_config(self.factory) # testing multiple splitNames + client = self.factory.client() result = client.get_treatments_with_config('invalidKey', [ 'all_feature', 'killed_feature', @@ -1673,55 +1415,74 @@ def test_get_treatments_with_config(self): 'sample_feature' ]) assert len(result) == 4 - assert result['all_feature'] == ('on', None) assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) assert result['sample_feature'] == ('off', None) - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + _validate_last_impressions(client,) - def test_manager_methods(self): - """Test manager.split/splits.""" - manager = self.factory.manager() - result = manager.split('all_feature') - assert result.name == 'all_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs == {} - - result = manager.split('killed_feature') - assert result.name == 'killed_feature' - assert result.traffic_type is None - assert result.killed is True - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}' - assert result.configs['off'] == '{"size":15,"test":20}' - - result = manager.split('sample_feature') - assert result.name == 'sample_feature' - assert result.traffic_type is None - assert result.killed is False - assert len(result.treatments) == 2 - assert result.change_number == 123 - assert result.configs['on'] == '{"size":15,"test":20}' - - assert len(manager.split_names()) == 7 - assert len(manager.splits()) == 7 + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + _validate_last_impressions(client, ) + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + client = self.factory.client() + result = client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + _validate_last_impressions(client, ) def test_track(self): """Test client.track().""" - client = self.factory.client() - assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not client.track(None, 'user', 'conversion')) - assert(not client.track('user1', None, 'conversion')) - assert(not client.track('user1', 'user', None)) - self._validate_last_events( - client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) + + def test_manager_methods(self): + """Test manager.split/splits.""" + _manager_methods(self.factory) + + def teardown_method(self): + """Clear pluggable cache.""" + keys_to_delete = [ + "SPLITIO.segment.human_beigns", + "SPLITIO.segment.employees.till", + "SPLITIO.split.sample_feature", + "SPLITIO.splits.till", + "SPLITIO.split.killed_feature", + "SPLITIO.split.all_feature", + "SPLITIO.split.whitelist_feature", + "SPLITIO.segment.employees", + "SPLITIO.split.regex_test", + "SPLITIO.segment.human_beigns.till", + "SPLITIO.split.boolean_test", + "SPLITIO.split.dependency_test", + "SPLITIO.split.set.set1", + "SPLITIO.split.set.set2", + "SPLITIO.split.set.set3", + "SPLITIO.split.set.set4" + ] + for key in keys_to_delete: + self.pluggable_storage_adapter.delete(key) class PluggableNoneIntegrationTests(object): """Pluggable storage-based integration tests.""" @@ -1730,25 +1491,24 @@ def setup_method(self): """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapter() - split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix') - segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix') + split_storage = PluggableSplitStorage(self.pluggable_storage_adapter) + segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter) - telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix') + telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata) telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage) - telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storages = { 'splits': split_storage, 'segments': segment_storage, - 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), - 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'), + 'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata), + 'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata), 'telemetry': telemetry_pluggable_storage } unique_keys_synchronizer, clear_filter_sync, unique_keys_task, \ clear_filter_task, impressions_count_sync, impressions_count_task, \ - imp_strategy = set_classes('PLUGGABLE', ImpressionsMode.NONE, self.pluggable_storage_adapter, 'myprefix') + imp_strategy = set_classes('PLUGGABLE', ImpressionsMode.NONE, self.pluggable_storage_adapter) impmanager = ImpressionsManager(imp_strategy, telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], @@ -1787,8 +1547,11 @@ def setup_method(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split) - self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till']) + if split.get('sets') is not None: + for flag_set in split.get('sets'): + self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) + self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') with open(segment_fn, 'r') as flo: @@ -1803,67 +1566,79 @@ def setup_method(self): self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till']) self.client = self.factory.client() - - def _validate_last_events(self, client, *to_validate): - """Validate the last N impressions are present disregarding the order.""" - event_storage = client._factory._get_storage('events') - events_raw = [] - stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key) - if stored_events is not None: - events_raw = [json.loads(im) for im in stored_events] - - as_tup_set = set( - (i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties'])) - for i in events_raw - ) - assert as_tup_set == set(to_validate) - def test_get_treatment(self): """Test client.get_treatment().""" - assert self.client.get_treatment('user1', 'sample_feature') == 'on' - assert self.client.get_treatment('invalidKey', 'sample_feature') == 'off' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + _get_treatment(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_get_treatments(self): """Test client.get_treatments().""" - result = self.client.get_treatments('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == 'on' - - result = self.client.get_treatments('invalidKey', ['sample_feature']) - assert len(result) == 1 + _get_treatments(self.factory) + result = self.client.get_treatments('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == 'on' + assert result['killed_feature'] == 'defTreatment' + assert result['invalid_feature'] == 'control' assert result['sample_feature'] == 'off' - result = self.client.get_treatments('invalidKey', ['invalid_feature']) - assert len(result) == 1 - assert result['invalid_feature'] == 'control' - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_get_treatments_with_config(self): """Test client.get_treatments_with_config().""" - result = self.client.get_treatments_with_config('user1', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('on', '{"size":15,"test":20}') - - result = self.client.get_treatments_with_config('invalidKey2', ['sample_feature']) - assert len(result) == 1 - assert result['sample_feature'] == ('off', None) - - result = self.client.get_treatments_with_config('invalidKey', ['invalid_feature']) - assert len(result) == 1 + _get_treatments_with_config(self.factory) + result = self.client.get_treatments_with_config('invalidKey', [ + 'all_feature', + 'killed_feature', + 'invalid_feature', + 'sample_feature' + ]) + assert len(result) == 4 + assert result['all_feature'] == ('on', None) + assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}') assert result['invalid_feature'] == ('control', None) - assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == [] + assert result['sample_feature'] == ('off', None) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_by_flag_set(self): + """Test client.get_treatments_by_flag_set().""" + _get_treatments_by_flag_set(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_by_flag_sets(self): + """Test client.get_treatments_by_flag_sets().""" + _get_treatments_by_flag_sets(self.factory) + result = self.client.get_treatments_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': 'on', + 'whitelist_feature': 'off', + 'all_feature': 'on' + } + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_with_config_by_flag_set(self): + """Test client.get_treatments_with_config_by_flag_set().""" + _get_treatments_with_config_by_flag_set(self.factory) + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + + def test_get_treatments_with_config_by_flag_sets(self): + """Test client.get_treatments_with_config_by_flag_sets().""" + _get_treatments_with_config_by_flag_sets(self.factory) + result = self.client.get_treatments_with_config_by_flag_sets('user1', ['set1', 'set2', 'set4']) + assert len(result) == 3 + assert result == {'sample_feature': ('on', '{"size":15,"test":20}'), + 'whitelist_feature': ('off', None), + 'all_feature': ('on', None) + } + assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] def test_track(self): """Test client.track().""" - assert(self.client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"})) - assert(not self.client.track(None, 'user', 'conversion')) - assert(not self.client.track('user1', None, 'conversion')) - assert(not self.client.track('user1', 'user', None)) - self._validate_last_events( - self.client, - ('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}") - ) + _track(self.factory) def test_mtk(self): self.client.get_treatment('user1', 'sample_feature') @@ -1873,6 +1648,6 @@ def test_mtk(self): event = threading.Event() self.factory.destroy(event) event.wait() - assert(json.loads(self.pluggable_storage_adapter._keys['myprefix.SPLITIO.uniquekeys'][0])["f"] =="sample_feature") - assert(json.loads(self.pluggable_storage_adapter._keys['myprefix.SPLITIO.uniquekeys'][0])["ks"].sort() == + assert(json.loads(self.pluggable_storage_adapter._keys['SPLITIO.uniquekeys'][0])["f"] =="sample_feature") + assert(json.loads(self.pluggable_storage_adapter._keys['SPLITIO.uniquekeys'][0])["ks"].sort() == ["invalidKey2", "invalidKey", "user1"].sort()) \ No newline at end of file diff --git a/tests/push/test_split_worker.py b/tests/push/test_split_worker.py index 09ede0bb..23831bc5 100644 --- a/tests/push/test_split_worker.py +++ b/tests/push/test_split_worker.py @@ -55,18 +55,13 @@ def test_handler(self, mocker): def get_change_number(): return 2345 - - self._feature_flag = None - def put(feature_flag): - self._feature_flag = feature_flag + split_worker._feature_flag_storage.get_change_number = get_change_number self.new_change_number = 0 - def set_change_number(new_change_number): - self.new_change_number = new_change_number - - split_worker._feature_flag_storage.get_change_number = get_change_number - split_worker._feature_flag_storage.set_change_number = set_change_number - split_worker._feature_flag_storage.put = put + def update(to_add, to_delete, change_number): + self.new_change_number = change_number + split_worker._feature_flag_storage.update = update + split_worker._feature_flag_storage.config_flag_sets_used = 0 # should call the handler q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 12345, "{}", 1)) @@ -98,45 +93,45 @@ def test_compression(self, mocker): split_worker.start() def get_change_number(): return 2345 - - def put(feature_flag): - self._feature_flag = feature_flag - - def remove(feature_flag): - self._feature_flag_delete = feature_flag - split_worker._feature_flag_storage.get_change_number = get_change_number - split_worker._feature_flag_storage.put = put - split_worker._feature_flag_storage.remove = remove + + self._feature_flag_added = None + self._feature_flag_deleted = None + def update(feature_flag_add, feature_flag_delete, change_number): + self._feature_flag_added = feature_flag_add + self._feature_flag_deleted = feature_flag_delete + split_worker._feature_flag_storage.update = update + split_worker._feature_flag_storage.config_flag_sets_used = 0 # compression 0 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiIzM2VhZmE1MC0xYTY1LTExZWQtOTBkZi1mYTMwZDk2OTA0NDUiLCJuYW1lIjoiYmlsYWxfc3BsaXQiLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOi0xMzY0MTE5MjgyLCJzZWVkIjotNjA1OTM4ODQzLCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib2ZmIiwiY2hhbmdlTnVtYmVyIjoxNjg0MzQwOTA4NDc1LCJhbGdvIjoyLCJjb25maWd1cmF0aW9ucyI6e30sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoidXNlciJ9LCJtYXRjaGVyVHlwZSI6IklOX1NFR01FTlQiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6eyJzZWdtZW50TmFtZSI6ImJpbGFsX3NlZ21lbnQifX1dfSwicGFydGl0aW9ucyI6W3sidHJlYXRtZW50Ijoib24iLCJzaXplIjowfSx7InRyZWF0bWVudCI6Im9mZiIsInNpemUiOjEwMH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgYmlsYWxfc2VnbWVudCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiQUxMX0tFWVMiLCJuZWdhdGUiOmZhbHNlfV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjB9LHsidHJlYXRtZW50Ijoib2ZmIiwic2l6ZSI6MTAwfV0sImxhYmVsIjoiZGVmYXVsdCBydWxlIn1dfQ==', 0)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' +# pytest.set_trace() + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 1 # compression 2 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eJzEUtFq20AQ/JUwz2c4WZZr3ZupTQh1FKjcQinGrKU95cjpZE6nh9To34ssJ3FNX0sfd3Zm53b2TgietDbF9vXIGdUMha5lDwFTQiGOmTQlchLRPJlEEZeTVJZ6oimWZTpP5WyWQMCNyoOxZPft0ZoA8TZ5aW1TUDCNg4qk/AueM5dQkyiez6IonS6mAu0IzWWSxovFLBZoA4WuhcLy8/bh+xoCL8bagaXJtixQsqbOhq1nCjW7AIVGawgUz+Qqzrr6wB4qmi9m00/JIk7TZCpAtmqgpgJF47SpOn9+UQt16s9YaS71z9NHOYQFha9Pm83Tty0EagrFM/t733RHqIFZH4wb7LDMVh+Ecc4Lv+ZsuQiNH8hXF3hLv39XXNCHbJ+v7x/X2eDmuKLA74sPihVr47jMuRpWfxy1Kwo0GLQjmv1xpBFD3+96gSP5cLVouM7QQaA1vxhK9uKmd853bEZS9jsBSwe2UDDu7mJxd2Mo/muQy81m/2X9I7+N8R/FcPmUd76zjH7X/w4AAP//90glTw==', 2)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 2 # compression 1 - self._feature_flag = None + self._feature_flag_added = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'H4sIAAkVZWQC/8WST0+DQBDFv0qzZ0ig/BF6a2xjGismUk2MaZopzOKmy9Isy0EbvrtDwbY2Xo233Tdv5se85cCMBs5FtvrYYwIlsglratTMYiKns+chcAgc24UwsF0Xczt2cm5z8Jw8DmPH9wPyqr5zKyTITb2XwpA4TJ5KWWVgRKXYxHWcX/QUkVi264W+68bjaGyxupdCJ4i9KPI9UgyYpibI9Ha1eJnT/J2QsnNxkDVaLEcOjTQrjWBKVIasFefky95BFZg05Zb2mrhh5I9vgsiL44BAIIuKTeiQVYqLotHHLyLOoT1quRjub4fztQuLxj89LpePzytClGCyd9R3umr21ErOcitUh2PTZHY29HN2+JGixMxUujNfvMB3+u2pY1AXySad3z3Mk46msACDp8W7jhly4uUpFt3qD33vDAx0gLpXkx+P1GusbdcE24M2F4uaywwVEWvxSa1Oa13Vjvn2RXradm0xCVuUVBJqNCBGV0DrX4OcLpeb+/lreh3jH8Uw/JQj3UhkxPgCCurdEnADAAA=', 1)) time.sleep(0.1) - assert self._feature_flag.name == 'bilal_split' + assert self._feature_flag_added[0].name == 'bilal_split' assert telemetry_storage._counters._update_from_sse['sp'] == 3 # should call delete split - self._feature_flag = None - self._feature_flag_delete = None + self._feature_flag_added = None + self._feature_flag_deleted = None q.put(SplitChangeUpdate('some', 'SPLIT_UPDATE', 123456790, 2345, 'eyJ0cmFmZmljVHlwZU5hbWUiOiAidXNlciIsICJpZCI6ICIzM2VhZmE1MC0xYTY1LTExZWQtOTBkZi1mYTMwZDk2OTA0NDUiLCAibmFtZSI6ICJiaWxhbF9zcGxpdCIsICJ0cmFmZmljQWxsb2NhdGlvbiI6IDEwMCwgInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6IC0xMzY0MTE5MjgyLCAic2VlZCI6IC02MDU5Mzg4NDMsICJzdGF0dXMiOiAiQVJDSElWRUQiLCAia2lsbGVkIjogZmFsc2UsICJkZWZhdWx0VHJlYXRtZW50IjogIm9mZiIsICJjaGFuZ2VOdW1iZXIiOiAxNjg0Mjc1ODM5OTUyLCAiYWxnbyI6IDIsICJjb25maWd1cmF0aW9ucyI6IHt9LCAiY29uZGl0aW9ucyI6IFt7ImNvbmRpdGlvblR5cGUiOiAiUk9MTE9VVCIsICJtYXRjaGVyR3JvdXAiOiB7ImNvbWJpbmVyIjogIkFORCIsICJtYXRjaGVycyI6IFt7ImtleVNlbGVjdG9yIjogeyJ0cmFmZmljVHlwZSI6ICJ1c2VyIn0sICJtYXRjaGVyVHlwZSI6ICJJTl9TRUdNRU5UIiwgIm5lZ2F0ZSI6IGZhbHNlLCAidXNlckRlZmluZWRTZWdtZW50TWF0Y2hlckRhdGEiOiB7InNlZ21lbnROYW1lIjogImJpbGFsX3NlZ21lbnQifX1dfSwgInBhcnRpdGlvbnMiOiBbeyJ0cmVhdG1lbnQiOiAib24iLCAic2l6ZSI6IDB9LCB7InRyZWF0bWVudCI6ICJvZmYiLCAic2l6ZSI6IDEwMH1dLCAibGFiZWwiOiAiaW4gc2VnbWVudCBiaWxhbF9zZWdtZW50In0sIHsiY29uZGl0aW9uVHlwZSI6ICJST0xMT1VUIiwgIm1hdGNoZXJHcm91cCI6IHsiY29tYmluZXIiOiAiQU5EIiwgIm1hdGNoZXJzIjogW3sia2V5U2VsZWN0b3IiOiB7InRyYWZmaWNUeXBlIjogInVzZXIifSwgIm1hdGNoZXJUeXBlIjogIkFMTF9LRVlTIiwgIm5lZ2F0ZSI6IGZhbHNlfV19LCAicGFydGl0aW9ucyI6IFt7InRyZWF0bWVudCI6ICJvbiIsICJzaXplIjogMH0sIHsidHJlYXRtZW50IjogIm9mZiIsICJzaXplIjogMTAwfV0sICJsYWJlbCI6ICJkZWZhdWx0IHJ1bGUifV19', 0)) time.sleep(0.1) - assert self._feature_flag_delete == 'bilal_split' - assert self._feature_flag == None + assert self._feature_flag_deleted[0] == 'bilal_split' + self._feature_flag_added = None def test_edge_cases(self, mocker): q = queue.Queue() diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index df5449c6..67501272 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -10,7 +10,66 @@ import splitio.models.telemetry as ModelTelemetry from splitio.engine.telemetry import TelemetryStorageProducer from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ - InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage + InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, FlagSets, FlagSetsFilter + + +class FlagSetsFilterTests(object): + """Flag sets filter storage tests.""" + def test_without_initial_set(self): + flag_set = FlagSets() + assert flag_set.sets_feature_flag_map == {} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == False + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_with_initial_set(self): + flag_set = FlagSets(['set1', 'set2']) + assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == True + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_flag_set_filter(self): + flag_set_filter = FlagSetsFilter() + assert flag_set_filter.flag_sets == set() + assert not flag_set_filter.should_filter + + flag_set_filter = FlagSetsFilter(['set1', 'set2']) + assert flag_set_filter.flag_sets == set({'set1', 'set2'}) + assert flag_set_filter.should_filter + assert flag_set_filter.intersect(set({'set1', 'set2'})) + assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) + assert not flag_set_filter.intersect(set({'set4'})) + assert not flag_set_filter.set_exist('set4') + assert flag_set_filter.set_exist('set1') class InMemorySplitStorageTests(object): @@ -215,8 +274,10 @@ def test_kill_locally(self): def test_flag_sets_with_config_sets(self): storage = InMemorySplitStorage(['set10', 'set02', 'set05']) - assert storage.config_flag_sets_used == 3 - assert storage._sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} + assert storage.flag_set_filter.flag_sets == {'set10', 'set02', 'set05'} + assert storage.flag_set_filter.should_filter + + assert storage.flag_set.sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set10', 'set02']) @@ -250,7 +311,7 @@ def test_flag_sets_with_config_sets(self): storage.update([], [split1.name], 1) assert storage.get_feature_flags_by_sets(['set02']) == [] - assert storage._sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} + assert storage.flag_set.sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} storage.update([split3], [], 1) assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] @@ -258,8 +319,10 @@ def test_flag_sets_with_config_sets(self): def test_flag_sets_withut_config_sets(self): storage = InMemorySplitStorage() - assert storage._sets_feature_flag_map == {} - assert storage.config_flag_sets_used == 0 + assert storage.flag_set_filter.flag_sets == set({}) + assert not storage.flag_set_filter.should_filter + + assert storage.flag_set.sets_feature_flag_map == {} split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set10', 'set02']) @@ -291,7 +354,7 @@ def test_flag_sets_withut_config_sets(self): storage.update([], [split1.name], 1) assert storage.get_feature_flags_by_sets(['set02']) == [] - assert storage._sets_feature_flag_map == {'set02': set()} + assert storage.flag_set.sets_feature_flag_map == {} storage.update([split3], [], 1) assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index 38a5b511..bcfde8f9 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -85,7 +85,10 @@ def get_many(self, keys): returned_keys = [] for key in self._keys: if key in keys: - returned_keys.append(self._keys[key]) + if isinstance(self._keys[key], list): + returned_keys.extend(self._keys[key]) + else: + returned_keys.append(self._keys[key]) return returned_keys def add_items(self, key, added_items): diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index 69df2bec..65dbe97e 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -80,7 +80,15 @@ def change_number_mock(): return 123 change_number_mock._calls = 0 storage.get_change_number.side_effect = change_number_mock - storage.config_flag_sets_used = 0 + + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + + storage.flag_set_filter = flag_set_filter api = mocker.Mock() def get_changes(*args, **kwargs): @@ -167,7 +175,15 @@ def get_changes(*args, **kwargs): return { 'splits': [], 'since': 12345, 'till': 12345 } get_changes.called = 0 api.fetch_splits.side_effect = get_changes - storage.config_flag_sets_used = 0 + + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + + storage.flag_set_filter = flag_set_filter split_synchronizer = SplitSynchronizer(api, storage) split_synchronizer._backoff = Backoff(1, 1) From 23380afe07cd2ae641a046d865e198b47c8a1617 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 8 Sep 2023 14:51:42 -0700 Subject: [PATCH 45/77] polishing --- splitio/storage/inmemmory.py | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index bd7326b8..d1e87abd 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -17,10 +17,19 @@ class FlagSetsFilter(object): """Config Flagsets Filter storage.""" def __init__(self, flag_sets=[]): + """Constructor.""" self.flag_sets = set(flag_sets) self.should_filter = any(flag_sets) def set_exist(self, flag_set): + """ + Check if a flagset exist in flagset filter + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ if not self.should_filter: return True if not isinstance(flag_set, str) or flag_set == '': @@ -29,6 +38,14 @@ def set_exist(self, flag_set): return any(self.flag_sets.intersection(set([flag_set]))) def intersect(self, flag_sets): + """ + Check if a set exist in config flagset filter + + :param flag_set: set of flagsets + :type flag_set: set + + :rtype: bool + """ if not self.should_filter: return True if not isinstance(flag_sets, set) or len(flag_sets) == 0: @@ -40,36 +57,81 @@ class FlagSets(object): """InMemory Flagsets storage.""" def __init__(self, flag_sets=[]): + """Constructor.""" self._lock = threading.RLock() self.sets_feature_flag_map = {} for flag_set in flag_sets: self.sets_feature_flag_map[flag_set] = set() def flag_set_exist(self, flag_set): + """ + Check if a flagset exist in stored flagset + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ with self._lock: return flag_set in self.sets_feature_flag_map.keys() def get_flag_set(self, flag_set): + """ + fetch feature flags stored in a flag set + + :param flag_set: set name + :type flag_set: str + + :rtype: list(str) + """ with self._lock: if self.flag_set_exist(flag_set): return self.sets_feature_flag_map[flag_set] def add_flag_set(self, flag_set): + """ + Add new flag set to storage + + :param flag_set: set name + :type flag_set: str + """ with self._lock: if not self.flag_set_exist(flag_set): self.sets_feature_flag_map[flag_set] = set() def remove_flag_set(self, flag_set): + """ + Remove existing flag set from storage + + :param flag_set: set name + :type flag_set: str + """ with self._lock: if self.flag_set_exist(flag_set): del self.sets_feature_flag_map[flag_set] def add_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Add a feature flag to existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ with self._lock: if self.flag_set_exist(flag_set): self.sets_feature_flag_map[flag_set].add(feature_flag) def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Remove a feature flag from existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ with self._lock: if self.flag_set_exist(flag_set): self.sets_feature_flag_map[flag_set].remove(feature_flag) From 7cbfa0eac5f9d75ca55488e733b747e21b19d5a8 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 11 Sep 2023 13:51:51 -0700 Subject: [PATCH 46/77] Fixed all tests --- splitio/models/splits.py | 8 +- tests/client/test_client.py | 52 +++--- tests/client/test_manager.py | 2 +- tests/integration/files/split_changes.json | 12 +- .../integration/test_pluggable_integration.py | 10 +- tests/integration/test_redis_integration.py | 6 +- tests/models/test_splits.py | 6 +- tests/storage/test_inmemory_storage.py | 148 ++++++++++++++---- tests/storage/test_pluggable.py | 6 +- tests/sync/test_splits_synchronizer.py | 20 ++- tests/sync/test_synchronizer.py | 9 +- tests/sync/test_telemetry.py | 2 +- tests/tasks/test_split_sync.py | 10 +- tests/util/test_storage_helper.py | 62 +++++++- 14 files changed, 260 insertions(+), 93 deletions(-) diff --git a/splitio/models/splits.py b/splitio/models/splits.py index 241650e8..5ab32953 100644 --- a/splitio/models/splits.py +++ b/splitio/models/splits.py @@ -93,7 +93,7 @@ def __init__( # pylint: disable=too-many-arguments self._algo = HashAlgorithm.LEGACY self._configurations = configurations - self._sets = sets + self._sets = set(sets) if sets is not None else set() @property def name(self): @@ -183,7 +183,7 @@ def to_json(self): 'algo': self.algo.value, 'conditions': [c.to_json() for c in self.conditions], 'configurations': self._configurations, - 'sets': self._sets + 'sets': list(self._sets) } def to_split_view(self): @@ -200,7 +200,7 @@ def to_split_view(self): list(set(part.treatment for cond in self.conditions for part in cond.partitions)), self.change_number, self._configurations if self._configurations is not None else {}, - self._sets if self._sets is not None else [] + list(self._sets) if self._sets is not None else [] ) def local_kill(self, default_treatment, change_number): @@ -250,5 +250,5 @@ def from_raw(raw_split): traffic_allocation=raw_split.get('trafficAllocation'), traffic_allocation_seed=raw_split.get('trafficAllocationSeed'), configurations=raw_split.get('configurations'), - sets=raw_split.get('sets') + sets=set(raw_split.get('sets')) if raw_split.get('sets') is not None else [] ) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 6c78a3ff..dbcae6a4 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -353,14 +353,14 @@ def test_get_treatments_by_flag_set(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - def get_feature_flags_by_set(flag_sets): - if flag_sets == 'set1': + def get_feature_flags_by_sets(flag_sets): + if flag_sets == ['set1']: return ['f1', 'f2'] - if flag_sets == 'set2': + if flag_sets == ['set2']: return ['f3', 'f4'] - if flag_sets == 'set3': + if flag_sets == ['set3']: return ['some_feature'] - split_storage.get_feature_flags_by_set = get_feature_flags_by_set + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) @@ -440,18 +440,14 @@ def test_get_treatments_by_flag_sets(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - def get_feature_flags_by_set(flag_sets): - if flag_sets == 'set1': - return ['f1'] - if flag_sets == 'set2': - return ['f2'] - if flag_sets == 'set3': + def get_feature_flags_by_sets(flag_sets): + if flag_sets == ['set1', 'set2']: + return ['f1', 'f2'] + if flag_sets == ['set3', 'set4']: return ['f3', 'f4'] - if flag_sets == 'set4': - return [] - if flag_sets == 'set5': + if flag_sets == ['set5']: return ['some_feature'] - split_storage.get_feature_flags_by_set = get_feature_flags_by_set + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) @@ -530,14 +526,14 @@ def test_get_treatments_with_config_by_flag_set(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - def get_feature_flags_by_set(flag_sets): - if flag_sets == 'set1': + def get_feature_flags_by_sets(flag_sets): + if flag_sets == ['set1']: return ['f1', 'f2'] - if flag_sets == 'set2': + if flag_sets == ['set2']: return ['f3', 'f4'] - if flag_sets == 'set3': + if flag_sets == ['set3']: return ['some_feature'] - split_storage.get_feature_flags_by_set = get_feature_flags_by_set + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) @@ -626,18 +622,14 @@ def test_get_treatments_with_config_by_flag_sets(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - def get_feature_flags_by_set(flag_sets): - if flag_sets == 'set1': - return ['f1'] - if flag_sets == 'set2': - return ['f2'] - if flag_sets == 'set3': + def get_feature_flags_by_sets(flag_sets): + if flag_sets == ['set1', 'set2']: + return ['f1', 'f2'] + if flag_sets == ['set3', 'set4']: return ['f3', 'f4'] - if flag_sets == 'set4': - return [] - if flag_sets == 'set5': + if flag_sets == ['set5']: return ['some_feature'] - split_storage.get_feature_flags_by_set = get_feature_flags_by_set + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) diff --git a/tests/client/test_manager.py b/tests/client/test_manager.py index 6e30837c..b461d2bb 100644 --- a/tests/client/test_manager.py +++ b/tests/client/test_manager.py @@ -96,4 +96,4 @@ def _verify_split(self, split): assert sorted(split.treatments) == ['off', 'on'] assert split.change_number == 123 assert split.configs == {'on': '{"color": "blue", "size": 13}'} - assert split.sets == ['set1', 'set2'] + assert sorted(split.sets) == ['set1', 'set2'] diff --git a/tests/integration/files/split_changes.json b/tests/integration/files/split_changes.json index f536346d..2d21c0da 100644 --- a/tests/integration/files/split_changes.json +++ b/tests/integration/files/split_changes.json @@ -58,7 +58,8 @@ } ] } - ] + ], + "sets": ["set1", "set2"] }, { "orgId": null, @@ -95,7 +96,8 @@ } ] } - ] + ], + "sets": ["set4"] }, { "orgId": null, @@ -136,7 +138,8 @@ } ] } - ] + ], + "sets": ["set3"] }, { "orgId": null, @@ -199,7 +202,8 @@ } ] } - ] + ], + "sets": ["set1"] }, { "orgId": null, diff --git a/tests/integration/test_pluggable_integration.py b/tests/integration/test_pluggable_integration.py index f7e23f9f..024f1688 100644 --- a/tests/integration/test_pluggable_integration.py +++ b/tests/integration/test_pluggable_integration.py @@ -23,9 +23,9 @@ def test_put_fetch(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - adapter.set(storage._prefix.format(split_name=split['name']), split) + adapter.set(storage._prefix.format(feature_flag_name=split['name']), split) adapter.increment(storage._traffic_type_prefix.format(traffic_type_name=split['trafficTypeName']), 1) - adapter.set(storage._split_till_prefix, data['till']) + adapter.set(storage._feature_flag_till_prefix, data['till']) split_objects = [splits.from_raw(raw) for raw in data['splits']] for split_object in split_objects: @@ -52,7 +52,7 @@ def test_put_fetch(self): assert len(original_condition.matchers) == len(fetched_condition.matchers) assert len(original_condition.partitions) == len(fetched_condition.partitions) - adapter.set(storage._split_till_prefix, data['till']) + adapter.set(storage._feature_flag_till_prefix, data['till']) assert storage.get_change_number() == data['till'] assert storage.is_valid_traffic_type('user') is True @@ -89,9 +89,9 @@ def test_get_all(self): with open(split_fn, 'r') as flo: data = json.loads(flo.read()) for split in data['splits']: - adapter.set(storage._prefix.format(split_name=split['name']), split) + adapter.set(storage._prefix.format(feature_flag_name=split['name']), split) adapter.increment(storage._traffic_type_prefix.format(traffic_type_name=split['trafficTypeName']), 1) - adapter.set(storage._split_till_prefix, data['till']) + adapter.set(storage._feature_flag_till_prefix, data['till']) split_objects = [splits.from_raw(raw) for raw in data['splits']] original_splits = {split.name: split for split in split_objects} diff --git a/tests/integration/test_redis_integration.py b/tests/integration/test_redis_integration.py index 685f72c5..279b45a5 100644 --- a/tests/integration/test_redis_integration.py +++ b/tests/integration/test_redis_integration.py @@ -26,7 +26,7 @@ def test_put_fetch(self): split_objects = [splits.from_raw(raw) for raw in split_changes['splits']] for split_object in split_objects: raw = split_object.to_json() - adapter.set(RedisSplitStorage._SPLIT_KEY.format(split_name=split_object.name), json.dumps(raw)) + adapter.set(RedisSplitStorage._FEATURE_FLAG_KEY.format(feature_flag_name=split_object.name), json.dumps(raw)) adapter.incr(RedisSplitStorage._TRAFFIC_TYPE_KEY.format(traffic_type_name=split_object.traffic_type_name)) original_splits = {split.name: split for split in split_objects} @@ -50,7 +50,7 @@ def test_put_fetch(self): assert len(original_condition.matchers) == len(fetched_condition.matchers) assert len(original_condition.partitions) == len(fetched_condition.partitions) - adapter.set(RedisSplitStorage._SPLIT_TILL_KEY, split_changes['till']) + adapter.set(RedisSplitStorage._FEATURE_FLAG_TILL_KEY, split_changes['till']) assert storage.get_change_number() == split_changes['till'] assert storage.is_valid_traffic_type('user') is True @@ -89,7 +89,7 @@ def test_get_all(self): split_objects = [splits.from_raw(raw) for raw in split_changes['splits']] for split_object in split_objects: raw = split_object.to_json() - adapter.set(RedisSplitStorage._SPLIT_KEY.format(split_name=split_object.name), json.dumps(raw)) + adapter.set(RedisSplitStorage._FEATURE_FLAG_KEY.format(feature_flag_name=split_object.name), json.dumps(raw)) original_splits = {split.name: split for split in split_objects} fetched_names = storage.get_split_names() diff --git a/tests/models/test_splits.py b/tests/models/test_splits.py index da289ad0..d56e6f77 100644 --- a/tests/models/test_splits.py +++ b/tests/models/test_splits.py @@ -80,7 +80,7 @@ def test_from_raw(self): assert len(parsed.conditions) == 2 assert parsed.get_configurations_for('on') == '{"color": "blue", "size": 13}' assert parsed._configurations == {'on': '{"color": "blue", "size": 13}'} - assert parsed.sets == ['set1', 'set2'] + assert parsed.sets == {'set1', 'set2'} def test_get_segment_names(self, mocker): """Test fetching segment names.""" @@ -106,7 +106,7 @@ def test_to_json(self): assert as_json['defaultTreatment'] == 'off' assert as_json['algo'] == 2 assert len(as_json['conditions']) == 2 - assert as_json['sets'] == ['set1', 'set2'] + assert sorted(as_json['sets']) == ['set1', 'set2'] def test_to_split_view(self): """Test SplitView creation.""" @@ -117,4 +117,4 @@ def test_to_split_view(self): assert as_split_view.killed == self.raw['killed'] assert as_split_view.traffic_type == self.raw['trafficTypeName'] assert set(as_split_view.treatments) == set(['on', 'off']) - assert as_split_view.sets == self.raw['sets'] + assert sorted(as_split_view.sets) == sorted(list(self.raw['sets'])) diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index a0e7fff3..a4816329 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -10,7 +10,66 @@ import splitio.models.telemetry as ModelTelemetry from splitio.engine.telemetry import TelemetryStorageProducer from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ - InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage + InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, FlagSets, FlagSetsFilter + + +class FlagSetsFilterTests(object): + """Flag sets filter storage tests.""" + def test_without_initial_set(self): + flag_set = FlagSets() + assert flag_set.sets_feature_flag_map == {} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == False + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_with_initial_set(self): + flag_set = FlagSets(['set1', 'set2']) + assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == True + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_flag_set_filter(self): + flag_set_filter = FlagSetsFilter() + assert flag_set_filter.flag_sets == set() + assert not flag_set_filter.should_filter + + flag_set_filter = FlagSetsFilter(['set1', 'set2']) + assert flag_set_filter.flag_sets == set({'set1', 'set2'}) + assert flag_set_filter.should_filter + assert flag_set_filter.intersect(set({'set1', 'set2'})) + assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) + assert not flag_set_filter.intersect(set({'set4'})) + assert not flag_set_filter.set_exist('set4') + assert flag_set_filter.set_exist('set1') class InMemorySplitStorageTests(object): @@ -215,8 +274,10 @@ def test_kill_locally(self): def test_flag_sets_with_config_sets(self): storage = InMemorySplitStorage(['set10', 'set02', 'set05']) - assert storage.config_flag_sets_used == 3 - assert storage._sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} + assert storage.flag_set_filter.flag_sets == {'set10', 'set02', 'set05'} + assert storage.flag_set_filter.should_filter + + assert storage.flag_set.sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set10', 'set02']) @@ -225,40 +286,43 @@ def test_flag_sets_with_config_sets(self): split3 = Split('split3', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set04', 'set05']) storage.update([split1], [], 1) - assert storage.get_feature_flags_by_set('set10') == ['split1'] - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set10']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02', 'set10']) == ['split1'] assert storage.is_flag_set_exist('set10') assert storage.is_flag_set_exist('set02') assert not storage.is_flag_set_exist('set03') storage.update([split2], [], 1) - assert storage.get_feature_flags_by_set('set05') == ['split2'] - assert sorted(storage.get_feature_flags_by_set('set02')) == ['split1', 'split2'] + assert storage.get_feature_flags_by_sets(['set05']) == ['split2'] + assert sorted(storage.get_feature_flags_by_sets(['set02', 'set05'])) == ['split1', 'split2'] assert storage.is_flag_set_exist('set05') storage.update([], [split2.name], 1) assert storage.is_flag_set_exist('set05') - assert storage.get_feature_flags_by_set('set02') == ['split1'] - assert storage.get_feature_flags_by_set('set05') == [] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set05']) == [] split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set02']) storage.update([split1], [], 1) assert storage.is_flag_set_exist('set10') - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] storage.update([], [split1.name], 1) - assert storage.get_feature_flags_by_set('set02') == [] - assert storage._sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} + assert storage.get_feature_flags_by_sets(['set02']) == [] + assert storage.flag_set.sets_feature_flag_map == {'set10': set(), 'set02': set(), 'set05': set()} storage.update([split3], [], 1) - assert storage.get_feature_flags_by_set('set05') == ['split3'] + assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] assert not storage.is_flag_set_exist('set04') def test_flag_sets_withut_config_sets(self): storage = InMemorySplitStorage() - assert storage._sets_feature_flag_map == {} - assert storage.config_flag_sets_used == 0 + assert storage.flag_set_filter.flag_sets == set({}) + assert not storage.flag_set_filter.should_filter + + assert storage.flag_set.sets_feature_flag_map == {} split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set10', 'set02']) @@ -267,34 +331,34 @@ def test_flag_sets_withut_config_sets(self): split3 = Split('split3', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set04', 'set05']) storage.update([split1], [], 1) - assert storage.get_feature_flags_by_set('set10') == ['split1'] - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set10']) == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] assert storage.is_flag_set_exist('set10') assert storage.is_flag_set_exist('set02') assert not storage.is_flag_set_exist('set03') storage.update([split2], [], 1) - assert storage.get_feature_flags_by_set('set05') == ['split2'] - assert sorted(storage.get_feature_flags_by_set('set02')) == ['split1', 'split2'] + assert storage.get_feature_flags_by_sets(['set05']) == ['split2'] + assert sorted(storage.get_feature_flags_by_sets(['set02', 'set05'])) == ['split1', 'split2'] assert storage.is_flag_set_exist('set05') storage.update([], [split2.name], 1) assert not storage.is_flag_set_exist('set05') - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] split1 = Split('split1', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1, sets=['set02']) storage.update([split1], [], 1) assert not storage.is_flag_set_exist('set10') - assert storage.get_feature_flags_by_set('set02') == ['split1'] + assert storage.get_feature_flags_by_sets(['set02']) == ['split1'] storage.update([], [split1.name], 1) - assert storage.get_feature_flags_by_set('set02') == [] - assert storage._sets_feature_flag_map == {} + assert storage.get_feature_flags_by_sets(['set02']) == [] + assert storage.flag_set.sets_feature_flag_map == {} storage.update([split3], [], 1) - assert storage.get_feature_flags_by_set('set05') == ['split3'] - assert storage.get_feature_flags_by_set('set04') == ['split3'] + assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] + assert storage.get_feature_flags_by_sets(['set04', 'set05']) == ['split3'] class InMemorySegmentStorageTests(object): @@ -547,7 +611,7 @@ def test_resets(self): assert(storage._counters._auth_rejections == 0) assert(storage._counters._token_refreshes == 0) - assert(storage._method_exceptions.pop_all() == {'methodExceptions': {'treatment': 0, 'treatments': 0, 'treatment_with_config': 0, 'treatments_with_config': 0, 'track': 0}}) + assert(storage._method_exceptions.pop_all() == {'methodExceptions': {'treatment': 0, 'treatments': 0, 'treatment_with_config': 0, 'treatments_with_config': 0, 'treatments_by_flag_set': 0, 'treatments_by_flag_sets': 0, 'treatments_with_config_by_flag_set': 0, 'treatments_with_config_by_flag_sets': 0, 'track': 0}}) assert(storage._last_synchronization.get_all() == {'lastSynchronizations': {'split': 0, 'segment': 0, 'impression': 0, 'impressionCount': 0, 'event': 0, 'telemetry': 0, 'token': 0}}) assert(storage._http_sync_errors.pop_all() == {'httpErrors': {'split': {}, 'segment': {}, 'impression': {}, 'impressionCount': {}, 'event': {}, 'telemetry': {}, 'token': {}}}) assert(storage._tel_config.get_stats() == { @@ -571,7 +635,7 @@ def test_resets(self): assert(storage._streaming_events.pop_streaming_events() == {'streamingEvents': []}) assert(storage._tags == []) - assert(storage._method_latencies.pop_all() == {'methodLatencies': {'treatment': [0] * 23, 'treatments': [0] * 23, 'treatment_with_config': [0] * 23, 'treatments_with_config': [0] * 23, 'track': [0] * 23}}) + assert(storage._method_latencies.pop_all() == {'methodLatencies': {'treatment': [0] * 23, 'treatments': [0] * 23, 'treatment_with_config': [0] * 23, 'treatments_with_config': [0] * 23, 'treatments_by_flag_set': [0] * 23, 'treatments_by_flag_sets': [0] * 23, 'treatments_with_config_by_flag_set': [0] * 23, 'treatments_with_config_by_flag_sets': [0] * 23, 'track': [0] * 23}}) assert(storage._http_latencies.pop_all() == {'httpLatencies': {'split': [0] * 23, 'segment': [0] * 23, 'impression': [0] * 23, 'impressionCount': [0] * 23, 'event': [0] * 23, 'telemetry': [0] * 23, 'token': [0] * 23}}) def test_record_config(self): @@ -698,6 +762,14 @@ def _get_method_latency(self, resource, storage): return storage._method_latencies._treatment_with_config elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG: return storage._method_latencies._treatments_with_config + elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET: + return storage._method_latencies._treatments_by_flag_set + elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS: + return storage._method_latencies._treatments_by_flag_sets + elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET: + return storage._method_latencies._treatments_with_config_by_flag_set + elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS: + return storage._method_latencies._treatments_with_config_by_flag_sets elif resource == ModelTelemetry.MethodExceptionsAndLatencies.TRACK: return storage._method_latencies._track else: @@ -728,6 +800,10 @@ def test_pop_counters(self): storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS) storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG) [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG) for i in range(5)] + [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET) for i in range(3)] + [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS) for i in range(10)] + [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET) for i in range(7)] + [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS) for i in range(6)] [storage.record_exception(ModelTelemetry.MethodExceptionsAndLatencies.TRACK) for i in range(3)] exceptions = storage.pop_exceptions() assert(storage._method_exceptions._treatment == 0) @@ -735,7 +811,7 @@ def test_pop_counters(self): assert(storage._method_exceptions._treatment_with_config == 0) assert(storage._method_exceptions._treatments_with_config == 0) assert(storage._method_exceptions._track == 0) - assert(exceptions == {'methodExceptions': {'treatment': 2, 'treatments': 1, 'treatment_with_config': 1, 'treatments_with_config': 5, 'track': 3}}) + assert(exceptions == {'methodExceptions': {'treatment': 2, 'treatments': 1, 'treatment_with_config': 1, 'treatments_with_config': 5, 'treatments_by_flag_set': 3, 'treatments_by_flag_sets': 10, 'treatments_with_config_by_flag_set': 7, 'treatments_with_config_by_flag_sets': 6, 'track': 3}}) storage.add_tag('tag1') storage.add_tag('tag2') @@ -787,6 +863,10 @@ def test_pop_latencies(self): [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS, i) for i in [7, 10, 14, 13]] [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, i) for i in [200]] [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, i) for i in [50, 40]] + [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, i) for i in [15, 20]] + [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, i) for i in [14, 25]] + [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, i) for i in [100]] + [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, i) for i in [50, 20]] [storage.record_latency(ModelTelemetry.MethodExceptionsAndLatencies.TRACK, i) for i in [1, 10, 100]] latencies = storage.pop_latencies() @@ -795,8 +875,16 @@ def test_pop_latencies(self): assert(storage._method_latencies._treatment_with_config == [0] * 23) assert(storage._method_latencies._treatments_with_config == [0] * 23) assert(storage._method_latencies._track == [0] * 23) - assert(latencies == {'methodLatencies': {'treatment': [4] + [0] * 22, 'treatments': [4] + [0] * 22, - 'treatment_with_config': [1] + [0] * 22, 'treatments_with_config': [2] + [0] * 22, 'track': [3] + [0] * 22}}) + assert(latencies == {'methodLatencies': { + 'treatment': [4] + [0] * 22, + 'treatments': [4] + [0] * 22, + 'treatment_with_config': [1] + [0] * 22, + 'treatments_with_config': [2] + [0] * 22, + 'treatments_by_flag_set': [2] + [0] * 22, + 'treatments_by_flag_sets': [2] + [0] * 22, + 'treatments_with_config_by_flag_set': [1] + [0] * 22, + 'treatments_with_config_by_flag_sets': [2] + [0] * 22, + 'track': [3] + [0] * 22}}) [storage.record_sync_latency(ModelTelemetry.HTTPExceptionsAndLatencies.SPLIT, i) for i in [50, 10, 20, 40]] [storage.record_sync_latency(ModelTelemetry.HTTPExceptionsAndLatencies.SEGMENT, i) for i in [70, 100, 40, 30]] diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index 0639ab4a..39420bf4 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -166,10 +166,10 @@ def test_get(self): pluggable_split_storage = PluggableSplitStorage(self.mock_adapter, prefix=sprefix) split1 = splits.from_raw(splits_json['splitChange1_2']['splits'][0]) - feature_flag_name = splits_json['splitChange1_2']['splits'][0]['name'] + split_name = splits_json['splitChange1_2']['splits'][0]['name'] - self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=feature_flag_name), split1.to_json()) - assert(pluggable_split_storage.get(feature_flag_name).to_json() == splits.from_raw(splits_json['splitChange1_2']['splits'][0]).to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split_name), split1.to_json()) + assert(pluggable_split_storage.get(split_name).to_json() == splits.from_raw(splits_json['splitChange1_2']['splits'][0]).to_json()) assert(pluggable_split_storage.get('not_existing') == None) def test_fetch_many(self): diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index 69df2bec..65dbe97e 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -80,7 +80,15 @@ def change_number_mock(): return 123 change_number_mock._calls = 0 storage.get_change_number.side_effect = change_number_mock - storage.config_flag_sets_used = 0 + + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + + storage.flag_set_filter = flag_set_filter api = mocker.Mock() def get_changes(*args, **kwargs): @@ -167,7 +175,15 @@ def get_changes(*args, **kwargs): return { 'splits': [], 'since': 12345, 'till': 12345 } get_changes.called = 0 api.fetch_splits.side_effect = get_changes - storage.config_flag_sets_used = 0 + + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + + storage.flag_set_filter = flag_set_filter split_synchronizer = SplitSynchronizer(api, storage) split_synchronizer._backoff = Backoff(1, 1) diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index 70c61ff2..fe208b59 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -161,7 +161,14 @@ def test_sync_all(self, mocker): split_storage = mocker.Mock(spec=SplitStorage) split_storage.get_change_number.return_value = 123 split_storage.get_segment_names.return_value = ['segmentA'] - split_storage.config_flag_sets_used = 0 + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + + split_storage.flag_set_filter = flag_set_filter split_api = mocker.Mock() split_api.fetch_splits.return_value = {'splits': self.splits, 'since': 123, 'till': 123} diff --git a/tests/sync/test_telemetry.py b/tests/sync/test_telemetry.py index 11257d0f..7884bd96 100644 --- a/tests/sync/test_telemetry.py +++ b/tests/sync/test_telemetry.py @@ -32,7 +32,7 @@ def test_synchronize_telemetry(self, mocker): telemetry_storage = InMemoryTelemetryStorage() telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) split_storage = InMemorySplitStorage() - split_storage.put(Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)) + split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], 123) segment_storage = InMemorySegmentStorage() segment_storage.put(Segment('segment1', [], 123)) telemetry_submitter = InMemoryTelemetrySubmitter(telemetry_consumer, split_storage, segment_storage, api) diff --git a/tests/tasks/test_split_sync.py b/tests/tasks/test_split_sync.py index adc90724..8e8c2962 100644 --- a/tests/tasks/test_split_sync.py +++ b/tests/tasks/test_split_sync.py @@ -25,6 +25,14 @@ def change_number_mock(): change_number_mock._calls = 0 storage.get_change_number.side_effect = change_number_mock + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + api = mocker.Mock() splits = [{ 'changeNumber': 123, @@ -92,7 +100,7 @@ def get_changes(*args, **kwargs): assert mocker.call(-1, fetch_options) in api.fetch_splits.mock_calls assert mocker.call(123, fetch_options) in api.fetch_splits.mock_calls - inserted_split = storage.put.mock_calls[0][1][0] + inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index e6537580..cfe85577 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -1,13 +1,13 @@ """Storage Helper tests.""" -from splitio.util.storage_helper import update_feature_flag_storage +from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets from splitio.storage.inmemmory import InMemorySplitStorage from splitio.models import splits from tests.sync.test_splits_synchronizer import splits as split_sample class StorageHelperTests(object): - def test_helper_scenarios(self, mocker): + def test_update_feature_flag_storage(self, mocker): storage = mocker.Mock(spec=InMemorySplitStorage) split = splits.from_raw(split_sample[0]) @@ -24,13 +24,27 @@ def is_flag_set_exist(flag_set): return False storage.is_flag_set_exist = is_flag_set_exist - storage.config_flag_sets_used = 0 + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + update_feature_flag_storage(storage, [split], 123) assert self.added[0] == split assert self.deleted == [] assert self.change_number == 123 - storage.config_flag_sets_used = 2 + class flag_set_filter(): + def should_filter(): + return True + + def intersect(sets): + return False + storage.flag_set_filter = flag_set_filter + update_feature_flag_storage(storage, [split], 123) assert self.added == [] assert self.deleted[0] == split.name @@ -38,6 +52,15 @@ def is_flag_set_exist(flag_set): def is_flag_set_exist2(flag_set): return True storage.is_flag_set_exist = is_flag_set_exist2 + + class flag_set_filter(): + def should_filter(): + return True + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + update_feature_flag_storage(storage, [split], 123) assert self.added[0] == split assert self.deleted == [] @@ -71,5 +94,34 @@ def is_flag_set_exist2(flag_set): ) split = splits.from_raw(split_json) - storage.config_flag_sets_used = 0 assert update_feature_flag_storage(storage, [split], 123) == {'segment1'} + + def test_get_valid_flag_sets(self): + flag_sets = ['set1', 'set2'] + config_flag_sets = [] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1', 'set2'] + + config_flag_sets = ['set1'] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1'] + + flag_sets = ['set2', 'set3'] + config_flag_sets = ['set1', 'set2'] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set2'] + + flag_sets = ['set3', 'set4'] + config_flag_sets = ['set1', 'set2'] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] + + flag_sets = [] + config_flag_sets = ['set1', 'set2'] + assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] + + def test_combine_valid_flag_sets(self): + results_set = [{'set1', 'set2'}, {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set1', 'set2', 'set3'} + + results_set = [{}, {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} + + results_set = ['set1', {'set2', 'set3'}] + assert combine_valid_flag_sets(results_set) == {'set2', 'set3'} From 0dc547c02d67e14528e0c427683d7d0c9e5f5de2 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 11 Sep 2023 15:05:31 -0700 Subject: [PATCH 47/77] added updating fetchOptions with filter flagsets --- splitio/sync/split.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/splitio/sync/split.py b/splitio/sync/split.py index eadd75b4..209e59f1 100644 --- a/splitio/sync/split.py +++ b/splitio/sync/split.py @@ -128,6 +128,17 @@ def _attempt_feature_flag_sync(self, fetch_options, till=None): how_long = self._backoff.get() time.sleep(how_long) + def _get_config_sets(self): + """ + Get all filter flag sets cnverrted to string, if no filter flagsets exist return None + + :return: string with flagsets + :rtype: str + """ + if self._feature_flag_storage.flag_set_filter.flag_sets == set({}): + return None + return ','.join(self._feature_flag_storage.flag_set_filter.flag_sets) + def synchronize_splits(self, till=None): """ Hit endpoint, update storage and return True if sync is complete. @@ -136,7 +147,7 @@ def synchronize_splits(self, till=None): :type till: int """ final_segment_list = set() - fetch_options = FetchOptions(True) # Set Cache-Control to no-cache + fetch_options = FetchOptions(True, sets=self._get_config_sets()) # Set Cache-Control to no-cache successful_sync, remaining_attempts, change_number, segment_list = self._attempt_feature_flag_sync(fetch_options, till) final_segment_list.update(segment_list) @@ -144,7 +155,7 @@ def synchronize_splits(self, till=None): if successful_sync: # succedeed sync _LOGGER.debug('Refresh completed in %d attempts.', attempts) return final_segment_list - with_cdn_bypass = FetchOptions(True, change_number) # Set flag for bypassing CDN + with_cdn_bypass = FetchOptions(True, change_number, sets=self._get_config_sets()) # Set flag for bypassing CDN without_cdn_successful_sync, remaining_attempts, change_number, segment_list = self._attempt_feature_flag_sync(with_cdn_bypass, till) final_segment_list.update(segment_list) without_cdn_attempts = _ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES - remaining_attempts From f483caf7a55559a9ab65a133a2b1409f13f89fd1 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 12 Sep 2023 08:28:44 -0700 Subject: [PATCH 48/77] moved sorting from flagset validation to main validation --- splitio/client/config.py | 4 ++-- tests/client/test_config.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index 3576ecde..800d472f 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -153,7 +153,7 @@ def sanitize_flag_sets(flag_sets): sanitized_flag_sets.add(flag_set.strip()) - return sorted(list(sanitized_flag_sets)) + return list(sanitized_flag_sets) def sanitize(sdk_key, config): """ @@ -179,6 +179,6 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 - processed['flagSetsFilter'] = sanitize_flag_sets(processed['flagSetsFilter']) if processed['flagSetsFilter'] is not None else None + processed['flagSetsFilter'] = sorted(sanitize_flag_sets(processed['flagSetsFilter'])) if processed['flagSetsFilter'] is not None else None return processed diff --git a/tests/client/test_config.py b/tests/client/test_config.py index d12c0ab8..7a00e86d 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -73,19 +73,19 @@ def test_sanitize(self): def test_sanitize_flag_sets(self): """Test sanitization for flag sets.""" flag_sets = config.sanitize_flag_sets([' set1', 'set2 ', 'set3']) - assert flag_sets == ['set1', 'set2', 'set3'] + assert sorted(flag_sets) == ['set1', 'set2', 'set3'] flag_sets = config.sanitize_flag_sets(['1set', '_set2']) assert flag_sets == ['1set'] flag_sets = config.sanitize_flag_sets(['Set1', 'SET2']) - assert flag_sets == ['set1', 'set2'] + assert sorted(flag_sets) == ['set1', 'set2'] flag_sets = config.sanitize_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5']) assert flag_sets == [] flag_sets = config.sanitize_flag_sets(['set4', 'set1', 'set3', 'set1']) - assert flag_sets == ['set1', 'set3', 'set4'] + assert sorted(flag_sets) == ['set1', 'set3', 'set4'] flag_sets = config.sanitize_flag_sets(['w' * 50, 's' * 51]) assert flag_sets == ['w' * 50] From 17b6d2f2f6fd1aa3280d4cc95294375cd03d9d4b Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 12 Sep 2023 12:09:15 -0700 Subject: [PATCH 49/77] fixed tests and pluggable fetching flagset --- splitio/storage/pluggable.py | 7 +++--- tests/client/test_client.py | 9 ++++--- tests/integration/test_client_e2e.py | 1 - tests/storage/test_pluggable.py | 5 +--- tests/sync/test_splits_synchronizer.py | 34 +++++++++++++++++++++----- tests/sync/test_synchronizer.py | 18 ++++++++++++-- tests/tasks/test_split_sync.py | 9 +++++-- 7 files changed, 61 insertions(+), 22 deletions(-) diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 3e572569..eaea0daf 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -36,7 +36,7 @@ def __init__(self, pluggable_adapter, prefix=None, config_flag_sets=[]): self._prefix = prefix + "." + self._prefix self._traffic_type_prefix = prefix + "." + self._traffic_type_prefix self._feature_flag_till_prefix = prefix + "." + self._feature_flag_till_prefix - self._feature_flag_set_prefix = prefix + "." + self._feature_flag_till_prefix + self._feature_flag_set_prefix = prefix + "." + self._feature_flag_set_prefix def get(self, feature_flag_name): """ @@ -91,13 +91,14 @@ def get_feature_flags_by_sets(self, flag_sets): return [] keys = [self._feature_flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] - return self._pluggable_adapter.get_many(keys) + result_sets = [] + [result_sets.append(set(key)) for key in self._pluggable_adapter.get_many(keys)] + return list(combine_valid_flag_sets(result_sets)) except Exception: _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None - # TODO: To be added when producer mode is supported # def put_many(self, splits, change_number): # """ diff --git a/tests/client/test_client.py b/tests/client/test_client.py index dbcae6a4..fcddbf79 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -441,9 +441,9 @@ def test_get_treatments_by_flag_sets(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) def get_feature_flags_by_sets(flag_sets): - if flag_sets == ['set1', 'set2']: + if sorted(flag_sets) == ['set1', 'set2']: return ['f1', 'f2'] - if flag_sets == ['set3', 'set4']: + if sorted(flag_sets) == ['set3', 'set4']: return ['f3', 'f4'] if flag_sets == ['set5']: return ['some_feature'] @@ -465,6 +465,7 @@ def evaluate_features(feature_flag_names, matching_key, bucketing_key, attribute client._evaluator.evaluate_features = evaluate_features _logger = mocker.Mock() client._send_impression_to_listener = mocker.Mock() +# pytest.set_trace() assert client.get_treatments_by_flag_sets('key', ['set1', 'set2']) == {'f1': 'on', 'f2': 'on'} impressions_called = impmanager.process_impressions.mock_calls[0][1][0] @@ -623,9 +624,9 @@ def test_get_treatments_with_config_by_flag_sets(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) def get_feature_flags_by_sets(flag_sets): - if flag_sets == ['set1', 'set2']: + if sorted(flag_sets) == ['set1', 'set2']: return ['f1', 'f2'] - if flag_sets == ['set3', 'set4']: + if sorted(flag_sets) == ['set3', 'set4']: return ['f3', 'f4'] if flag_sets == ['set5']: return ['some_feature'] diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 117d29ba..b3ecf076 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -1179,7 +1179,6 @@ def setup_method(self): if split.get('sets') is not None: for flag_set in split.get('sets'): self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) - self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index 39420bf4..b472bfef 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -85,10 +85,7 @@ def get_many(self, keys): returned_keys = [] for key in self._keys: if key in keys: - if isinstance(self._keys[key], list): - returned_keys.extend(self._keys[key]) - else: - returned_keys.append(self._keys[key]) + returned_keys.append(self._keys[key]) return returned_keys def add_items(self, key, added_items): diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index 65dbe97e..8b8379e1 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -63,6 +63,14 @@ def run(x, c): run._calls = 0 api.fetch_splits.side_effect = run storage.get_change_number.return_value = -1 + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} split_synchronizer = SplitSynchronizer(api, storage) @@ -87,8 +95,8 @@ def should_filter(): def intersect(sets): return True - storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} api = mocker.Mock() def get_changes(*args, **kwargs): @@ -112,8 +120,10 @@ def get_changes(*args, **kwargs): split_synchronizer = SplitSynchronizer(api, storage) split_synchronizer.synchronize_splits() - assert mocker.call(-1, FetchOptions(True)) in api.fetch_splits.mock_calls - assert mocker.call(123, FetchOptions(True)) in api.fetch_splits.mock_calls + assert api.fetch_splits.mock_calls[0][1][0] == -1 + assert api.fetch_splits.mock_calls[0][1][1].cache_control_headers == True + assert api.fetch_splits.mock_calls[1][1][0] == 123 + assert api.fetch_splits.mock_calls[1][1][1].cache_control_headers == True inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) @@ -122,6 +132,14 @@ def get_changes(*args, **kwargs): def test_not_called_on_till(self, mocker): """Test that sync is not called when till is less than previous changenumber""" storage = mocker.Mock(spec=InMemorySplitStorage) + class flag_set_filter(): + def should_filter(): + return False + + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} def change_number_mock(): return 2 @@ -184,17 +202,21 @@ def intersect(sets): return True storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} split_synchronizer = SplitSynchronizer(api, storage) split_synchronizer._backoff = Backoff(1, 1) split_synchronizer.synchronize_splits() - assert mocker.call(-1, FetchOptions(True)) in api.fetch_splits.mock_calls - assert mocker.call(123, FetchOptions(True)) in api.fetch_splits.mock_calls + assert api.fetch_splits.mock_calls[0][1][0] == -1 + assert api.fetch_splits.mock_calls[0][1][1].cache_control_headers == True + assert api.fetch_splits.mock_calls[1][1][0] == 123 + assert api.fetch_splits.mock_calls[1][1][1].cache_control_headers == True split_synchronizer._backoff = Backoff(1, 0.1) split_synchronizer.synchronize_splits(12345) - assert mocker.call(12345, FetchOptions(True, 1234)) in api.fetch_splits.mock_calls + assert api.fetch_splits.mock_calls[3][1][0] == 1234 + assert api.fetch_splits.mock_calls[3][1][1].cache_control_headers == True assert len(api.fetch_splits.mock_calls) == 8 # 2 ok + BACKOFF(2 since==till + 2 re-attempts) + CDN(2 since==till) inserted_split = storage.update.mock_calls[0][1][0][0] diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index fe208b59..c74638a2 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -23,6 +23,13 @@ class SynchronizerTests(object): def test_sync_all_failed_splits(self, mocker): api = mocker.Mock() storage = mocker.Mock() + class flag_set_filter(): + def should_filter(): + return False + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} def run(x, c): raise APIException("something broke") @@ -41,6 +48,13 @@ def run(x, c): def test_sync_all_failed_splits_with_flagsets(self, mocker): api = mocker.Mock() storage = mocker.Mock() + class flag_set_filter(): + def should_filter(): + return False + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} def run(x, c): raise APIException("something broke", 414) @@ -164,11 +178,11 @@ def test_sync_all(self, mocker): class flag_set_filter(): def should_filter(): return False - def intersect(sets): return True - split_storage.flag_set_filter = flag_set_filter + split_storage.flag_set_filter.flag_sets = {} + split_api = mocker.Mock() split_api.fetch_splits.return_value = {'splits': self.splits, 'since': 123, 'till': 123} diff --git a/tests/tasks/test_split_sync.py b/tests/tasks/test_split_sync.py index 8e8c2962..f42daa7e 100644 --- a/tests/tasks/test_split_sync.py +++ b/tests/tasks/test_split_sync.py @@ -32,6 +32,7 @@ def should_filter(): def intersect(sets): return True storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} api = mocker.Mock() splits = [{ @@ -97,8 +98,12 @@ def get_changes(*args, **kwargs): task.stop(stop_event) stop_event.wait() assert not task.is_running() - assert mocker.call(-1, fetch_options) in api.fetch_splits.mock_calls - assert mocker.call(123, fetch_options) in api.fetch_splits.mock_calls + assert api.fetch_splits.mock_calls[0][1][0] == -1 + assert api.fetch_splits.mock_calls[0][1][1].cache_control_headers == True + assert api.fetch_splits.mock_calls[1][1][0] == 123 + assert api.fetch_splits.mock_calls[1][1][1].cache_control_headers == True +# assert mocker.call(-1, fetch_options) in api.fetch_splits.mock_calls +# assert mocker.call(123, fetch_options) in api.fetch_splits.mock_calls inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) From 6b9b8acdcbea517e14593cd79a2e14af332a12ee Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 12 Sep 2023 13:18:53 -0700 Subject: [PATCH 50/77] Fixed exception when no token is fecthed in SSE --- splitio/push/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/splitio/push/manager.py b/splitio/push/manager.py index 1fec6ea1..51f44343 100644 --- a/splitio/push/manager.py +++ b/splitio/push/manager.py @@ -143,7 +143,8 @@ def _trigger_connection_flow(self): self._feedback_loop.put(Status.PUSH_RETRYABLE_ERROR) return - if not token.push_enabled: + + if token is None or not token.push_enabled: self._feedback_loop.put(Status.PUSH_NONRETRYABLE_ERROR) return self._telemetry_runtime_producer.record_token_refreshes() From 5fca0c18d86762bac65a7f617689be63d021951a Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 13 Sep 2023 15:57:18 -0700 Subject: [PATCH 51/77] moved flagset classes to model, polish in input_validator and storage helper --- splitio/client/client.py | 10 +- splitio/client/config.py | 40 +------ splitio/client/input_validator.py | 60 ++++++++--- splitio/models/flag_sets.py | 125 ++++++++++++++++++++++ splitio/storage/inmemmory.py | 125 +--------------------- splitio/storage/pluggable.py | 14 ++- splitio/storage/redis.py | 8 +- splitio/util/storage_helper.py | 4 +- tests/client/test_client.py | 52 ++++----- tests/client/test_config.py | 28 +---- tests/client/test_input_validator.py | 52 ++++++--- tests/integration/files/splitChanges.json | 12 ++- tests/models/test_flag_sets.py | 59 ++++++++++ tests/storage/test_inmemory_storage.py | 61 +---------- tests/storage/test_pluggable.py | 43 +++++--- tests/storage/test_redis.py | 8 +- tests/util/test_storage_helper.py | 38 +++++-- 17 files changed, 387 insertions(+), 352 deletions(-) create mode 100644 splitio/models/flag_sets.py create mode 100644 tests/models/test_flag_sets.py diff --git a/splitio/client/client.py b/splitio/client/client.py index b8368d28..c81263b4 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -404,9 +404,9 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets) + feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets, method) if feature_flags_names == []: - _LOGGER.warning("No valid Flag set or no feature flags found for evaluating treatments") + _LOGGER.warning("%s: No valid Flag set or no feature flags found for evaluating treatments" % (method)) return {} if 'config' in method.value: @@ -418,7 +418,7 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} - def _get_feature_flag_names_by_flag_sets(self, flag_sets): + def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): """ Sanitize given flag sets and return list of feature flag names associated with them @@ -428,10 +428,10 @@ def _get_feature_flag_names_by_flag_sets(self, flag_sets): :return: list of feature flag names :rtype: list """ - sanitized_flag_sets = config.sanitize_flag_sets(flag_sets) + sanitized_flag_sets = input_validator.validate_flag_sets(flag_sets, method_name) feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets) if feature_flags_by_set is None: - _LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_sets)) + _LOGGER.warning("%s: Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (method_name, flag_sets)) return [] return feature_flags_by_set diff --git a/splitio/client/config.py b/splitio/client/config.py index 02a2d696..6182f2d7 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -4,11 +4,11 @@ import re from splitio.engine.impressions import ImpressionsMode +from splitio.client.input_validator import validate_flag_sets _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 -_FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$' DEFAULT_CONFIG = { 'operationMode': 'standalone', @@ -119,42 +119,6 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None): return mode, refresh_rate - -def sanitize_flag_sets(flag_sets): - """ - Check supplied flag sets list - - :param flag_set: list of flag sets - :type flag_set: list[str] - - :returns: Sanitized and sorted flag sets - :rtype: list[str] - """ - if not isinstance(flag_sets, list): - _LOGGER.warning("SDK config: FlagSets config parameters type should be list object, parameter is discarded") - return [] - - sanitized_flag_sets = set() - for flag_set in flag_sets: - if not isinstance(flag_set, str): - _LOGGER.warning("SDK config: Flag Set name %s should be str object, this flag set is discarded" % (flag_set)) - continue - if flag_set != flag_set.strip(): - _LOGGER.warning("SDK config: Flag Set name %s has extra whitespace, trimming" % (flag_set)) - flag_set = flag_set.strip() - - if flag_set != flag_set.lower(): - _LOGGER.warning("SDK config: Flag Set name %s should be all lowercase - converting string to lowercase" % (flag_set)) - flag_set = flag_set.lower() - - if re.search(_FLAG_SETS_REGEX, flag_set) is None or re.search(_FLAG_SETS_REGEX, flag_set).group() != flag_set: - _LOGGER.warning("SDK config: you passed %s, Flag Set must adhere to the regular expressions %s. This means a Flag Set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.", flag_set, _FLAG_SETS_REGEX, flag_set) - continue - - sanitized_flag_sets.add(flag_set.strip()) - - return sorted(list(sanitized_flag_sets)) - def sanitize(sdk_key, config): """ Look for inconsistencies or ill-formed configs and tune it accordingly. @@ -179,6 +143,6 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 - processed['FlagSetsFilter'] = sanitize_flag_sets(processed['FlagSetsFilter']) if processed['FlagSetsFilter'] is not None else None + processed['FlagSetsFilter'] = validate_flag_sets(processed['FlagSetsFilter'], 'SDK Config') if processed['FlagSetsFilter'] is not None else None return processed diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index a15caf91..3e135b59 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -15,6 +15,7 @@ MAX_LENGTH = 250 EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$' MAX_PROPERTIES_LENGTH_BYTES = 32768 +_FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$' def _check_not_null(value, name, operation): @@ -165,10 +166,7 @@ def _check_valid_object_key(key, name, operation): :return: The result of validation :rtype: str|None """ - if key is None: - _LOGGER.error( - '%s: you passed a null %s, %s must be a non-empty string.', - operation, name, name) + if not _check_not_null(key, 'key', operation): return None if isinstance(key, str): if not _check_string_not_empty(key, name, operation): @@ -179,7 +177,7 @@ def _check_valid_object_key(key, name, operation): return key_str -def _remove_empty_spaces(value, operation): +def _remove_empty_spaces(value, name, operation): """ Check if an string has whitespaces. @@ -192,10 +190,17 @@ def _remove_empty_spaces(value, operation): """ strip_value = value.strip() if value != strip_value: - _LOGGER.warning("%s: feature flag name '%s' has extra whitespace, trimming.", operation, value) + _LOGGER.warning("%s: %s '%s' has extra whitespace, trimming.", operation, name, value) return strip_value +def _convert_str_to_lower(value, name, operation): + lower_value = value.lower() + if value != lower_value: + _LOGGER.warning("%s: %s '%s' should be all lowercase - converting string to lowercase" % (operation, name, value)) + return lower_value + + def validate_key(key, method_name): """ Validate Key parameter for get_treatment/s. @@ -211,8 +216,7 @@ def validate_key(key, method_name): """ matching_key_result = None bucketing_key_result = None - if key is None: - _LOGGER.error('%s: you passed a null key, key must be a non-empty string.', method_name) + if not _check_not_null(key, 'key', method_name): return None, None if isinstance(key, Key): @@ -255,7 +259,7 @@ def validate_feature_flag_name(feature_flag_name, should_validate_existance, fea ) return None - return _remove_empty_spaces(feature_flag_name, method_name) + return _remove_empty_spaces(feature_flag_name, 'feature flag name', method_name) def validate_track_key(key): @@ -294,10 +298,7 @@ def validate_traffic_type(traffic_type, should_validate_existance, feature_flag_ (not _check_is_string(traffic_type, 'traffic_type', 'track')) or \ (not _check_string_not_empty(traffic_type, 'traffic_type', 'track')): return None - if not traffic_type.islower(): - _LOGGER.warning('track: %s should be all lowercase - converting string to lowercase.', - traffic_type) - traffic_type = traffic_type.lower() + traffic_type = _convert_str_to_lower(traffic_type, 'traffic type', 'track') if should_validate_existance and not feature_flag_storage.is_valid_traffic_type(traffic_type): _LOGGER.warning( @@ -390,7 +391,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name _LOGGER.error("%s: feature flag names must be a non-empty array.", method_name) return None, None filtered_feature_flags = set( - _remove_empty_spaces(feature_flag, method_name) for feature_flag in feature_flags + _remove_empty_spaces(feature_flag, 'feature flag name', method_name) for feature_flag in feature_flags if feature_flag is not None and _check_is_string(feature_flag, 'feature flag name', method_name) and _check_string_not_empty(feature_flag, 'feature flag name', method_name) @@ -566,3 +567,34 @@ def validate_pluggable_adapter(config): _LOGGER.error("Pluggable adapter method %s has less than required arguments count: %s : " % (exp_method, len(get_method_args))) return False return True + +def validate_flag_sets(flag_sets, method_name): + """ + Validate flag sets list + + :param flag_set: list of flag sets + :type flag_set: list[str] + + :returns: Sanitized and sorted flag sets + :rtype: list[str] + """ + if not isinstance(flag_sets, list): + _LOGGER.warning("%s: flag sets parameter type should be list object, parameter is discarded" % (method_name)) + return [] + + sanitized_flag_sets = set() + for flag_set in flag_sets: + if not _check_not_null(flag_set, 'flag set', method_name): + continue + if not _check_is_string(flag_set, 'flag set', method_name): + continue + flag_set = _remove_empty_spaces(flag_set, 'flag set', method_name) + flag_set = _convert_str_to_lower(flag_set, 'flag set', method_name) + + if re.search(_FLAG_SETS_REGEX, flag_set) is None or re.search(_FLAG_SETS_REGEX, flag_set).group() != flag_set: + _LOGGER.warning("%s: you passed %s, flag set must adhere to the regular expressions %s. This means a flag set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.", flag_set, _FLAG_SETS_REGEX, flag_set) + continue + + sanitized_flag_sets.add(flag_set.strip()) + + return sorted(list(sanitized_flag_sets)) diff --git a/splitio/models/flag_sets.py b/splitio/models/flag_sets.py new file mode 100644 index 00000000..0e7cd80e --- /dev/null +++ b/splitio/models/flag_sets.py @@ -0,0 +1,125 @@ +"""Flagsets classes.""" +import threading + +class FlagSetsFilter(object): + """Config Flagsets Filter storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self.flag_sets = set(flag_sets) + self.should_filter = any(flag_sets) + + def set_exist(self, flag_set): + """ + Check if a flagset exist in flagset filter + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + if not self.should_filter: + return True + if not isinstance(flag_set, str) or flag_set == '': + return False + + return any(self.flag_sets.intersection(set([flag_set]))) + + def intersect(self, flag_sets): + """ + Check if a set exist in config flagset filter + + :param flag_set: set of flagsets + :type flag_set: set + + :rtype: bool + """ + if not self.should_filter: + return True + if not isinstance(flag_sets, set) or len(flag_sets) == 0: + return False + return any(self.flag_sets.intersection(flag_sets)) + + +class FlagSets(object): + """InMemory Flagsets storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self._lock = threading.RLock() + self.sets_feature_flag_map = {} + for flag_set in flag_sets: + self.sets_feature_flag_map[flag_set] = set() + + def flag_set_exist(self, flag_set): + """ + Check if a flagset exist in stored flagset + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + with self._lock: + return flag_set in self.sets_feature_flag_map.keys() + + def get_flag_set(self, flag_set): + """ + fetch feature flags stored in a flag set + + :param flag_set: set name + :type flag_set: str + + :rtype: list(str) + """ + with self._lock: + if self.flag_set_exist(flag_set): + return self.sets_feature_flag_map[flag_set] + + def add_flag_set(self, flag_set): + """ + Add new flag set to storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if not self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set] = set() + + def remove_flag_set(self, flag_set): + """ + Remove existing flag set from storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + del self.sets_feature_flag_map[flag_set] + + def add_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Add a feature flag to existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].add(feature_flag) + + def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Remove a feature flag from existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].remove(feature_flag) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index d1e87abd..acbae771 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -6,6 +6,7 @@ from splitio.models.segments import Segment from splitio.models.telemetry import HTTPErrors, HTTPLatencies, MethodExceptions, MethodLatencies, LastSynchronization, StreamingEvents, TelemetryConfig, TelemetryCounters, CounterConstants +from splitio.models.flag_sets import FlagSets, FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage MAX_SIZE_BYTES = 5 * 1024 * 1024 @@ -13,130 +14,6 @@ _LOGGER = logging.getLogger(__name__) -class FlagSetsFilter(object): - """Config Flagsets Filter storage.""" - - def __init__(self, flag_sets=[]): - """Constructor.""" - self.flag_sets = set(flag_sets) - self.should_filter = any(flag_sets) - - def set_exist(self, flag_set): - """ - Check if a flagset exist in flagset filter - - :param flag_set: set name - :type flag_set: str - - :rtype: bool - """ - if not self.should_filter: - return True - if not isinstance(flag_set, str) or flag_set == '': - return False - - return any(self.flag_sets.intersection(set([flag_set]))) - - def intersect(self, flag_sets): - """ - Check if a set exist in config flagset filter - - :param flag_set: set of flagsets - :type flag_set: set - - :rtype: bool - """ - if not self.should_filter: - return True - if not isinstance(flag_sets, set) or len(flag_sets) == 0: - return False - return any(self.flag_sets.intersection(flag_sets)) - - -class FlagSets(object): - """InMemory Flagsets storage.""" - - def __init__(self, flag_sets=[]): - """Constructor.""" - self._lock = threading.RLock() - self.sets_feature_flag_map = {} - for flag_set in flag_sets: - self.sets_feature_flag_map[flag_set] = set() - - def flag_set_exist(self, flag_set): - """ - Check if a flagset exist in stored flagset - - :param flag_set: set name - :type flag_set: str - - :rtype: bool - """ - with self._lock: - return flag_set in self.sets_feature_flag_map.keys() - - def get_flag_set(self, flag_set): - """ - fetch feature flags stored in a flag set - - :param flag_set: set name - :type flag_set: str - - :rtype: list(str) - """ - with self._lock: - if self.flag_set_exist(flag_set): - return self.sets_feature_flag_map[flag_set] - - def add_flag_set(self, flag_set): - """ - Add new flag set to storage - - :param flag_set: set name - :type flag_set: str - """ - with self._lock: - if not self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set] = set() - - def remove_flag_set(self, flag_set): - """ - Remove existing flag set from storage - - :param flag_set: set name - :type flag_set: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - del self.sets_feature_flag_map[flag_set] - - def add_feature_flag_to_flag_set(self, flag_set, feature_flag): - """ - Add a feature flag to existing flag set - - :param flag_set: set name - :type flag_set: str - :param feature_flag: feature flag name - :type feature_flag: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set].add(feature_flag) - - def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): - """ - Remove a feature flag from existing flag set - - :param flag_set: set name - :type flag_set: str - :param feature_flag: feature flag name - :type feature_flag: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set].remove(feature_flag) - - class InMemorySplitStorage(SplitStorage): """InMemory implementation of a feature flag storage.""" diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 4a9db0b9..778f9e9a 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -7,6 +7,7 @@ from splitio.models import splits, segments from splitio.models.impressions import Impression from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MAX_TAGS, get_latency_bucket_index +from splitio.models.flag_sets import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets @@ -31,12 +32,13 @@ def __init__(self, pluggable_adapter, prefix=None, config_flag_sets=[]): self._prefix = "SPLITIO.split.{feature_flag_name}" self._traffic_type_prefix = "SPLITIO.trafficType.{traffic_type_name}" self._feature_flag_till_prefix = "SPLITIO.splits.till" - self._feature_flag_set_prefix = 'SPLITIO.set.{flag_set}' + self._flag_set_prefix = 'SPLITIO.flagSet.{flag_set}' + self.flag_set_filter = FlagSetsFilter(config_flag_sets) if prefix is not None: self._prefix = prefix + "." + self._prefix self._traffic_type_prefix = prefix + "." + self._traffic_type_prefix self._feature_flag_till_prefix = prefix + "." + self._feature_flag_till_prefix - self._feature_flag_set_prefix = prefix + "." + self._feature_flag_till_prefix + self._flag_set_prefix = prefix + "." + self._flag_set_prefix def get(self, feature_flag_name): """ @@ -86,12 +88,14 @@ def get_feature_flags_by_sets(self, flag_sets): :rtype: listt(str) """ try: - sets_to_fetch = get_valid_flag_sets(flag_sets, self._config_flag_sets) + sets_to_fetch = get_valid_flag_sets(flag_sets, self.flag_set_filter) if sets_to_fetch == []: return [] - keys = [self._feature_flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] - return self._pluggable_adapter.get_many(keys) + keys = [self._flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] + result_sets = [] + [result_sets.append(set(key)) for key in self._pluggable_adapter.get_many(keys)] + return list(combine_valid_flag_sets(result_sets)) except Exception: _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index d39d6054..cb4b8a6b 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -6,13 +6,13 @@ from splitio.models.impressions import Impression from splitio.models import splits, segments from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, get_latency_bucket_index +from splitio.models.flag_sets import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, \ ImpressionPipelinedStorage, TelemetryStorage from splitio.storage.adapters.redis import RedisAdapterException from splitio.storage.adapters.cache_trait import decorate as add_cache, DEFAULT_MAX_AGE from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets - _LOGGER = logging.getLogger(__name__) MAX_TAGS = 10 @@ -22,7 +22,7 @@ class RedisSplitStorage(SplitStorage): _FEATURE_FLAG_KEY = 'SPLITIO.split.{feature_flag_name}' _FEATURE_FLAG_TILL_KEY = 'SPLITIO.splits.till' _TRAFFIC_TYPE_KEY = 'SPLITIO.trafficType.{traffic_type_name}' - _SET_KEY = 'SPLITIO.set.{flag_set}' + _SET_KEY = 'SPLITIO.flagSet.{flag_set}' def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, config_flag_sets=[]): """ @@ -32,7 +32,7 @@ def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, :type redis_client: splitio.storage.adapters.redis.RedisAdapter """ self._redis = redis_client - self._config_flag_sets = config_flag_sets + self.flag_set_filter = FlagSetsFilter(config_flag_sets) self._pipe = self._redis.pipeline if enable_caching: self.get = add_cache(lambda *p, **_: p[0], max_age)(self.get) @@ -106,7 +106,7 @@ def get_feature_flags_by_sets(self, flag_sets): :rtype: listt(str) """ try: - sets_to_fetch = get_valid_flag_sets(flag_sets, self._config_flag_sets) + sets_to_fetch = get_valid_flag_sets(flag_sets, self.flag_set_filter) if sets_to_fetch == []: return [] diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index c8667da2..d8f5fbd8 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -33,7 +33,7 @@ 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 get_valid_flag_sets(flag_sets, config_flag_sets): +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 @@ -47,7 +47,7 @@ def get_valid_flag_sets(flag_sets, config_flag_sets): """ sets_to_fetch = [] for flag_set in flag_sets: - if flag_set not in config_flag_sets and len(config_flag_sets) > 0: + if not flag_set_filter.set_exist(flag_set) and flag_set_filter.should_filter: _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) continue sets_to_fetch.append(flag_set) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 6c78a3ff..d07c3baa 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -353,14 +353,14 @@ def test_get_treatments_by_flag_set(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - def get_feature_flags_by_set(flag_sets): - if flag_sets == 'set1': + def get_feature_flags_by_sets(flag_sets): + if flag_sets == ['set1']: return ['f1', 'f2'] - if flag_sets == 'set2': + if flag_sets == ['set2']: return ['f3', 'f4'] - if flag_sets == 'set3': + if flag_sets == ['set3']: return ['some_feature'] - split_storage.get_feature_flags_by_set = get_feature_flags_by_set + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) @@ -440,18 +440,14 @@ def test_get_treatments_by_flag_sets(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - def get_feature_flags_by_set(flag_sets): - if flag_sets == 'set1': - return ['f1'] - if flag_sets == 'set2': - return ['f2'] - if flag_sets == 'set3': + def get_feature_flags_by_sets(flag_sets): + if sorted(flag_sets) == ['set1', 'set2']: + return ['f1', 'f2'] + if sorted(flag_sets) == ['set3', 'set4']: return ['f3', 'f4'] - if flag_sets == 'set4': - return [] - if flag_sets == 'set5': + if flag_sets == ['set5']: return ['some_feature'] - split_storage.get_feature_flags_by_set = get_feature_flags_by_set + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) @@ -530,14 +526,14 @@ def test_get_treatments_with_config_by_flag_set(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - def get_feature_flags_by_set(flag_sets): - if flag_sets == 'set1': + def get_feature_flags_by_sets(flag_sets): + if flag_sets == ['set1']: return ['f1', 'f2'] - if flag_sets == 'set2': + if flag_sets == ['set2']: return ['f3', 'f4'] - if flag_sets == 'set3': + if flag_sets == ['set3']: return ['some_feature'] - split_storage.get_feature_flags_by_set = get_feature_flags_by_set + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) @@ -626,18 +622,14 @@ def test_get_treatments_with_config_by_flag_sets(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - def get_feature_flags_by_set(flag_sets): - if flag_sets == 'set1': - return ['f1'] - if flag_sets == 'set2': - return ['f2'] - if flag_sets == 'set3': + def get_feature_flags_by_sets(flag_sets): + if sorted(flag_sets) == ['set1', 'set2']: + return ['f1', 'f2'] + if sorted(flag_sets) == ['set3', 'set4']: return ['f3', 'f4'] - if flag_sets == 'set4': - return [] - if flag_sets == 'set5': + if flag_sets == ['set5']: return ['some_feature'] - split_storage.get_feature_flags_by_set = get_feature_flags_by_set + split_storage.get_feature_flags_by_sets = get_feature_flags_by_sets client = Client(factory, recorder, True) client._evaluator = mocker.Mock(spec=Evaluator) diff --git a/tests/client/test_config.py b/tests/client/test_config.py index d12c0ab8..fdda0f84 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -68,30 +68,4 @@ def test_sanitize(self): configs = {} processed = config.sanitize('some', configs) - assert processed['redisLocalCacheEnabled'] # check default is True - - def test_sanitize_flag_sets(self): - """Test sanitization for flag sets.""" - flag_sets = config.sanitize_flag_sets([' set1', 'set2 ', 'set3']) - assert flag_sets == ['set1', 'set2', 'set3'] - - flag_sets = config.sanitize_flag_sets(['1set', '_set2']) - assert flag_sets == ['1set'] - - flag_sets = config.sanitize_flag_sets(['Set1', 'SET2']) - assert flag_sets == ['set1', 'set2'] - - flag_sets = config.sanitize_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5']) - assert flag_sets == [] - - flag_sets = config.sanitize_flag_sets(['set4', 'set1', 'set3', 'set1']) - assert flag_sets == ['set1', 'set3', 'set4'] - - flag_sets = config.sanitize_flag_sets(['w' * 50, 's' * 51]) - assert flag_sets == ['w' * 50] - - flag_sets = config.sanitize_flag_sets('set1') - assert flag_sets == [] - - flag_sets = config.sanitize_flag_sets([12, 33]) - assert flag_sets == [] + assert processed['redisLocalCacheEnabled'] # check default is True \ No newline at end of file diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index bceb39b0..df82b6cf 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -55,7 +55,7 @@ def test_get_treatment(self, mocker): assert client.get_treatment(None, 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatment') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -139,7 +139,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment(Key(None, 'bucketing_key'), 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'matching_key', 'matching_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -188,7 +188,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment(Key('matching_key', None), 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'bucketing_key', 'bucketing_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -232,7 +232,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment('matching_key', ' some_feature ', None) == 'default_treatment' assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatment', ' some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatment', 'feature flag name', ' some_feature ') ] _logger.reset_mock() @@ -289,7 +289,7 @@ def _configs(treatment): assert client.get_treatment_with_config(None, 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatment_with_config') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -373,7 +373,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config(Key(None, 'bucketing_key'), 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'matching_key', 'matching_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -422,7 +422,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config(Key('matching_key', None), 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'bucketing_key', 'bucketing_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -466,7 +466,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config('matching_key', ' some_feature ', None) == ('default_treatment', '{"some": "property"}') assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatment_with_config', ' some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatment_with_config', 'feature flag name', ' some_feature ') ] _logger.reset_mock() @@ -629,7 +629,7 @@ def test_track(self, mocker): _logger.reset_mock() assert client.track("some_key", "TRAFFIC_type", "event_type", 1) is True assert _logger.warning.mock_calls == [ - mocker.call("track: %s should be all lowercase - converting string to lowercase.", 'TRAFFIC_type') + mocker.call("track: traffic type 'TRAFFIC_type' should be all lowercase - converting string to lowercase") ] assert client.track("some_key", "traffic_type", None, 1) is False @@ -837,7 +837,7 @@ def test_get_treatments(self, mocker): assert client.get_treatments(None, ['some_feature']) == {'some_feature': CONTROL} assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatments') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatments', 'key', 'key') ] _logger.reset_mock() @@ -912,7 +912,7 @@ def test_get_treatments(self, mocker): _logger.reset_mock() assert client.get_treatments('some_key', ['some ']) == {'some': 'default_treatment'} assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatments', 'some ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatments', 'feature flag name', 'some ') ] _logger.reset_mock() @@ -978,7 +978,7 @@ def _configs(treatment): assert client.get_treatments_with_config(None, ['some_feature']) == {'some_feature': (CONTROL, None)} assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatments_with_config') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatments_with_config', 'key', 'key') ] _logger.reset_mock() @@ -1053,7 +1053,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatments_with_config('some_key', ['some_feature ']) == {'some_feature': ('default_treatment', '{"some": "property"}')} assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatments_with_config', 'some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatments_with_config', 'feature flag name', 'some_feature ') ] _logger.reset_mock() @@ -1265,3 +1265,29 @@ def test_validate_pluggable_adapter(self): # using non-string type prefix should not pass assert(not input_validator.validate_pluggable_adapter({'storageType': 'pluggable', 'storagePrefix': 'myprefix', 123: self.mock_adapter4()})) + + def test_sanitize_flag_sets(self): + """Test sanitization for flag sets.""" + flag_sets = input_validator.validate_flag_sets([' set1', 'set2 ', 'set3']) + assert flag_sets == ['set1', 'set2', 'set3'] + + flag_sets = input_validator.validate_flag_sets(['1set', '_set2']) + assert flag_sets == ['1set'] + + flag_sets = input_validator.validate_flag_sets(['Set1', 'SET2']) + assert flag_sets == ['set1', 'set2'] + + flag_sets = input_validator.validate_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5']) + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets(['set4', 'set1', 'set3', 'set1']) + assert flag_sets == ['set1', 'set3', 'set4'] + + flag_sets = input_validator.validate_flag_sets(['w' * 50, 's' * 51]) + assert flag_sets == ['w' * 50] + + flag_sets = input_validator.validate_flag_sets('set1') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets([12, 33]) + assert flag_sets == [] diff --git a/tests/integration/files/splitChanges.json b/tests/integration/files/splitChanges.json index d5401c93..f77ce97e 100644 --- a/tests/integration/files/splitChanges.json +++ b/tests/integration/files/splitChanges.json @@ -58,7 +58,8 @@ } ] } - ] + ], + "sets": ["set1", "set2"] }, { "orgId": null, @@ -95,7 +96,8 @@ } ] } - ] + ], + "sets": ["set4"] }, { "orgId": null, @@ -136,7 +138,8 @@ } ] } - ] + ], + "sets": ["set3"] }, { "orgId": null, @@ -199,7 +202,8 @@ } ] } - ] + ], + "sets": ["set1"] }, { "orgId": null, diff --git a/tests/models/test_flag_sets.py b/tests/models/test_flag_sets.py new file mode 100644 index 00000000..fddff1c6 --- /dev/null +++ b/tests/models/test_flag_sets.py @@ -0,0 +1,59 @@ +from splitio.models.flag_sets import FlagSets, FlagSetsFilter + +class FlagSetsFilterTests(object): + """Flag sets filter storage tests.""" + def test_without_initial_set(self): + flag_set = FlagSets() + assert flag_set.sets_feature_flag_map == {} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == False + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_with_initial_set(self): + flag_set = FlagSets(['set1', 'set2']) + assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == True + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_flag_set_filter(self): + flag_set_filter = FlagSetsFilter() + assert flag_set_filter.flag_sets == set() + assert not flag_set_filter.should_filter + + flag_set_filter = FlagSetsFilter(['set1', 'set2']) + assert flag_set_filter.flag_sets == set({'set1', 'set2'}) + assert flag_set_filter.should_filter + assert flag_set_filter.intersect(set({'set1', 'set2'})) + assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) + assert not flag_set_filter.intersect(set({'set4'})) + assert not flag_set_filter.set_exist('set4') + assert flag_set_filter.set_exist('set1') diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 67501272..e50a14ab 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -10,66 +10,7 @@ import splitio.models.telemetry as ModelTelemetry from splitio.engine.telemetry import TelemetryStorageProducer from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ - InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, FlagSets, FlagSetsFilter - - -class FlagSetsFilterTests(object): - """Flag sets filter storage tests.""" - def test_without_initial_set(self): - flag_set = FlagSets() - assert flag_set.sets_feature_flag_map == {} - - flag_set.add_flag_set('set1') - assert flag_set.get_flag_set('set1') == set({}) - assert flag_set.flag_set_exist('set1') == True - assert flag_set.flag_set_exist('set2') == False - - flag_set.add_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split1'} - flag_set.add_feature_flag_to_flag_set('set1', 'split2') - assert flag_set.get_flag_set('set1') == {'split1', 'split2'} - flag_set.remove_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split2'} - flag_set.remove_flag_set('set2') - assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} - flag_set.remove_flag_set('set1') - assert flag_set.sets_feature_flag_map == {} - assert flag_set.flag_set_exist('set1') == False - - def test_with_initial_set(self): - flag_set = FlagSets(['set1', 'set2']) - assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} - - flag_set.add_flag_set('set1') - assert flag_set.get_flag_set('set1') == set({}) - assert flag_set.flag_set_exist('set1') == True - assert flag_set.flag_set_exist('set2') == True - - flag_set.add_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split1'} - flag_set.add_feature_flag_to_flag_set('set1', 'split2') - assert flag_set.get_flag_set('set1') == {'split1', 'split2'} - flag_set.remove_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split2'} - flag_set.remove_flag_set('set2') - assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} - flag_set.remove_flag_set('set1') - assert flag_set.sets_feature_flag_map == {} - assert flag_set.flag_set_exist('set1') == False - - def test_flag_set_filter(self): - flag_set_filter = FlagSetsFilter() - assert flag_set_filter.flag_sets == set() - assert not flag_set_filter.should_filter - - flag_set_filter = FlagSetsFilter(['set1', 'set2']) - assert flag_set_filter.flag_sets == set({'set1', 'set2'}) - assert flag_set_filter.should_filter - assert flag_set_filter.intersect(set({'set1', 'set2'})) - assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) - assert not flag_set_filter.intersect(set({'set4'})) - assert not flag_set_filter.set_exist('set4') - assert flag_set_filter.set_exist('set1') + InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage class InMemorySplitStorageTests(object): diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index bcfde8f9..aa381791 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -1,18 +1,18 @@ """Pluggable storage test module.""" import json import threading +import pytest from splitio.models.splits import Split from splitio.models import splits, segments from splitio.models.segments import Segment from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper +from splitio.models.flag_sets import FlagSetsFilter from splitio.storage.pluggable import PluggableSplitStorage, PluggableSegmentStorage, PluggableImpressionsStorage, PluggableEventsStorage, PluggableTelemetryStorage from splitio.client.util import get_metadata, SdkMetadata from splitio.models.telemetry import MAX_TAGS, MethodExceptionsAndLatencies, OperationMode - from tests.integration import splits_json -import pytest class StorageMockAdapter(object): def __init__(self): @@ -85,10 +85,7 @@ def get_many(self, keys): returned_keys = [] for key in self._keys: if key in keys: - if isinstance(self._keys[key], list): - returned_keys.extend(self._keys[key]) - else: - returned_keys.append(self._keys[key]) + returned_keys.append(self._keys[key]) return returned_keys def add_items(self, key, added_items): @@ -141,9 +138,10 @@ def test_init(self): prefix = 'myprefix.' else: prefix = '' - assert(pluggable_split_storage._prefix == prefix + "SPLITIO.split.{split_name}") + assert(pluggable_split_storage._prefix == prefix + "SPLITIO.split.{feature_flag_name}") assert(pluggable_split_storage._traffic_type_prefix == prefix + "SPLITIO.trafficType.{traffic_type_name}") - assert(pluggable_split_storage._split_till_prefix == prefix + "SPLITIO.splits.till") + assert(pluggable_split_storage._flag_set_prefix == prefix + "SPLITIO.flagSet.{flag_set}") + assert(pluggable_split_storage._feature_flag_till_prefix == prefix + "SPLITIO.splits.till") # TODO: To be added when producer mode is aupported # def test_put_many(self): @@ -168,7 +166,7 @@ def test_get(self): split1 = splits.from_raw(splits_json['splitChange1_2']['splits'][0]) split_name = splits_json['splitChange1_2']['splits'][0]['name'] - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split_name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split_name), split1.to_json()) assert(pluggable_split_storage.get(split_name).to_json() == splits.from_raw(splits_json['splitChange1_2']['splits'][0]).to_json()) assert(pluggable_split_storage.get('not_existing') == None) @@ -181,8 +179,8 @@ def test_fetch_many(self): split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) fetched = pluggable_split_storage.fetch_many([split1.name, split2.name]) assert(fetched[split1.name].to_json() == split1.to_json()) assert(fetched[split2.name].to_json() == split2.to_json()) @@ -220,8 +218,8 @@ def test_get_split_names(self): split2_temp = splits_json['splitChange1_2']['splits'][0].copy() split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) assert(pluggable_split_storage.get_split_names() == [split1.name, split2.name]) def test_get_all(self): @@ -233,11 +231,26 @@ def test_get_all(self): split2_temp['name'] = 'another_split' split2 = splits.from_raw(split2_temp) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split1.name), split1.to_json()) - self.mock_adapter.set(pluggable_split_storage._prefix.format(split_name=split2.name), split2.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split1.name), split1.to_json()) + self.mock_adapter.set(pluggable_split_storage._prefix.format(feature_flag_name=split2.name), split2.to_json()) all_splits = pluggable_split_storage.get_all() assert([all_splits[0].to_json(), all_splits[1].to_json()] == [split1.to_json(), split2.to_json()]) + def test_flag_sets(self, mocker): + """Test Flag sets scenarios.""" + self.mock_adapter._keys = {'SPLITIO.flagSet.set1': ['split1'], 'SPLITIO.flagSet.set2': ['split1','split2']} + pluggable_split_storage = PluggableSplitStorage(self.mock_adapter) + assert pluggable_split_storage.flag_set_filter.flag_sets == set({}) + assert sorted(pluggable_split_storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] + + pluggable_split_storage.flag_set_filter = FlagSetsFilter(['set2', 'set3']) + assert pluggable_split_storage.get_feature_flags_by_sets(['set1']) == [] + assert sorted(pluggable_split_storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] + + storage2 = PluggableSplitStorage(self.mock_adapter, None, ['set2', 'set3']) + assert storage2.flag_set_filter.flag_sets == set({'set2', 'set3'}) + + # TODO: To be added when producer mode is aupported # def test_kill_locally(self): # self.mock_adapter._keys = {} diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 7ee00ca8..0413ca8b 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -14,7 +14,7 @@ from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MethodExceptionsAndLatencies - +from splitio.models.flag_sets import FlagSetsFilter class RedisSplitStorageTests(object): """Redis split storage test cases.""" @@ -177,15 +177,15 @@ def test_flag_sets(self, mocker): """Test Flag sets scenarios.""" adapter = build({}) storage = RedisSplitStorage(adapter, True, 1) - assert storage._config_flag_sets == [] + assert storage.flag_set_filter.flag_sets == set({}) assert sorted(storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] - storage._config_flag_sets = ['set2', 'set3'] + storage.flag_set_filter = FlagSetsFilter(['set2', 'set3']) assert storage.get_feature_flags_by_sets(['set1']) == [] assert sorted(storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) - assert storage2._config_flag_sets == ['set2', 'set3'] + assert storage2.flag_set_filter.flag_sets == set({'set2', 'set3'}) class RedisSegmentStorageTests(object): """Redis segment storage test cases.""" diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index 8c148942..e59e9a4d 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -1,8 +1,10 @@ """Storage Helper tests.""" +import pytest from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets from splitio.storage.inmemmory import InMemorySplitStorage from splitio.models import splits +from splitio.models.flag_sets import FlagSetsFilter from tests.sync.test_splits_synchronizer import splits as split_sample class StorageHelperTests(object): @@ -24,17 +26,39 @@ def is_flag_set_exist(flag_set): return False storage.is_flag_set_exist = is_flag_set_exist - storage.config_flag_sets_used = 0 + class flag_set_filter(): + def should_filter(): + return False + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} + update_feature_flag_storage(storage, [split], 123) assert self.added[0] == split assert self.deleted == [] assert self.change_number == 123 - storage.config_flag_sets_used = 2 + class flag_set_filter2(): + def should_filter(): + return True + def intersect(sets): + return False + storage.flag_set_filter = flag_set_filter2 + storage.flag_set_filter.flag_sets = set({'set1', 'set2'}) + update_feature_flag_storage(storage, [split], 123) assert self.added == [] assert self.deleted[0] == split.name + class flag_set_filter3(): + def should_filter(): + return True + def intersect(sets): + return True + storage.flag_set_filter = flag_set_filter3 + storage.flag_set_filter.flag_sets = set({'set1', 'set2'}) + def is_flag_set_exist2(flag_set): return True storage.is_flag_set_exist = is_flag_set_exist2 @@ -76,22 +100,22 @@ def is_flag_set_exist2(flag_set): def test_get_valid_flag_sets(self): flag_sets = ['set1', 'set2'] - config_flag_sets = [] + config_flag_sets = FlagSetsFilter([]) assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1', 'set2'] - config_flag_sets = ['set1'] + config_flag_sets = FlagSetsFilter(['set1']) assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1'] flag_sets = ['set2', 'set3'] - config_flag_sets = ['set1', 'set2'] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set2'] flag_sets = ['set3', 'set4'] - config_flag_sets = ['set1', 'set2'] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] flag_sets = [] - config_flag_sets = ['set1', 'set2'] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] def test_combine_valid_flag_sets(self): From 1157c93e45c048595003ea7170616776d9dc8b87 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 13 Sep 2023 16:36:10 -0700 Subject: [PATCH 52/77] polish --- splitio/client/input_validator.py | 15 +++++++------- splitio/models/flag_sets.py | 3 +-- tests/client/test_input_validator.py | 31 ++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 3e135b59..0a65f310 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -80,7 +80,7 @@ def _check_string_not_empty(value, name, operation): return True -def _check_string_matches(value, operation, pattern): +def _check_string_matches(value, operation, pattern, name): """ Check if value is adhere to a regular expression passed. @@ -93,14 +93,14 @@ def _check_string_matches(value, operation, pattern): :return: The result of validation :rtype: True|False """ - if not re.match(pattern, value): + if re.search(pattern, value) is None or re.search(pattern, value).group() != value: _LOGGER.error( '%s: you passed %s, event_type must ' + 'adhere to the regular expression %s. ' + - 'This means an event name must be alphanumeric, cannot be more ' + + 'This means %s must be alphanumeric, cannot be more ' + 'than 80 characters long, and can only include a dash, underscore, ' + 'period, or colon as separators of alphanumeric characters.', - operation, value, pattern + operation, value, pattern, name ) return False return True @@ -323,7 +323,7 @@ def validate_event_type(event_type): if (not _check_not_null(event_type, 'event_type', 'track')) or \ (not _check_is_string(event_type, 'event_type', 'track')) or \ (not _check_string_not_empty(event_type, 'event_type', 'track')) or \ - (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN)): + (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN, 'an event name')): return None return event_type @@ -591,10 +591,9 @@ def validate_flag_sets(flag_sets, method_name): flag_set = _remove_empty_spaces(flag_set, 'flag set', method_name) flag_set = _convert_str_to_lower(flag_set, 'flag set', method_name) - if re.search(_FLAG_SETS_REGEX, flag_set) is None or re.search(_FLAG_SETS_REGEX, flag_set).group() != flag_set: - _LOGGER.warning("%s: you passed %s, flag set must adhere to the regular expressions %s. This means a flag set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.", flag_set, _FLAG_SETS_REGEX, flag_set) + if not _check_string_matches(flag_set, method_name, _FLAG_SETS_REGEX, 'a flag set'): continue - sanitized_flag_sets.add(flag_set.strip()) + sanitized_flag_sets.add(flag_set) return sorted(list(sanitized_flag_sets)) diff --git a/splitio/models/flag_sets.py b/splitio/models/flag_sets.py index 0e7cd80e..a01de740 100644 --- a/splitio/models/flag_sets.py +++ b/splitio/models/flag_sets.py @@ -73,8 +73,7 @@ def get_flag_set(self, flag_set): :rtype: list(str) """ with self._lock: - if self.flag_set_exist(flag_set): - return self.sets_feature_flag_map[flag_set] + return self.sets_feature_flag_map.get(flag_set) def add_flag_set(self, flag_set): """ diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index df82b6cf..abe3f5c4 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -666,10 +666,10 @@ def test_track(self, mocker): assert _logger.error.mock_calls == [ mocker.call("%s: you passed %s, event_type must adhere to the regular " "expression %s. This means " - "an event name must be alphanumeric, cannot be more than 80 " + "%s must be alphanumeric, cannot be more than 80 " "characters long, and can only include a dash, underscore, " "period, or colon as separators of alphanumeric characters.", - 'track', '@@', '^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$') + 'track', '@@', '^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$', 'an event name') ] _logger.reset_mock() @@ -1074,6 +1074,33 @@ def _configs(treatment): ) ] + def test_flag_sets_validation(self): + """Test sanitization for flag sets.""" + flag_sets = input_validator.validate_flag_sets([' set1', 'set2 ', 'set3'], 'method') + assert sorted(flag_sets) == ['set1', 'set2', 'set3'] + + flag_sets = input_validator.validate_flag_sets(['1set', '_set2'], 'method') + assert flag_sets == ['1set'] + + flag_sets = input_validator.validate_flag_sets(['Set1', 'SET2'], 'method') + assert sorted(flag_sets) == ['set1', 'set2'] + + flag_sets = input_validator.validate_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5'], 'method') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets(['set4', 'set1', 'set3', 'set1'], 'method') + assert sorted(flag_sets) == ['set1', 'set3', 'set4'] + + flag_sets = input_validator.validate_flag_sets(['w' * 50, 's' * 51], 'method') + assert flag_sets == ['w' * 50] + + flag_sets = input_validator.validate_flag_sets('set1', 'method') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets([12, 33], 'method') + assert flag_sets == [] + + class ManagerInputValidationTests(object): #pylint: disable=too-few-public-methods """Manager input validation test cases.""" From 43ec2a42a82277f12c746b37135df550555ada74 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 13 Sep 2023 16:50:45 -0700 Subject: [PATCH 53/77] polish --- splitio/storage/pluggable.py | 5 ++--- splitio/storage/redis.py | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 778f9e9a..02e58b6e 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -28,7 +28,6 @@ def __init__(self, pluggable_adapter, prefix=None, config_flag_sets=[]): :type prefix: str """ self._pluggable_adapter = pluggable_adapter - self._config_flag_sets = config_flag_sets self._prefix = "SPLITIO.split.{feature_flag_name}" self._traffic_type_prefix = "SPLITIO.trafficType.{traffic_type_name}" self._feature_flag_till_prefix = "SPLITIO.splits.till" @@ -81,8 +80,8 @@ def get_feature_flags_by_sets(self, flag_sets): """ Retrieve feature flags by flag set. - :param flag_set: Names of the flag set to fetch. - :type flag_set: str + :param flag_sets: List of flag sets to fetch. + :type flag_sets: list(str) :return: Feature flag names that are tagged with the flag set :rtype: listt(str) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index cb4b8a6b..5c7ae450 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -22,7 +22,7 @@ class RedisSplitStorage(SplitStorage): _FEATURE_FLAG_KEY = 'SPLITIO.split.{feature_flag_name}' _FEATURE_FLAG_TILL_KEY = 'SPLITIO.splits.till' _TRAFFIC_TYPE_KEY = 'SPLITIO.trafficType.{traffic_type_name}' - _SET_KEY = 'SPLITIO.flagSet.{flag_set}' + _FLAG_SET_KEY = 'SPLITIO.flagSet.{flag_set}' def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, config_flag_sets=[]): """ @@ -63,7 +63,7 @@ def _get_traffic_type_key(self, traffic_type_name): """ return self._TRAFFIC_TYPE_KEY.format(traffic_type_name=traffic_type_name) - def _get_set_key(self, flag_set): + def _get_flag_set_key(self, flag_set): """ Use the provided flag set to build the appropriate redis key. @@ -73,7 +73,7 @@ def _get_set_key(self, flag_set): :return: Redis key. :rtype: str. """ - return self._SET_KEY.format(flag_set=flag_set) + return self._FLAG_SET_KEY.format(flag_set=flag_set) def get(self, feature_flag_name): # pylint: disable=method-hidden """ @@ -110,7 +110,7 @@ def get_feature_flags_by_sets(self, flag_sets): if sets_to_fetch == []: return [] - keys = [self._get_set_key(flag_set) for flag_set in sets_to_fetch] + keys = [self._get_flag_set_key(flag_set) for flag_set in sets_to_fetch] pipe = self._pipe() [pipe.smembers(key) for key in keys] result_sets = pipe.execute() From 474c72f16ae2a9aec7b467c51d93794a05478f0d Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 14 Sep 2023 08:32:19 -0700 Subject: [PATCH 54/77] corrected config param typo --- splitio/client/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index 6182f2d7..1cde7ea8 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -60,7 +60,7 @@ 'storageWrapper': None, 'storagePrefix': None, 'storageType': None, - 'FlagSetsFilter': None + 'flagSetsFilter': None } def _parse_operation_mode(sdk_key, config): @@ -143,6 +143,6 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 - processed['FlagSetsFilter'] = validate_flag_sets(processed['FlagSetsFilter'], 'SDK Config') if processed['FlagSetsFilter'] is not None else None + processed['flagSetsFilter'] = validate_flag_sets(processed['flagSetsFilter'], 'SDK Config') if processed['flagSetsFilter'] is not None else None return processed From d16c9ff26f69550183d1929a8d77725dce7987f9 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 14 Sep 2023 11:16:26 -0700 Subject: [PATCH 55/77] updated tests --- splitio/client/client.py | 21 ++-- splitio/client/config.py | 38 +------ splitio/client/factory.py | 2 +- splitio/client/input_validator.py | 69 ++++++++---- splitio/models/flag_sets.py | 124 +++++++++++++++++++++ splitio/models/telemetry.py | 80 +++++++------- splitio/storage/adapters/redis.py | 4 + splitio/storage/inmemmory.py | 44 ++++---- splitio/storage/pluggable.py | 11 +- splitio/storage/redis.py | 156 +++++++++++++++++---------- splitio/sync/split.py | 15 --- splitio/util/storage_helper.py | 50 ++++++--- tests/client/test_config.py | 26 ----- tests/client/test_input_validator.py | 83 +++++++++++--- tests/integration/test_client_e2e.py | 8 +- tests/models/test_flag_sets.py | 59 ++++++++++ tests/storage/test_pluggable.py | 20 +++- tests/storage/test_redis.py | 8 +- tests/util/test_storage_helper.py | 34 +++--- 19 files changed, 564 insertions(+), 288 deletions(-) create mode 100644 splitio/models/flag_sets.py create mode 100644 tests/models/test_flag_sets.py diff --git a/splitio/client/client.py b/splitio/client/client.py index ab29065c..5ff555e8 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -404,7 +404,7 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets) + feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets, method) if feature_flags_names == []: _LOGGER.warning("No valid Flag set or no feature flags found for evaluating treatments") return {} @@ -418,7 +418,7 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} - def _get_feature_flag_names_by_flag_sets(self, flag_sets): + def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): """ Sanitize given flag sets and return list of feature flag names associated with them @@ -428,17 +428,12 @@ def _get_feature_flag_names_by_flag_sets(self, flag_sets): :return: list of feature flag names :rtype: list """ - sanitized_flag_sets = config.sanitize_flag_sets(flag_sets) - feature_flags = [] - for flag_set in sanitized_flag_sets: - feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(flag_set) - if feature_flags_by_set is None: - _LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_set)) - continue - feature_flags.extend(feature_flags_by_set) - feature_flags_names = [] - [feature_flags_names.append(feature_flag) for feature_flag in feature_flags] - return feature_flags_names + sanitized_flag_sets = input_validator.validate_flag_sets(flag_sets, method_name) + feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets) + if feature_flags_by_set is None: + _LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_sets)) + return [] + return feature_flags_by_set def _build_impression( # pylint: disable=too-many-arguments self, diff --git a/splitio/client/config.py b/splitio/client/config.py index 800d472f..31b16bec 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -4,6 +4,7 @@ import re from splitio.engine.impressions import ImpressionsMode +from splitio.client.input_validator import validate_flag_sets _LOGGER = logging.getLogger(__name__) @@ -120,41 +121,6 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None): return mode, refresh_rate -def sanitize_flag_sets(flag_sets): - """ - Check supplied flag sets list - - :param flag_set: list of flag sets - :type flag_set: list[str] - - :returns: Sanitized and sorted flag sets - :rtype: list[str] - """ - if not isinstance(flag_sets, list): - _LOGGER.warning("SDK config: FlagSets config parameters type should be list object, parameter is discarded") - return [] - - sanitized_flag_sets = set() - for flag_set in flag_sets: - if not isinstance(flag_set, str): - _LOGGER.warning("SDK config: Flag Set name %s should be str object, this flag set is discarded" % (flag_set)) - continue - if flag_set != flag_set.strip(): - _LOGGER.warning("SDK config: Flag Set name %s has extra whitespace, trimming" % (flag_set)) - flag_set = flag_set.strip() - - if flag_set != flag_set.lower(): - _LOGGER.warning("SDK config: Flag Set name %s should be all lowercase - converting string to lowercase" % (flag_set)) - flag_set = flag_set.lower() - - if re.search(_FLAG_SETS_REGEX, flag_set) is None or re.search(_FLAG_SETS_REGEX, flag_set).group() != flag_set: - _LOGGER.warning("SDK config: you passed %s, Flag Set must adhere to the regular expressions %s. This means a Flag Set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.", flag_set, _FLAG_SETS_REGEX, flag_set) - continue - - sanitized_flag_sets.add(flag_set.strip()) - - return list(sanitized_flag_sets) - def sanitize(sdk_key, config): """ Look for inconsistencies or ill-formed configs and tune it accordingly. @@ -179,6 +145,6 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 - processed['flagSetsFilter'] = sorted(sanitize_flag_sets(processed['flagSetsFilter'])) if processed['flagSetsFilter'] is not None else None + processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'])) if processed['flagSetsFilter'] is not None else None return processed diff --git a/splitio/client/factory.py b/splitio/client/factory.py index fb22a86d..5a8309e5 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -440,7 +440,7 @@ def _build_redis_factory(api_key, cfg): cache_enabled = cfg.get('redisLocalCacheEnabled', False) cache_ttl = cfg.get('redisLocalCacheTTL', 5) storages = { - 'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl), + 'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': RedisSegmentStorage(redis_adapter), 'impressions': RedisImpressionsStorage(redis_adapter, sdk_metadata), 'events': RedisEventsStorage(redis_adapter, sdk_metadata), diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index a15caf91..0a65f310 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -15,6 +15,7 @@ MAX_LENGTH = 250 EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$' MAX_PROPERTIES_LENGTH_BYTES = 32768 +_FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$' def _check_not_null(value, name, operation): @@ -79,7 +80,7 @@ def _check_string_not_empty(value, name, operation): return True -def _check_string_matches(value, operation, pattern): +def _check_string_matches(value, operation, pattern, name): """ Check if value is adhere to a regular expression passed. @@ -92,14 +93,14 @@ def _check_string_matches(value, operation, pattern): :return: The result of validation :rtype: True|False """ - if not re.match(pattern, value): + if re.search(pattern, value) is None or re.search(pattern, value).group() != value: _LOGGER.error( '%s: you passed %s, event_type must ' + 'adhere to the regular expression %s. ' + - 'This means an event name must be alphanumeric, cannot be more ' + + 'This means %s must be alphanumeric, cannot be more ' + 'than 80 characters long, and can only include a dash, underscore, ' + 'period, or colon as separators of alphanumeric characters.', - operation, value, pattern + operation, value, pattern, name ) return False return True @@ -165,10 +166,7 @@ def _check_valid_object_key(key, name, operation): :return: The result of validation :rtype: str|None """ - if key is None: - _LOGGER.error( - '%s: you passed a null %s, %s must be a non-empty string.', - operation, name, name) + if not _check_not_null(key, 'key', operation): return None if isinstance(key, str): if not _check_string_not_empty(key, name, operation): @@ -179,7 +177,7 @@ def _check_valid_object_key(key, name, operation): return key_str -def _remove_empty_spaces(value, operation): +def _remove_empty_spaces(value, name, operation): """ Check if an string has whitespaces. @@ -192,10 +190,17 @@ def _remove_empty_spaces(value, operation): """ strip_value = value.strip() if value != strip_value: - _LOGGER.warning("%s: feature flag name '%s' has extra whitespace, trimming.", operation, value) + _LOGGER.warning("%s: %s '%s' has extra whitespace, trimming.", operation, name, value) return strip_value +def _convert_str_to_lower(value, name, operation): + lower_value = value.lower() + if value != lower_value: + _LOGGER.warning("%s: %s '%s' should be all lowercase - converting string to lowercase" % (operation, name, value)) + return lower_value + + def validate_key(key, method_name): """ Validate Key parameter for get_treatment/s. @@ -211,8 +216,7 @@ def validate_key(key, method_name): """ matching_key_result = None bucketing_key_result = None - if key is None: - _LOGGER.error('%s: you passed a null key, key must be a non-empty string.', method_name) + if not _check_not_null(key, 'key', method_name): return None, None if isinstance(key, Key): @@ -255,7 +259,7 @@ def validate_feature_flag_name(feature_flag_name, should_validate_existance, fea ) return None - return _remove_empty_spaces(feature_flag_name, method_name) + return _remove_empty_spaces(feature_flag_name, 'feature flag name', method_name) def validate_track_key(key): @@ -294,10 +298,7 @@ def validate_traffic_type(traffic_type, should_validate_existance, feature_flag_ (not _check_is_string(traffic_type, 'traffic_type', 'track')) or \ (not _check_string_not_empty(traffic_type, 'traffic_type', 'track')): return None - if not traffic_type.islower(): - _LOGGER.warning('track: %s should be all lowercase - converting string to lowercase.', - traffic_type) - traffic_type = traffic_type.lower() + traffic_type = _convert_str_to_lower(traffic_type, 'traffic type', 'track') if should_validate_existance and not feature_flag_storage.is_valid_traffic_type(traffic_type): _LOGGER.warning( @@ -322,7 +323,7 @@ def validate_event_type(event_type): if (not _check_not_null(event_type, 'event_type', 'track')) or \ (not _check_is_string(event_type, 'event_type', 'track')) or \ (not _check_string_not_empty(event_type, 'event_type', 'track')) or \ - (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN)): + (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN, 'an event name')): return None return event_type @@ -390,7 +391,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name _LOGGER.error("%s: feature flag names must be a non-empty array.", method_name) return None, None filtered_feature_flags = set( - _remove_empty_spaces(feature_flag, method_name) for feature_flag in feature_flags + _remove_empty_spaces(feature_flag, 'feature flag name', method_name) for feature_flag in feature_flags if feature_flag is not None and _check_is_string(feature_flag, 'feature flag name', method_name) and _check_string_not_empty(feature_flag, 'feature flag name', method_name) @@ -566,3 +567,33 @@ def validate_pluggable_adapter(config): _LOGGER.error("Pluggable adapter method %s has less than required arguments count: %s : " % (exp_method, len(get_method_args))) return False return True + +def validate_flag_sets(flag_sets, method_name): + """ + Validate flag sets list + + :param flag_set: list of flag sets + :type flag_set: list[str] + + :returns: Sanitized and sorted flag sets + :rtype: list[str] + """ + if not isinstance(flag_sets, list): + _LOGGER.warning("%s: flag sets parameter type should be list object, parameter is discarded" % (method_name)) + return [] + + sanitized_flag_sets = set() + for flag_set in flag_sets: + if not _check_not_null(flag_set, 'flag set', method_name): + continue + if not _check_is_string(flag_set, 'flag set', method_name): + continue + flag_set = _remove_empty_spaces(flag_set, 'flag set', method_name) + flag_set = _convert_str_to_lower(flag_set, 'flag set', method_name) + + if not _check_string_matches(flag_set, method_name, _FLAG_SETS_REGEX, 'a flag set'): + continue + + sanitized_flag_sets.add(flag_set) + + return sorted(list(sanitized_flag_sets)) diff --git a/splitio/models/flag_sets.py b/splitio/models/flag_sets.py new file mode 100644 index 00000000..a01de740 --- /dev/null +++ b/splitio/models/flag_sets.py @@ -0,0 +1,124 @@ +"""Flagsets classes.""" +import threading + +class FlagSetsFilter(object): + """Config Flagsets Filter storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self.flag_sets = set(flag_sets) + self.should_filter = any(flag_sets) + + def set_exist(self, flag_set): + """ + Check if a flagset exist in flagset filter + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + if not self.should_filter: + return True + if not isinstance(flag_set, str) or flag_set == '': + return False + + return any(self.flag_sets.intersection(set([flag_set]))) + + def intersect(self, flag_sets): + """ + Check if a set exist in config flagset filter + + :param flag_set: set of flagsets + :type flag_set: set + + :rtype: bool + """ + if not self.should_filter: + return True + if not isinstance(flag_sets, set) or len(flag_sets) == 0: + return False + return any(self.flag_sets.intersection(flag_sets)) + + +class FlagSets(object): + """InMemory Flagsets storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self._lock = threading.RLock() + self.sets_feature_flag_map = {} + for flag_set in flag_sets: + self.sets_feature_flag_map[flag_set] = set() + + def flag_set_exist(self, flag_set): + """ + Check if a flagset exist in stored flagset + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + with self._lock: + return flag_set in self.sets_feature_flag_map.keys() + + def get_flag_set(self, flag_set): + """ + fetch feature flags stored in a flag set + + :param flag_set: set name + :type flag_set: str + + :rtype: list(str) + """ + with self._lock: + return self.sets_feature_flag_map.get(flag_set) + + def add_flag_set(self, flag_set): + """ + Add new flag set to storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if not self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set] = set() + + def remove_flag_set(self, flag_set): + """ + Remove existing flag set from storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + del self.sets_feature_flag_map[flag_set] + + def add_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Add a feature flag to existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].add(feature_flag) + + def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Remove a feature flag from existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].remove(feature_flag) diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index b15f15e7..3e9af26f 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -27,7 +27,7 @@ class CounterConstants(Enum): EVENTS_QUEUED = 'eventsQueued' EVENTS_DROPPED = 'eventsDropped' -class _ConfigParams(Enum): +class ConfigParams(Enum): """Config parameters constants""" SPLITS_REFRESH_RATE = 'featuresRefreshRate' SEGMENTS_REFRESH_RATE = 'segmentsRefreshRate' @@ -41,9 +41,8 @@ class _ConfigParams(Enum): EVENTS_QUEUE_SIZE = 'eventsQueueSize' IMPRESSIONS_MODE = 'impressionsMode' IMPRESSIONS_LISTENER = 'impressionListener' - FLAG_SETS = 'flagSetsFilter' -class _ExtraConfig(Enum): +class ExtraConfig(Enum): """Extra config constants""" ACTIVE_FACTORY_COUNT = 'activeFactoryCount' REDUNDANT_FACTORY_COUNT = 'redundantFactoryCount' @@ -54,7 +53,7 @@ class _ExtraConfig(Enum): HTTP_PROXY = 'httpProxy' HTTPS_PROXY_ENV = 'HTTPS_PROXY' -class _ApiURLs(Enum): +class ApiURLs(Enum): """Api URL constants""" SDK_URL = 'sdk_url' EVENTS_URL = 'events_url' @@ -89,7 +88,7 @@ class MethodExceptionsAndLatencies(Enum): TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 'treatments_with_config_by_flag_sets' TRACK = 'track' -class _LastSynchronizationConstants(Enum): +class LastSynchronizationConstants(Enum): """Last sync constants""" LAST_SYNCHRONIZATIONS = 'lastSynchronizations' @@ -109,7 +108,7 @@ class SSESyncMode(Enum): STREAMING = 0 POLLING = 1 -class _StreamingEventsConstant(Enum): +class StreamingEventsConstant(Enum): """Storage types constant""" STREAMING_EVENTS = 'streamingEvents' @@ -427,7 +426,7 @@ def get_all(self): :rtype: dict """ with self._lock: - return {_LastSynchronizationConstants.LAST_SYNCHRONIZATIONS.value: {HTTPExceptionsAndLatencies.SPLIT.value: self._split, HTTPExceptionsAndLatencies.SEGMENT.value: self._segment, HTTPExceptionsAndLatencies.IMPRESSION.value: self._impression, + return {LastSynchronizationConstants.LAST_SYNCHRONIZATIONS.value: {HTTPExceptionsAndLatencies.SPLIT.value: self._split, HTTPExceptionsAndLatencies.SEGMENT.value: self._segment, HTTPExceptionsAndLatencies.IMPRESSION.value: self._impression, HTTPExceptionsAndLatencies.IMPRESSION_COUNT.value: self._impression_count, HTTPExceptionsAndLatencies.EVENT.value: self._event, HTTPExceptionsAndLatencies.TELEMETRY.value: self._telemetry, HTTPExceptionsAndLatencies.TOKEN.value: self._token} } @@ -761,7 +760,7 @@ def pop_streaming_events(self): with self._lock: streaming_events = self._streaming_events self._streaming_events = [] - return {_StreamingEventsConstant.STREAMING_EVENTS.value: [{'e': streaming_event.type, 'd': streaming_event.data, + return {StreamingEventsConstant.STREAMING_EVENTS.value: [{'e': streaming_event.type, 'd': streaming_event.data, 't': streaming_event.time} for streaming_event in streaming_events]} class TelemetryConfig(object): @@ -783,10 +782,10 @@ def _reset_all(self): self._operation_mode = None self._storage_type = None self._streaming_enabled = None - self._refresh_rate = {_ConfigParams.SPLITS_REFRESH_RATE.value: 0, _ConfigParams.SEGMENTS_REFRESH_RATE.value: 0, - _ConfigParams.IMPRESSIONS_REFRESH_RATE.value: 0, _ConfigParams.EVENTS_REFRESH_RATE.value: 0, _ConfigParams.TELEMETRY_REFRESH_RATE.value: 0} - self._url_override = {_ApiURLs.SDK_URL.value: False, _ApiURLs.EVENTS_URL.value: False, _ApiURLs.AUTH_URL.value: False, - _ApiURLs.STREAMING_URL.value: False, _ApiURLs.TELEMETRY_URL.value: False} + self._refresh_rate = {ConfigParams.SPLITS_REFRESH_RATE.value: 0, ConfigParams.SEGMENTS_REFRESH_RATE.value: 0, + ConfigParams.IMPRESSIONS_REFRESH_RATE.value: 0, ConfigParams.EVENTS_REFRESH_RATE.value: 0, ConfigParams.TELEMETRY_REFRESH_RATE.value: 0} + self._url_override = {ApiURLs.SDK_URL.value: False, ApiURLs.EVENTS_URL.value: False, ApiURLs.AUTH_URL.value: False, + ApiURLs.STREAMING_URL.value: False, ApiURLs.TELEMETRY_URL.value: False} self._impressions_queue_size = 0 self._events_queue_size = 0 self._impressions_mode = None @@ -819,17 +818,16 @@ def record_config(self, config, extra_config): :type config: dict """ with self._lock: - self._operation_mode = self._get_operation_mode(config[_ConfigParams.OPERATION_MODE.value]) - self._storage_type = self._get_storage_type(config[_ConfigParams.OPERATION_MODE.value], config[_ConfigParams.STORAGE_TYPE.value]) - self._streaming_enabled = config[_ConfigParams.STREAMING_ENABLED.value] + self._operation_mode = self._get_operation_mode(config[ConfigParams.OPERATION_MODE.value]) + self._storage_type = self._get_storage_type(config[ConfigParams.OPERATION_MODE.value], config[ConfigParams.STORAGE_TYPE.value]) + self._streaming_enabled = config[ConfigParams.STREAMING_ENABLED.value] self._refresh_rate = self._get_refresh_rates(config) self._url_override = self._get_url_overrides(extra_config) - self._impressions_queue_size = config[_ConfigParams.IMPRESSIONS_QUEUE_SIZE.value] - self._events_queue_size = config[_ConfigParams.EVENTS_QUEUE_SIZE.value] - self._impressions_mode = self._get_impressions_mode(config[_ConfigParams.IMPRESSIONS_MODE.value]) - self._impression_listener = True if config[_ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False + self._impressions_queue_size = config[ConfigParams.IMPRESSIONS_QUEUE_SIZE.value] + self._events_queue_size = config[ConfigParams.EVENTS_QUEUE_SIZE.value] + self._impressions_mode = self._get_impressions_mode(config[ConfigParams.IMPRESSIONS_MODE.value]) + self._impression_listener = True if config[ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False self._http_proxy = self._check_if_proxy_detected() - self._flag_sets = len(config[_ConfigParams.FLAG_SETS.value]) if config[_ConfigParams.FLAG_SETS.value] is not None else 0 def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): with self._lock: @@ -915,16 +913,16 @@ def get_stats(self): 'oM': self._operation_mode, 'sT': self._storage_type, 'sE': self._streaming_enabled, - 'rR': {'sp': self._refresh_rate[_ConfigParams.SPLITS_REFRESH_RATE.value], - 'se': self._refresh_rate[_ConfigParams.SEGMENTS_REFRESH_RATE.value], - 'im': self._refresh_rate[_ConfigParams.IMPRESSIONS_REFRESH_RATE.value], - 'ev': self._refresh_rate[_ConfigParams.EVENTS_REFRESH_RATE.value], - 'te': self._refresh_rate[_ConfigParams.TELEMETRY_REFRESH_RATE.value]}, - 'uO': {'s': self._url_override[_ApiURLs.SDK_URL.value], - 'e': self._url_override[_ApiURLs.EVENTS_URL.value], - 'a': self._url_override[_ApiURLs.AUTH_URL.value], - 'st': self._url_override[_ApiURLs.STREAMING_URL.value], - 't': self._url_override[_ApiURLs.TELEMETRY_URL.value]}, + 'rR': {'sp': self._refresh_rate[ConfigParams.SPLITS_REFRESH_RATE.value], + 'se': self._refresh_rate[ConfigParams.SEGMENTS_REFRESH_RATE.value], + 'im': self._refresh_rate[ConfigParams.IMPRESSIONS_REFRESH_RATE.value], + 'ev': self._refresh_rate[ConfigParams.EVENTS_REFRESH_RATE.value], + 'te': self._refresh_rate[ConfigParams.TELEMETRY_REFRESH_RATE.value]}, + 'uO': {'s': self._url_override[ApiURLs.SDK_URL.value], + 'e': self._url_override[ApiURLs.EVENTS_URL.value], + 'a': self._url_override[ApiURLs.AUTH_URL.value], + 'st': self._url_override[ApiURLs.STREAMING_URL.value], + 't': self._url_override[ApiURLs.TELEMETRY_URL.value]}, 'iQ': self._impressions_queue_size, 'eQ': self._events_queue_size, 'iM': self._impressions_mode, @@ -983,11 +981,11 @@ def _get_refresh_rates(self, config): """ with self._lock: return { - _ConfigParams.SPLITS_REFRESH_RATE.value: config[_ConfigParams.SPLITS_REFRESH_RATE.value], - _ConfigParams.SEGMENTS_REFRESH_RATE.value: config[_ConfigParams.SEGMENTS_REFRESH_RATE.value], - _ConfigParams.IMPRESSIONS_REFRESH_RATE.value: config[_ConfigParams.IMPRESSIONS_REFRESH_RATE.value], - _ConfigParams.EVENTS_REFRESH_RATE.value: config[_ConfigParams.EVENTS_REFRESH_RATE.value], - _ConfigParams.TELEMETRY_REFRESH_RATE.value: config[_ConfigParams.TELEMETRY_REFRESH_RATE.value] + ConfigParams.SPLITS_REFRESH_RATE.value: config[ConfigParams.SPLITS_REFRESH_RATE.value], + ConfigParams.SEGMENTS_REFRESH_RATE.value: config[ConfigParams.SEGMENTS_REFRESH_RATE.value], + ConfigParams.IMPRESSIONS_REFRESH_RATE.value: config[ConfigParams.IMPRESSIONS_REFRESH_RATE.value], + ConfigParams.EVENTS_REFRESH_RATE.value: config[ConfigParams.EVENTS_REFRESH_RATE.value], + ConfigParams.TELEMETRY_REFRESH_RATE.value: config[ConfigParams.TELEMETRY_REFRESH_RATE.value] } def _get_url_overrides(self, config): @@ -1002,11 +1000,11 @@ def _get_url_overrides(self, config): """ with self._lock: return { - _ApiURLs.SDK_URL.value: True if _ApiURLs.SDK_URL.value in config else False, - _ApiURLs.EVENTS_URL.value: True if _ApiURLs.EVENTS_URL.value in config else False, - _ApiURLs.AUTH_URL.value: True if _ApiURLs.AUTH_URL.value in config else False, - _ApiURLs.STREAMING_URL.value: True if _ApiURLs.STREAMING_URL.value in config else False, - _ApiURLs.TELEMETRY_URL.value: True if _ApiURLs.TELEMETRY_URL.value in config else False + ApiURLs.SDK_URL.value: True if ApiURLs.SDK_URL.value in config else False, + ApiURLs.EVENTS_URL.value: True if ApiURLs.EVENTS_URL.value in config else False, + ApiURLs.AUTH_URL.value: True if ApiURLs.AUTH_URL.value in config else False, + ApiURLs.STREAMING_URL.value: True if ApiURLs.STREAMING_URL.value in config else False, + ApiURLs.TELEMETRY_URL.value: True if ApiURLs.TELEMETRY_URL.value in config else False } def _get_impressions_mode(self, imp_mode): @@ -1036,6 +1034,6 @@ def _check_if_proxy_detected(self): """ with self._lock: for x in os.environ: - if x.upper() == _ExtraConfig.HTTPS_PROXY_ENV.value: + if x.upper() == ExtraConfig.HTTPS_PROXY_ENV.value: return True return False \ No newline at end of file diff --git a/splitio/storage/adapters/redis.py b/splitio/storage/adapters/redis.py index de3026b3..8657b317 100644 --- a/splitio/storage/adapters/redis.py +++ b/splitio/storage/adapters/redis.py @@ -339,6 +339,10 @@ def execute(self): except RedisError as exc: raise RedisAdapterException('Error executing pipeline operation') from exc + def smembers(self, name): + """Mimic original redis function but using user custom prefix.""" + self._pipe.smembers(self._prefix_helper.add_prefix(name)) + def _build_default_client(config): # pylint: disable=too-many-locals """ diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index b8a621a6..acbae771 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -6,6 +6,7 @@ from splitio.models.segments import Segment from splitio.models.telemetry import HTTPErrors, HTTPLatencies, MethodExceptions, MethodLatencies, LastSynchronization, StreamingEvents, TelemetryConfig, TelemetryCounters, CounterConstants +from splitio.models.flag_sets import FlagSets, FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage MAX_SIZE_BYTES = 5 * 1024 * 1024 @@ -13,9 +14,8 @@ _LOGGER = logging.getLogger(__name__) - class InMemorySplitStorage(SplitStorage): - """InMemory implementation of a split storage.""" + """InMemory implementation of a feature flag storage.""" def __init__(self, flag_sets=[]): """Constructor.""" @@ -23,10 +23,8 @@ def __init__(self, flag_sets=[]): self._splits = {} self._change_number = -1 self._traffic_types = Counter() - self._sets_feature_flag_map = {} - self.config_flag_sets_used = len(flag_sets) - for flag_set in flag_sets: - self._sets_feature_flag_map[flag_set] = set() + self.flag_set = FlagSets(flag_sets) + self.flag_set_filter = FlagSetsFilter(flag_sets) def get(self, split_name): """ @@ -82,11 +80,11 @@ def _put(self, split): self._increase_traffic_type_count(split.traffic_type_name) if split.sets is not None: for flag_set in split.sets: - if flag_set not in self._sets_feature_flag_map.keys(): - if self.config_flag_sets_used > 0: + if not self.flag_set.flag_set_exist(flag_set): + if self.flag_set_filter.should_filter: continue - self._sets_feature_flag_map[flag_set] = set() - self._sets_feature_flag_map[flag_set].add(split.name) + self.flag_set.add_flag_set(flag_set) + self.flag_set.add_feature_flag_to_flag_set(flag_set, split.name) def _remove(self, split_name): """ @@ -118,11 +116,11 @@ def _remove_from_flag_sets(self, feature_flag): """ if feature_flag.sets is not None: for flag_set in feature_flag.sets: - self._sets_feature_flag_map[flag_set].remove(feature_flag.name) - if len(self._sets_feature_flag_map[flag_set]) == 0 and self.config_flag_sets_used == 0: - del self._sets_feature_flag_map[flag_set] + self.flag_set.remove_feature_flag_to_flag_set(flag_set, feature_flag.name) + if len(self.flag_set.get_flag_set(flag_set)) == 0 and not self.flag_set_filter.should_filter: + self.flag_set.remove_flag_set(flag_set) - def get_feature_flags_by_set(self, set): + def get_feature_flags_by_sets(self, sets): """ Get list of feature flag names associated to a set, if it does not exist will return empty list @@ -133,9 +131,16 @@ def get_feature_flags_by_set(self, set): :rtype: list """ with self._lock: - if set not in self._sets_feature_flag_map: - return [] - return list(self._sets_feature_flag_map[set]) + sets_to_fetch = [] + for flag_set in sets: + if not self.flag_set.flag_set_exist(flag_set): + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring it." % (flag_set)) + continue + sets_to_fetch.append(flag_set) + + to_return = set() + [to_return.update(self.flag_set.get_flag_set(flag_set)) for flag_set in sets_to_fetch] + return list(to_return) def get_change_number(self): """ @@ -247,10 +252,7 @@ def is_flag_set_exist(self, flag_set): :return: True if the flag_set exist. False otherwise. :rtype: bool """ - if flag_set in self._sets_feature_flag_map.keys(): - return True - return False - + return self.flag_set.flag_set_exist(flag_set) class InMemorySegmentStorage(SegmentStorage): """In-memory implementation of a segment storage.""" diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index eaea0daf..4ed7c9e9 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -7,6 +7,7 @@ from splitio.models import splits, segments from splitio.models.impressions import Impression from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MAX_TAGS, get_latency_bucket_index +from splitio.models.flag_sets import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets @@ -27,16 +28,16 @@ def __init__(self, pluggable_adapter, prefix=None, config_flag_sets=[]): :type prefix: str """ self._pluggable_adapter = pluggable_adapter - self._config_flag_sets = config_flag_sets self._prefix = "SPLITIO.split.{feature_flag_name}" self._traffic_type_prefix = "SPLITIO.trafficType.{traffic_type_name}" self._feature_flag_till_prefix = "SPLITIO.splits.till" - self._feature_flag_set_prefix = 'SPLITIO.set.{flag_set}' + self._flag_set_prefix = 'SPLITIO.flagSet.{flag_set}' + self.flag_set_filter = FlagSetsFilter(config_flag_sets) if prefix is not None: self._prefix = prefix + "." + self._prefix self._traffic_type_prefix = prefix + "." + self._traffic_type_prefix self._feature_flag_till_prefix = prefix + "." + self._feature_flag_till_prefix - self._feature_flag_set_prefix = prefix + "." + self._feature_flag_set_prefix + self._flag_set_prefix = prefix + "." + self._flag_set_prefix def get(self, feature_flag_name): """ @@ -86,11 +87,11 @@ def get_feature_flags_by_sets(self, flag_sets): :rtype: listt(str) """ try: - sets_to_fetch = get_valid_flag_sets(flag_sets, self._config_flag_sets) + sets_to_fetch = get_valid_flag_sets(flag_sets, self.flag_set_filter) if sets_to_fetch == []: return [] - keys = [self._feature_flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] + keys = [self._flag_set_prefix.format(flag_set=flag_set) for flag_set in sets_to_fetch] result_sets = [] [result_sets.append(set(key)) for key in self._pluggable_adapter.get_many(keys)] return list(combine_valid_flag_sets(result_sets)) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 9433fdd4..6cadc212 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -6,23 +6,26 @@ from splitio.models.impressions import Impression from splitio.models import splits, segments from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, get_latency_bucket_index +from splitio.models.flag_sets import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, \ ImpressionPipelinedStorage, TelemetryStorage from splitio.storage.adapters.redis import RedisAdapterException from splitio.storage.adapters.cache_trait import decorate as add_cache, DEFAULT_MAX_AGE +from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets _LOGGER = logging.getLogger(__name__) MAX_TAGS = 10 class RedisSplitStorage(SplitStorage): - """Redis-based storage for splits.""" + """Redis-based storage for feature flags.""" - _SPLIT_KEY = 'SPLITIO.split.{split_name}' - _SPLIT_TILL_KEY = 'SPLITIO.splits.till' + _FEATURE_FLAG_KEY = 'SPLITIO.split.{feature_flag_name}' + _FEATURE_FLAG_TILL_KEY = 'SPLITIO.splits.till' _TRAFFIC_TYPE_KEY = 'SPLITIO.trafficType.{traffic_type_name}' + _FLAG_SET_KEY = 'SPLITIO.flagSet.{flag_set}' - def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE): + def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE, config_flag_sets=[]): """ Class constructor. @@ -30,87 +33,128 @@ def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE): :type redis_client: splitio.storage.adapters.redis.RedisAdapter """ self._redis = redis_client + self.flag_set_filter = FlagSetsFilter(config_flag_sets) + self._pipe = self._redis.pipeline if enable_caching: self.get = add_cache(lambda *p, **_: p[0], max_age)(self.get) self.is_valid_traffic_type = add_cache(lambda *p, **_: p[0], max_age)(self.is_valid_traffic_type) # pylint: disable=line-too-long self.fetch_many = add_cache(lambda *p, **_: frozenset(p[0]), max_age)(self.fetch_many) - def _get_key(self, split_name): + def _get_key(self, feature_flag_name): """ - Use the provided split_name to build the appropriate redis key. + Use the provided feature_flag_name to build the appropriate redis key. - :param split_name: Name of the split to interact with in redis. - :type split_name: str + :param feature_flag_name: Name of the feature flag to interact with in redis. + :type feature_flag_name: str :return: Redis key. :rtype: str. """ - return self._SPLIT_KEY.format(split_name=split_name) + return self._FEATURE_FLAG_KEY.format(feature_flag_name=feature_flag_name) def _get_traffic_type_key(self, traffic_type_name): """ - Use the provided split_name to build the appropriate redis key. + Use the provided traffic_type_name to build the appropriate redis key. - :param split_name: Name of the split to interact with in redis. - :type split_name: str + :param trafic_type_name: Name of the traffic type to interact with in redis. + :type traffic_type_name: str :return: Redis key. :rtype: str. """ return self._TRAFFIC_TYPE_KEY.format(traffic_type_name=traffic_type_name) - def get(self, split_name): # pylint: disable=method-hidden + def _get_flag_set_key(self, flag_set): """ - Retrieve a split. + Use the provided flag set to build the appropriate redis key. + + :param flag_set: Name of the flag set to interact with in redis. + :type flag_set: str - :param split_name: Name of the feature to fetch. - :type split_name: str + :return: Redis key. + :rtype: str. + """ + return self._FLAG_SET_KEY.format(flag_set=flag_set) + + def get(self, feature_flag_name): # pylint: disable=method-hidden + """ + Retrieve a feature flag. + + :param feature_flag_name: Name of the feature to fetch. + :type feature_flag_name: str :return: A split object parsed from redis if the key exists. None otherwise :rtype: splitio.models.splits.Split """ try: - raw = self._redis.get(self._get_key(split_name)) - _LOGGER.debug("Fetchting Split [%s] from redis" % split_name) + raw = self._redis.get(self._get_key(feature_flag_name)) + _LOGGER.debug("Fetchting Feature flag [%s] from redis" % feature_flag_name) _LOGGER.debug(raw) return splits.from_raw(json.loads(raw)) if raw is not None else None except RedisAdapterException: - _LOGGER.error('Error fetching split from storage') + _LOGGER.error('Error fetching feature flag from storage') + _LOGGER.debug('Error: ', exc_info=True) + return None + + def get_feature_flags_by_sets(self, flag_sets): + """ + Retrieve feature flags by flag set. + + :param flag_set: Names of the flag set to fetch. + :type flag_set: str + + :return: Feature flag names that are tagged with the flag set + :rtype: listt(str) + """ + try: + sets_to_fetch = get_valid_flag_sets(flag_sets, self.flag_set_filter) + if sets_to_fetch == []: + return [] + + keys = [self._get_flag_set_key(flag_set) for flag_set in sets_to_fetch] + pipe = self._pipe() + [pipe.smembers(key) for key in keys] + result_sets = pipe.execute() + _LOGGER.debug("Fetchting Feature flags by set [%s] from redis" % (keys)) + _LOGGER.debug(result_sets) + return list(combine_valid_flag_sets(result_sets)) + except RedisAdapterException: + _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return None - def fetch_many(self, split_names): + def fetch_many(self, feature_flag_names): """ - Retrieve splits. + Retrieve feature flags. - :param split_names: Names of the features to fetch. - :type split_name: list(str) + :param feature_flag_names: Names of the features to fetch. + :type feature_flag_name: list(str) :return: A dict with split objects parsed from redis. :rtype: dict(split_name, splitio.models.splits.Split) """ to_return = dict() try: - keys = [self._get_key(split_name) for split_name in split_names] - raw_splits = self._redis.mget(keys) - _LOGGER.debug("Fetchting Splits [%s] from redis" % split_names) - _LOGGER.debug(raw_splits) - for i in range(len(split_names)): - split = None + keys = [self._get_key(feature_flag_name) for feature_flag_name in feature_flag_names] + raw_feature_flags = self._redis.mget(keys) + _LOGGER.debug("Fetchting feature flags [%s] from redis" % feature_flag_names) + _LOGGER.debug(raw_feature_flags) + for i in range(len(feature_flag_names)): + feature_flag = None try: - split = splits.from_raw(json.loads(raw_splits[i])) + feature_flag = splits.from_raw(json.loads(raw_feature_flags[i])) except (ValueError, TypeError): - _LOGGER.error('Could not parse split.') - _LOGGER.debug("Raw split that failed parsing attempt: %s", raw_splits[i]) - to_return[split_names[i]] = split + _LOGGER.error('Could not parse feature flag.') + _LOGGER.debug("Raw feature flag that failed parsing attempt: %s", raw_feature_flags[i]) + to_return[feature_flag_names[i]] = feature_flag except RedisAdapterException: - _LOGGER.error('Error fetching splits from storage') + _LOGGER.error('Error fetching feature flags from storage') _LOGGER.debug('Error: ', exc_info=True) return to_return def is_valid_traffic_type(self, traffic_type_name): # pylint: disable=method-hidden """ - Return whether the traffic type exists in at least one split in cache. + Return whether the traffic type exists in at least one feature flag in cache. :param traffic_type_name: Traffic type to validate. :type traffic_type_name: str @@ -124,7 +168,7 @@ def is_valid_traffic_type(self, traffic_type_name): # pylint: disable=method-hi _LOGGER.debug("Fetching TrafficType [%s] count in redis: %s" % (traffic_type_name, count)) return count > 0 except RedisAdapterException: - _LOGGER.error('Error fetching split from storage') + _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) return False @@ -143,32 +187,32 @@ def update(self, to_add, to_delete, new_change_number): def get_change_number(self): """ - Retrieve latest split change number. + Retrieve latest feature flag change number. :rtype: int """ try: - stored_value = self._redis.get(self._SPLIT_TILL_KEY) - _LOGGER.debug("Fetching Split Change Number from redis: %s" % stored_value) + stored_value = self._redis.get(self._FEATURE_FLAG_TILL_KEY) + _LOGGER.debug("Fetching feature flag Change Number from redis: %s" % stored_value) return json.loads(stored_value) if stored_value is not None else None except RedisAdapterException: - _LOGGER.error('Error fetching split change number from storage') + _LOGGER.error('Error fetching feature flag change number from storage') _LOGGER.debug('Error: ', exc_info=True) return None def get_split_names(self): """ - Retrieve a list of all split names. + Retrieve a list of all feature flag names. - :return: List of split names. + :return: List of feature flag names. :rtype: list(str) """ try: keys = self._redis.keys(self._get_key('*')) - _LOGGER.debug("Fetchting Split names from redis: %s" % keys) + _LOGGER.debug("Fetchting feature flag names from redis: %s" % keys) return [key.replace(self._get_key(''), '') for key in keys] except RedisAdapterException: - _LOGGER.error('Error fetching split names from storage') + _LOGGER.error('Error fetching feature flag names from storage') _LOGGER.debug('Error: ', exc_info=True) return [] @@ -182,33 +226,33 @@ def get_splits_count(self): def get_all_splits(self): """ - Return all the splits in cache. - :return: List of all splits in cache. + Return all the feature flags in cache. + :return: List of all feature flags in cache. :rtype: list(splitio.models.splits.Split) """ keys = self._redis.keys(self._get_key('*')) to_return = [] try: - _LOGGER.debug("Fetchting all Splits from redis: %s" % keys) - raw_splits = self._redis.mget(keys) - _LOGGER.debug(raw_splits) - for raw in raw_splits: + _LOGGER.debug("Fetchting all feature flags from redis: %s" % keys) + raw_feature_flags = self._redis.mget(keys) + _LOGGER.debug(raw_feature_flags) + for raw in raw_feature_flags: try: to_return.append(splits.from_raw(json.loads(raw))) except (ValueError, TypeError): - _LOGGER.error('Could not parse split. Skipping') - _LOGGER.debug("Raw split that failed parsing attempt: %s", raw) + _LOGGER.error('Could not parse feature flag. Skipping') + _LOGGER.debug("Raw feature flag that failed parsing attempt: %s", raw) except RedisAdapterException: _LOGGER.error('Error fetching all splits from storage') _LOGGER.debug('Error: ', exc_info=True) return to_return - def kill_locally(self, split_name, default_treatment, change_number): + def kill_locally(self, feature_flag_name, default_treatment, change_number): """ - Local kill for split + Local kill for feature flag - :param split_name: name of the split to perform kill - :type split_name: str + :param feature_flag_name: name of the feature flag to perform kill + :type feature_flag_name: str :param default_treatment: name of the default treatment to return :type default_treatment: str :param change_number: change_number diff --git a/splitio/sync/split.py b/splitio/sync/split.py index 209e59f1..559a1543 100644 --- a/splitio/sync/split.py +++ b/splitio/sync/split.py @@ -83,21 +83,6 @@ def _fetch_until(self, fetch_options, till=None): fetched_feature_flags = [] [fetched_feature_flags.append(splits.from_raw(feature_flag)) for feature_flag in feature_flag_changes.get('splits', [])] segment_list = update_feature_flag_storage(self._feature_flag_storage, fetched_feature_flags, feature_flag_changes['till']) - ''' - to_add = [] - to_delete = [] - for feature_flag in feature_flag_changes.get('splits', []): - if (self._feature_flag_storage.config_flag_sets_used == 0 and feature_flag['status'] == splits.Status.ACTIVE.value) or \ - (feature_flag['status'] == splits.Status.ACTIVE.value and self._check_flag_sets(feature_flag)): - parsed = splits.from_raw(feature_flag) - to_add.append(parsed) - segment_list.update(set(parsed.get_segment_names())) - else: - if self._feature_flag_storage.get(feature_flag['name']) is not None: - to_delete.append(feature_flag['name']) - - self._feature_flag_storage.update(to_add, to_delete, feature_flag_changes['till']) - ''' if feature_flag_changes['till'] == feature_flag_changes['since']: return feature_flag_changes['till'], segment_list diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index fb07e70c..d8f5fbd8 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -1,7 +1,10 @@ """Storage Helper.""" +import logging from splitio.models import splits +_LOGGER = logging.getLogger(__name__) + def update_feature_flag_storage(feature_flag_storage, feature_flags, change_number): """ Update feature flag storage from given list of feature flags while checking the flag set logic @@ -20,8 +23,7 @@ def update_feature_flag_storage(feature_flag_storage, feature_flags, change_numb to_add = [] to_delete = [] for feature_flag in feature_flags: - if (feature_flag_storage.config_flag_sets_used == 0 and feature_flag.status == splits.Status.ACTIVE) or \ - (feature_flag.status == splits.Status.ACTIVE and _check_flag_sets(feature_flag_storage, feature_flag)): + if feature_flag_storage.flag_set_filter.intersect(feature_flag.sets) and feature_flag.status == splits.Status.ACTIVE: to_add.append(feature_flag) segment_list.update(set(feature_flag.get_segment_names())) else: @@ -31,19 +33,39 @@ 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 _check_flag_sets(feature_flag_storage, feature_flag): +def get_valid_flag_sets(flag_sets, flag_set_filter): """ - Check all flag sets in a feature flag, return True if any of sets exist in storage + 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 - :param feature_flag_storage: Feature flag storage instance - :type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage - :param feature_flag: Feature flag instance to validate. - :type feature_flag: splitio.models.splits.Split + :param flag_sets: Flag sets array + :type flag_sets: list(str) + :param config_flag_sets: Config flag sets array + :type config_flag_sets: list(str) + + :return: array of flag sets + :rtype: list(str) + """ + sets_to_fetch = [] + for flag_set in flag_sets: + if not flag_set_filter.set_exist(flag_set) and flag_set_filter.should_filter: + _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) + continue + sets_to_fetch.append(flag_set) + + return sets_to_fetch + +def combine_valid_flag_sets(result_sets): + """ + Check each flag set in given array of sets, combine all flag sets in one unique set + + :param result_sets: Flag sets set + :type flag_sets: list(set) - :return: True if any of its flag_set exist. False otherwise. - :rtype: bool + :return: flag sets set + :rtype: set """ - for flag_set in feature_flag.sets: - if feature_flag_storage.is_flag_set_exist(flag_set): - return True - return False + to_return = set() + for result_set in result_sets: + if isinstance(result_set, set) and len(result_set) > 0: + to_return.update(result_set) + return to_return diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 7a00e86d..68760031 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -69,29 +69,3 @@ def test_sanitize(self): processed = config.sanitize('some', configs) assert processed['redisLocalCacheEnabled'] # check default is True - - def test_sanitize_flag_sets(self): - """Test sanitization for flag sets.""" - flag_sets = config.sanitize_flag_sets([' set1', 'set2 ', 'set3']) - assert sorted(flag_sets) == ['set1', 'set2', 'set3'] - - flag_sets = config.sanitize_flag_sets(['1set', '_set2']) - assert flag_sets == ['1set'] - - flag_sets = config.sanitize_flag_sets(['Set1', 'SET2']) - assert sorted(flag_sets) == ['set1', 'set2'] - - flag_sets = config.sanitize_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5']) - assert flag_sets == [] - - flag_sets = config.sanitize_flag_sets(['set4', 'set1', 'set3', 'set1']) - assert sorted(flag_sets) == ['set1', 'set3', 'set4'] - - flag_sets = config.sanitize_flag_sets(['w' * 50, 's' * 51]) - assert flag_sets == ['w' * 50] - - flag_sets = config.sanitize_flag_sets('set1') - assert flag_sets == [] - - flag_sets = config.sanitize_flag_sets([12, 33]) - assert flag_sets == [] diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index bceb39b0..0a92fc1d 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -55,7 +55,7 @@ def test_get_treatment(self, mocker): assert client.get_treatment(None, 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatment') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -139,7 +139,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment(Key(None, 'bucketing_key'), 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'matching_key', 'matching_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -188,7 +188,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment(Key('matching_key', None), 'some_feature') == CONTROL assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'bucketing_key', 'bucketing_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment', 'key', 'key') ] _logger.reset_mock() @@ -232,7 +232,7 @@ def test_get_treatment(self, mocker): _logger.reset_mock() assert client.get_treatment('matching_key', ' some_feature ', None) == 'default_treatment' assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatment', ' some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatment', 'feature flag name', ' some_feature ') ] _logger.reset_mock() @@ -289,7 +289,7 @@ def _configs(treatment): assert client.get_treatment_with_config(None, 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatment_with_config') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -373,7 +373,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config(Key(None, 'bucketing_key'), 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'matching_key', 'matching_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -422,7 +422,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config(Key('matching_key', None), 'some_feature') == (CONTROL, None) assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'bucketing_key', 'bucketing_key') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatment_with_config', 'key', 'key') ] _logger.reset_mock() @@ -466,7 +466,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatment_with_config('matching_key', ' some_feature ', None) == ('default_treatment', '{"some": "property"}') assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatment_with_config', ' some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatment_with_config', 'feature flag name', ' some_feature ') ] _logger.reset_mock() @@ -629,7 +629,7 @@ def test_track(self, mocker): _logger.reset_mock() assert client.track("some_key", "TRAFFIC_type", "event_type", 1) is True assert _logger.warning.mock_calls == [ - mocker.call("track: %s should be all lowercase - converting string to lowercase.", 'TRAFFIC_type') + mocker.call("track: traffic type 'TRAFFIC_type' should be all lowercase - converting string to lowercase") ] assert client.track("some_key", "traffic_type", None, 1) is False @@ -666,10 +666,10 @@ def test_track(self, mocker): assert _logger.error.mock_calls == [ mocker.call("%s: you passed %s, event_type must adhere to the regular " "expression %s. This means " - "an event name must be alphanumeric, cannot be more than 80 " + "%s must be alphanumeric, cannot be more than 80 " "characters long, and can only include a dash, underscore, " "period, or colon as separators of alphanumeric characters.", - 'track', '@@', '^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$') + 'track', '@@', '^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$', 'an event name') ] _logger.reset_mock() @@ -837,7 +837,7 @@ def test_get_treatments(self, mocker): assert client.get_treatments(None, ['some_feature']) == {'some_feature': CONTROL} assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatments') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatments', 'key', 'key') ] _logger.reset_mock() @@ -912,7 +912,7 @@ def test_get_treatments(self, mocker): _logger.reset_mock() assert client.get_treatments('some_key', ['some ']) == {'some': 'default_treatment'} assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatments', 'some ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatments', 'feature flag name', 'some ') ] _logger.reset_mock() @@ -978,7 +978,7 @@ def _configs(treatment): assert client.get_treatments_with_config(None, ['some_feature']) == {'some_feature': (CONTROL, None)} assert _logger.error.mock_calls == [ - mocker.call('%s: you passed a null key, key must be a non-empty string.', 'get_treatments_with_config') + mocker.call('%s: you passed a null %s, %s must be a non-empty string.', 'get_treatments_with_config', 'key', 'key') ] _logger.reset_mock() @@ -1053,7 +1053,7 @@ def _configs(treatment): _logger.reset_mock() assert client.get_treatments_with_config('some_key', ['some_feature ']) == {'some_feature': ('default_treatment', '{"some": "property"}')} assert _logger.warning.mock_calls == [ - mocker.call('%s: feature flag name \'%s\' has extra whitespace, trimming.', 'get_treatments_with_config', 'some_feature ') + mocker.call('%s: %s \'%s\' has extra whitespace, trimming.', 'get_treatments_with_config', 'feature flag name', 'some_feature ') ] _logger.reset_mock() @@ -1074,6 +1074,33 @@ def _configs(treatment): ) ] + def test_flag_sets_validation(self): + """Test sanitization for flag sets.""" + flag_sets = input_validator.validate_flag_sets([' set1', 'set2 ', 'set3'], 'method') + assert sorted(flag_sets) == ['set1', 'set2', 'set3'] + + flag_sets = input_validator.validate_flag_sets(['1set', '_set2'], 'method') + assert flag_sets == ['1set'] + + flag_sets = input_validator.validate_flag_sets(['Set1', 'SET2'], 'method') + assert sorted(flag_sets) == ['set1', 'set2'] + + flag_sets = input_validator.validate_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5'], 'method') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets(['set4', 'set1', 'set3', 'set1'], 'method') + assert sorted(flag_sets) == ['set1', 'set3', 'set4'] + + flag_sets = input_validator.validate_flag_sets(['w' * 50, 's' * 51], 'method') + assert flag_sets == ['w' * 50] + + flag_sets = input_validator.validate_flag_sets('set1', 'method') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets([12, 33], 'method') + assert flag_sets == [] + + class ManagerInputValidationTests(object): #pylint: disable=too-few-public-methods """Manager input validation test cases.""" @@ -1265,3 +1292,29 @@ def test_validate_pluggable_adapter(self): # using non-string type prefix should not pass assert(not input_validator.validate_pluggable_adapter({'storageType': 'pluggable', 'storagePrefix': 'myprefix', 123: self.mock_adapter4()})) + + def test_sanitize_flag_sets(self): + """Test sanitization for flag sets.""" + flag_sets = input_validator.validate_flag_sets([' set1', 'set2 ', 'set3'], 'm') + assert flag_sets == ['set1', 'set2', 'set3'] + + flag_sets = input_validator.validate_flag_sets(['1set', '_set2'], 'm') + assert flag_sets == ['1set'] + + flag_sets = input_validator.validate_flag_sets(['Set1', 'SET2'], 'm') + assert flag_sets == ['set1', 'set2'] + + flag_sets = input_validator.validate_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5'], 'm') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets(['set4', 'set1', 'set3', 'set1'], 'm') + assert flag_sets == ['set1', 'set3', 'set4'] + + flag_sets = input_validator.validate_flag_sets(['w' * 50, 's' * 51], 'm') + assert flag_sets == ['w' * 50] + + flag_sets = input_validator.validate_flag_sets('set1', 'm') + assert flag_sets == [] + + flag_sets = input_validator.validate_flag_sets([12, 33], 'm') + assert flag_sets == [] diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index b3ecf076..b1babada 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -733,7 +733,7 @@ def setup_method(self): redis_client.set(split_storage._get_key(split['name']), json.dumps(split)) if split.get('sets') is not None: for flag_set in split.get('sets'): - redis_client.sadd(split_storage._get_set_key(flag_set), split['name']) + redis_client.sadd(split_storage._get_flag_set_key(flag_set), split['name']) redis_client.set(split_storage._FEATURE_FLAG_TILL_KEY, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') @@ -1178,7 +1178,7 @@ def setup_method(self): self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) if split.get('sets') is not None: for flag_set in split.get('sets'): - self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.push_items(split_storage._flag_set_prefix.format(flag_set=flag_set), split['name']) self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') @@ -1363,7 +1363,7 @@ def setup_method(self): for split in data['splits']: if split.get('sets') is not None: for flag_set in split.get('sets'): - self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.push_items(split_storage._flag_set_prefix.format(flag_set=flag_set), split['name']) self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) @@ -1548,7 +1548,7 @@ def setup_method(self): for split in data['splits']: if split.get('sets') is not None: for flag_set in split.get('sets'): - self.pluggable_storage_adapter.push_items(split_storage._feature_flag_set_prefix.format(flag_set=flag_set), split['name']) + self.pluggable_storage_adapter.push_items(split_storage._flag_set_prefix.format(flag_set=flag_set), split['name']) self.pluggable_storage_adapter.set(split_storage._prefix.format(feature_flag_name=split['name']), split) self.pluggable_storage_adapter.set(split_storage._feature_flag_till_prefix, data['till']) diff --git a/tests/models/test_flag_sets.py b/tests/models/test_flag_sets.py new file mode 100644 index 00000000..fddff1c6 --- /dev/null +++ b/tests/models/test_flag_sets.py @@ -0,0 +1,59 @@ +from splitio.models.flag_sets import FlagSets, FlagSetsFilter + +class FlagSetsFilterTests(object): + """Flag sets filter storage tests.""" + def test_without_initial_set(self): + flag_set = FlagSets() + assert flag_set.sets_feature_flag_map == {} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == False + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_with_initial_set(self): + flag_set = FlagSets(['set1', 'set2']) + assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == True + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_flag_set_filter(self): + flag_set_filter = FlagSetsFilter() + assert flag_set_filter.flag_sets == set() + assert not flag_set_filter.should_filter + + flag_set_filter = FlagSetsFilter(['set1', 'set2']) + assert flag_set_filter.flag_sets == set({'set1', 'set2'}) + assert flag_set_filter.should_filter + assert flag_set_filter.intersect(set({'set1', 'set2'})) + assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) + assert not flag_set_filter.intersect(set({'set4'})) + assert not flag_set_filter.set_exist('set4') + assert flag_set_filter.set_exist('set1') diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index b472bfef..aa381791 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -1,18 +1,18 @@ """Pluggable storage test module.""" import json import threading +import pytest from splitio.models.splits import Split from splitio.models import splits, segments from splitio.models.segments import Segment from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper +from splitio.models.flag_sets import FlagSetsFilter from splitio.storage.pluggable import PluggableSplitStorage, PluggableSegmentStorage, PluggableImpressionsStorage, PluggableEventsStorage, PluggableTelemetryStorage from splitio.client.util import get_metadata, SdkMetadata from splitio.models.telemetry import MAX_TAGS, MethodExceptionsAndLatencies, OperationMode - from tests.integration import splits_json -import pytest class StorageMockAdapter(object): def __init__(self): @@ -140,6 +140,7 @@ def test_init(self): prefix = '' assert(pluggable_split_storage._prefix == prefix + "SPLITIO.split.{feature_flag_name}") assert(pluggable_split_storage._traffic_type_prefix == prefix + "SPLITIO.trafficType.{traffic_type_name}") + assert(pluggable_split_storage._flag_set_prefix == prefix + "SPLITIO.flagSet.{flag_set}") assert(pluggable_split_storage._feature_flag_till_prefix == prefix + "SPLITIO.splits.till") # TODO: To be added when producer mode is aupported @@ -235,6 +236,21 @@ def test_get_all(self): all_splits = pluggable_split_storage.get_all() assert([all_splits[0].to_json(), all_splits[1].to_json()] == [split1.to_json(), split2.to_json()]) + def test_flag_sets(self, mocker): + """Test Flag sets scenarios.""" + self.mock_adapter._keys = {'SPLITIO.flagSet.set1': ['split1'], 'SPLITIO.flagSet.set2': ['split1','split2']} + pluggable_split_storage = PluggableSplitStorage(self.mock_adapter) + assert pluggable_split_storage.flag_set_filter.flag_sets == set({}) + assert sorted(pluggable_split_storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] + + pluggable_split_storage.flag_set_filter = FlagSetsFilter(['set2', 'set3']) + assert pluggable_split_storage.get_feature_flags_by_sets(['set1']) == [] + assert sorted(pluggable_split_storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] + + storage2 = PluggableSplitStorage(self.mock_adapter, None, ['set2', 'set3']) + assert storage2.flag_set_filter.flag_sets == set({'set2', 'set3'}) + + # TODO: To be added when producer mode is aupported # def test_kill_locally(self): # self.mock_adapter._keys = {} diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 7ee00ca8..0413ca8b 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -14,7 +14,7 @@ from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MethodExceptionsAndLatencies - +from splitio.models.flag_sets import FlagSetsFilter class RedisSplitStorageTests(object): """Redis split storage test cases.""" @@ -177,15 +177,15 @@ def test_flag_sets(self, mocker): """Test Flag sets scenarios.""" adapter = build({}) storage = RedisSplitStorage(adapter, True, 1) - assert storage._config_flag_sets == [] + assert storage.flag_set_filter.flag_sets == set({}) assert sorted(storage.get_feature_flags_by_sets(['set1', 'set2'])) == ['split1', 'split2'] - storage._config_flag_sets = ['set2', 'set3'] + storage.flag_set_filter = FlagSetsFilter(['set2', 'set3']) assert storage.get_feature_flags_by_sets(['set1']) == [] assert sorted(storage.get_feature_flags_by_sets(['set2'])) == ['split1', 'split2'] storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) - assert storage2._config_flag_sets == ['set2', 'set3'] + assert storage2.flag_set_filter.flag_sets == set({'set2', 'set3'}) class RedisSegmentStorageTests(object): """Redis segment storage test cases.""" diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index cfe85577..e59e9a4d 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -1,8 +1,10 @@ """Storage Helper tests.""" +import pytest from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets from splitio.storage.inmemmory import InMemorySplitStorage from splitio.models import splits +from splitio.models.flag_sets import FlagSetsFilter from tests.sync.test_splits_synchronizer import splits as split_sample class StorageHelperTests(object): @@ -27,40 +29,39 @@ def is_flag_set_exist(flag_set): class flag_set_filter(): def should_filter(): return False - def intersect(sets): return True storage.flag_set_filter = flag_set_filter + storage.flag_set_filter.flag_sets = {} update_feature_flag_storage(storage, [split], 123) assert self.added[0] == split assert self.deleted == [] assert self.change_number == 123 - class flag_set_filter(): + class flag_set_filter2(): def should_filter(): return True - def intersect(sets): return False - storage.flag_set_filter = flag_set_filter + storage.flag_set_filter = flag_set_filter2 + storage.flag_set_filter.flag_sets = set({'set1', 'set2'}) update_feature_flag_storage(storage, [split], 123) assert self.added == [] assert self.deleted[0] == split.name - def is_flag_set_exist2(flag_set): - return True - storage.is_flag_set_exist = is_flag_set_exist2 - - class flag_set_filter(): + class flag_set_filter3(): def should_filter(): return True - def intersect(sets): return True - storage.flag_set_filter = flag_set_filter + storage.flag_set_filter = flag_set_filter3 + storage.flag_set_filter.flag_sets = set({'set1', 'set2'}) + def is_flag_set_exist2(flag_set): + return True + storage.is_flag_set_exist = is_flag_set_exist2 update_feature_flag_storage(storage, [split], 123) assert self.added[0] == split assert self.deleted == [] @@ -94,26 +95,27 @@ def intersect(sets): ) split = splits.from_raw(split_json) + storage.config_flag_sets_used = 0 assert update_feature_flag_storage(storage, [split], 123) == {'segment1'} def test_get_valid_flag_sets(self): flag_sets = ['set1', 'set2'] - config_flag_sets = [] + config_flag_sets = FlagSetsFilter([]) assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1', 'set2'] - config_flag_sets = ['set1'] + config_flag_sets = FlagSetsFilter(['set1']) assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set1'] flag_sets = ['set2', 'set3'] - config_flag_sets = ['set1', 'set2'] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) assert get_valid_flag_sets(flag_sets, config_flag_sets) == ['set2'] flag_sets = ['set3', 'set4'] - config_flag_sets = ['set1', 'set2'] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] flag_sets = [] - config_flag_sets = ['set1', 'set2'] + config_flag_sets = FlagSetsFilter(['set1', 'set2']) assert get_valid_flag_sets(flag_sets, config_flag_sets) == [] def test_combine_valid_flag_sets(self): From 3d9f00565f70d13df8d8a81cbbdaa37fa2777404 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 14 Sep 2023 11:35:26 -0700 Subject: [PATCH 56/77] fixed config --- splitio/client/config.py | 2 +- tests/client/test_config.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index 31b16bec..38dbc0b8 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -145,6 +145,6 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 - processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'])) if processed['flagSetsFilter'] is not None else None + processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None return processed diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 68760031..095d4c76 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -69,3 +69,4 @@ def test_sanitize(self): processed = config.sanitize('some', configs) assert processed['redisLocalCacheEnabled'] # check default is True + assert processed['flagSetsFilter'] is None From 8a48ab4150351a2d6322b09836b116677c05dcce Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 14 Sep 2023 16:39:39 -0700 Subject: [PATCH 57/77] Added flagset methods to Telemetry and added localhost json sync splits with flag sets --- splitio/client/factory.py | 2 +- splitio/engine/telemetry.py | 30 ++-- splitio/sync/split.py | 24 ++- tests/integration/__init__.py | 6 +- .../integration/files/split_changes_temp.json | 2 +- tests/sync/test_splits_synchronizer.py | 169 ++++++++++++------ tests/sync/test_telemetry.py | 14 +- tests/util/test_storage_helper.py | 2 +- 8 files changed, 160 insertions(+), 89 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 5a8309e5..86f74fe9 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -600,7 +600,7 @@ def _build_localhost_factory(cfg): telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() storages = { - 'splits': InMemorySplitStorage(), + 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(), # not used, just to avoid possible future errors. 'impressions': LocalhostImpressionsStorage(), 'events': LocalhostEventsStorage(), diff --git a/splitio/engine/telemetry.py b/splitio/engine/telemetry.py index e1802131..afb8cf2d 100644 --- a/splitio/engine/telemetry.py +++ b/splitio/engine/telemetry.py @@ -222,17 +222,27 @@ def pop_formatted_stats(self): exceptions = self.pop_exceptions()['methodExceptions'] latencies = self.pop_latencies()['methodLatencies'] return { - 'mE': {'t': exceptions['treatment'], - 'ts': exceptions['treatments'], - 'tc': exceptions['treatment_with_config'], - 'tcs': exceptions['treatments_with_config'], - 'tr': exceptions['track'] + 'mE': { + 't': exceptions['treatment'], + 'ts': exceptions['treatments'], + 'tc': exceptions['treatment_with_config'], + 'tcs': exceptions['treatments_with_config'], + 'tf': exceptions['treatments_by_flag_set'], + 'tfs': exceptions['treatments_by_flag_sets'], + 'tcf': exceptions['treatments_with_config_by_flag_set'], + 'tcfs': exceptions['treatments_with_config_by_flag_sets'], + 'tr': exceptions['track'] }, - 'mL': {'t': latencies['treatment'], - 'ts': latencies['treatments'], - 'tc': latencies['treatment_with_config'], - 'tcs': latencies['treatments_with_config'], - 'tr': latencies['track'] + 'mL': { + 't': latencies['treatment'], + 'ts': latencies['treatments'], + 'tc': latencies['treatment_with_config'], + 'tcs': latencies['treatments_with_config'], + 'tf': latencies['treatments_by_flag_set'], + 'tfs': latencies['treatments_by_flag_sets'], + 'tcf': latencies['treatments_with_config_by_flag_set'], + 'tcfs': latencies['treatments_with_config_by_flag_sets'], + 'tr': latencies['track'] }, } diff --git a/splitio/sync/split.py b/splitio/sync/split.py index 559a1543..ff42286b 100644 --- a/splitio/sync/split.py +++ b/splitio/sync/split.py @@ -10,6 +10,7 @@ from splitio.api import APIException from splitio.api.commons import FetchOptions +from splitio.client.input_validator import validate_flag_sets from splitio.models import splits from splitio.util.backoff import Backoff from splitio.util.time import get_current_epoch_time_ms @@ -376,20 +377,12 @@ def _synchronize_json(self): self._current_json_sha = fecthed_sha if self._feature_flag_storage.get_change_number() > till and till != self._DEFAULT_FEATURE_FLAG_TILL: return [] - to_add = [] - to_delete = [] - for feature_flag in fetched: - if feature_flag['status'] == splits.Status.ACTIVE.value: - parsed = splits.from_raw(feature_flag) - to_add.append(parsed) - _LOGGER.debug("feature flag %s is updated", parsed.name) - segment_list.update(set(parsed.get_segment_names())) - else: - to_delete.append(feature_flag['name']) - - self._feature_flag_storage.update(to_add, to_delete, till) + fetched_feature_flags = [] + [fetched_feature_flags.append(splits.from_raw(feature_flag)) for feature_flag in fetched] + segment_list = update_feature_flag_storage(self._feature_flag_storage, fetched_feature_flags, till) return segment_list except Exception as exc: + _LOGGER.debug(exc) raise ValueError("Error reading feature flags from json.") from exc def _read_feature_flags_from_json_file(self, filename): @@ -441,7 +434,7 @@ def _sanitize_json_elements(self, parsed): if 'till' not in parsed or parsed['till'] is None or parsed['till'] < -1: parsed['till'] = -1 if 'since' not in parsed or parsed['since'] is None or parsed['since'] < -1 or parsed['since'] > parsed['till']: - parsed['since'] = parsed['till'] + parsed['since'] = parsed['till'] return parsed @@ -471,6 +464,11 @@ def _sanitize_feature_flag_elements(self, parsed_feature_flags): ('algo', 2, 2, 2, None, None)]: feature_flag = util._sanitize_object_element(feature_flag, 'split', element[0], element[1], lower_value=element[2], upper_value=element[3], in_list=element[4], not_in_list=element[5]) feature_flag = self._sanitize_condition(feature_flag) + + if 'sets' not in feature_flag: + feature_flag['sets'] = [] + feature_flag['sets'] = validate_flag_sets(feature_flag['sets'], 'Localhost Validator') + sanitized_feature_flags.append(feature_flag) return sanitized_feature_flags diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 6475e24d..aae9e014 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1,6 +1,6 @@ -split11 = {"splits": [{"trafficTypeName": "user", "name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779, "seed": -113875324, "status": "ACTIVE","killed": False, "defaultTreatment": "off", "changeNumber": 1675443569027,"algo": 2, "configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}]},{"trafficTypeName": "user", "name": "SPLIT_1", "trafficAllocation": 100, "trafficAllocationSeed": -1780071202,"seed": -1442762199, "status": "ACTIVE","killed": False, "defaultTreatment": "off", "changeNumber": 1675443537882,"algo": 2, "configurations": {},"conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 0 },{ "treatment": "off", "size": 100 }],"label": "default rule"}]}],"since": -1,"till": 1675443569027} -split12 = {"splits": [{"trafficTypeName": "user","name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779,"seed": -113875324,"status": "ACTIVE","killed": True,"defaultTreatment": "off","changeNumber": 1675443767288,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}]}],"since": 1675443569027,"till": 167544376728} -split13 = {"splits": [{"trafficTypeName": "user","name": "SPLIT_1","trafficAllocation": 100,"trafficAllocationSeed": -1780071202,"seed": -1442762199,"status": "ARCHIVED","killed": False,"defaultTreatment": "off","changeNumber": 1675443984594,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 0 },{ "treatment": "off", "size": 100 }],"label": "default rule"}]},{"trafficTypeName": "user","name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779,"seed": -113875324,"status": "ACTIVE","killed": False,"defaultTreatment": "off","changeNumber": 1675443954220,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}]}],"since": 1675443767288,"till": 1675443984594} +split11 = {"splits": [{"trafficTypeName": "user", "name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779, "seed": -113875324, "status": "ACTIVE","killed": False, "defaultTreatment": "off", "changeNumber": 1675443569027,"algo": 2, "configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}], "sets": ["set2"]},{"trafficTypeName": "user", "name": "SPLIT_1", "trafficAllocation": 100, "trafficAllocationSeed": -1780071202,"seed": -1442762199, "status": "ACTIVE","killed": False, "defaultTreatment": "off", "changeNumber": 1675443537882,"algo": 2, "configurations": {},"conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 0 },{ "treatment": "off", "size": 100 }],"label": "default rule"}], "sets": ["set1"]}],"since": -1,"till": 1675443569027} +split12 = {"splits": [{"trafficTypeName": "user","name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779,"seed": -113875324,"status": "ACTIVE","killed": True,"defaultTreatment": "off","changeNumber": 1675443767288,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}], "sets": ["set3"]}],"since": 1675443569027,"till": 167544376728} +split13 = {"splits": [{"trafficTypeName": "user","name": "SPLIT_1","trafficAllocation": 100,"trafficAllocationSeed": -1780071202,"seed": -1442762199,"status": "ARCHIVED","killed": False,"defaultTreatment": "off","changeNumber": 1675443984594,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 0 },{ "treatment": "off", "size": 100 }],"label": "default rule"}]},{"trafficTypeName": "user","name": "SPLIT_2","trafficAllocation": 100,"trafficAllocationSeed": 1057590779,"seed": -113875324,"status": "ACTIVE","killed": False,"defaultTreatment": "off","changeNumber": 1675443954220,"algo": 2,"configurations": {},"conditions": [{"conditionType": "ROLLOUT","matcherGroup": {"combiner": "AND","matchers": [{"keySelector": { "trafficType": "user", "attribute": None },"matcherType": "ALL_KEYS","negate": False,"userDefinedSegmentMatcherData": None,"whitelistMatcherData": None,"unaryNumericMatcherData": None,"betweenMatcherData": None,"booleanMatcherData": None,"dependencyMatcherData": None,"stringMatcherData": None}]},"partitions": [{ "treatment": "on", "size": 100 },{ "treatment": "off", "size": 0 }],"label": "default rule"}], "sets": ["set1", "set2"]}],"since": 1675443767288,"till": 1675443984594} split41 = split11 split42 = split12 diff --git a/tests/integration/files/split_changes_temp.json b/tests/integration/files/split_changes_temp.json index 162c0b17..c8ad59e1 100644 --- a/tests/integration/files/split_changes_temp.json +++ b/tests/integration/files/split_changes_temp.json @@ -1 +1 @@ -{"splits": [{"trafficTypeName": "user", "name": "SPLIT_1", "trafficAllocation": 100, "trafficAllocationSeed": -1780071202, "seed": -1442762199, "status": "ARCHIVED", "killed": false, "defaultTreatment": "off", "changeNumber": 1675443984594, "algo": 2, "configurations": {}, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": null}, "matcherType": "ALL_KEYS", "negate": false, "userDefinedSegmentMatcherData": null, "whitelistMatcherData": null, "unaryNumericMatcherData": null, "betweenMatcherData": null, "booleanMatcherData": null, "dependencyMatcherData": null, "stringMatcherData": null}]}, "partitions": [{"treatment": "on", "size": 0}, {"treatment": "off", "size": 100}], "label": "default rule"}]}, {"trafficTypeName": "user", "name": "SPLIT_2", "trafficAllocation": 100, "trafficAllocationSeed": 1057590779, "seed": -113875324, "status": "ACTIVE", "killed": false, "defaultTreatment": "off", "changeNumber": 1675443954220, "algo": 2, "configurations": {}, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": null}, "matcherType": "ALL_KEYS", "negate": false, "userDefinedSegmentMatcherData": null, "whitelistMatcherData": null, "unaryNumericMatcherData": null, "betweenMatcherData": null, "booleanMatcherData": null, "dependencyMatcherData": null, "stringMatcherData": null}]}, "partitions": [{"treatment": "on", "size": 100}, {"treatment": "off", "size": 0}], "label": "default rule"}]}], "since": -1, "till": -1} \ No newline at end of file +{"splits": [{"trafficTypeName": "user", "name": "SPLIT_1", "trafficAllocation": 100, "trafficAllocationSeed": -1780071202, "seed": -1442762199, "status": "ARCHIVED", "killed": false, "defaultTreatment": "off", "changeNumber": 1675443984594, "algo": 2, "configurations": {}, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": null}, "matcherType": "ALL_KEYS", "negate": false, "userDefinedSegmentMatcherData": null, "whitelistMatcherData": null, "unaryNumericMatcherData": null, "betweenMatcherData": null, "booleanMatcherData": null, "dependencyMatcherData": null, "stringMatcherData": null}]}, "partitions": [{"treatment": "on", "size": 0}, {"treatment": "off", "size": 100}], "label": "default rule"}]}, {"trafficTypeName": "user", "name": "SPLIT_2", "trafficAllocation": 100, "trafficAllocationSeed": 1057590779, "seed": -113875324, "status": "ACTIVE", "killed": false, "defaultTreatment": "off", "changeNumber": 1675443954220, "algo": 2, "configurations": {}, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": null}, "matcherType": "ALL_KEYS", "negate": false, "userDefinedSegmentMatcherData": null, "whitelistMatcherData": null, "unaryNumericMatcherData": null, "betweenMatcherData": null, "booleanMatcherData": null, "dependencyMatcherData": null, "stringMatcherData": null}]}, "partitions": [{"treatment": "on", "size": 100}, {"treatment": "off", "size": 0}], "label": "default rule"}], "sets": ["set1", "set2"]}], "since": -1, "till": -1} \ No newline at end of file diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index 8b8379e1..643fb144 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -3,6 +3,7 @@ import pytest import os import json +import copy from splitio.util.backoff import Backoff from splitio.api import APIException @@ -13,7 +14,7 @@ from splitio.sync.split import SplitSynchronizer, LocalSplitSynchronizer, LocalhostMode from tests.integration import splits_json -splits = [{ +splits_raw = [{ 'changeNumber': 123, 'trafficTypeName': 'user', 'name': 'some_name', @@ -53,6 +54,8 @@ class SplitsSynchronizerTests(object): """Split synchronizer test cases.""" + splits = copy.deepcopy(splits_raw) + def test_synchronize_splits_error(self, mocker): """Test that if fetching splits fails at some_point, the task will continue running.""" storage = mocker.Mock(spec=InMemorySplitStorage) @@ -104,7 +107,7 @@ def get_changes(*args, **kwargs): if get_changes.called == 1: return { - 'splits': splits, + 'splits': self.splits, 'since': -1, 'till': 123 } @@ -181,7 +184,7 @@ def change_number_mock(): def get_changes(*args, **kwargs): get_changes.called += 1 if get_changes.called == 1: - return { 'splits': splits, 'since': -1, 'till': 123 } + return { 'splits': self.splits, 'since': -1, 'till': 123 } elif get_changes.called == 2: return { 'splits': [], 'since': 123, 'till': 123 } elif get_changes.called == 3: @@ -227,12 +230,12 @@ def test_sync_flag_sets_with_config_sets(self, mocker): """Test split sync with flag sets.""" storage = InMemorySplitStorage(['set1', 'set2']) - split = splits[0].copy() + split = self.splits[0].copy() split['name'] = 'second' - splits1 = [splits[0].copy(), split] - splits2 = splits.copy() - splits3 = splits.copy() - splits4 = splits.copy() + splits1 = [self.splits[0].copy(), split] + splits2 = self.splits.copy() + splits3 = self.splits.copy() + splits4 = self.splits.copy() api = mocker.Mock() def get_changes(*args, **kwargs): get_changes.called += 1 @@ -268,12 +271,12 @@ def test_sync_flag_sets_without_config_sets(self, mocker): """Test split sync with flag sets.""" storage = InMemorySplitStorage() - split = splits[0].copy() + split = self.splits[0].copy() split['name'] = 'second' - splits1 = [splits[0].copy(), split] - splits2 = splits.copy() - splits3 = splits.copy() - splits4 = splits.copy() + splits1 = [self.splits[0].copy(), split] + splits2 = self.splits.copy() + splits3 = self.splits.copy() + splits4 = self.splits.copy() api = mocker.Mock() def get_changes(*args, **kwargs): get_changes.called += 1 @@ -308,6 +311,8 @@ def get_changes(*args, **kwargs): class LocalSplitsSynchronizerTests(object): """Split synchronizer test cases.""" + splits = copy.deepcopy(splits_raw) + def test_synchronize_splits_error(self, mocker): """Test that if fetching splits fails at some_point, the task will continue running.""" storage = mocker.Mock(spec=SplitStorage) @@ -321,80 +326,127 @@ def test_synchronize_splits(self, mocker): storage = InMemorySplitStorage() till = 123 - splits = [{ - 'changeNumber': 123, - 'trafficTypeName': 'user', - 'name': 'some_name', - 'trafficAllocation': 100, - 'trafficAllocationSeed': 123456, - 'seed': 321654, - 'status': 'ACTIVE', - 'killed': False, - 'defaultTreatment': 'off', - 'algo': 2, - 'conditions': [ - { - 'partitions': [ - {'treatment': 'on', 'size': 50}, - {'treatment': 'off', 'size': 50} - ], - 'contitionType': 'WHITELIST', - 'label': 'some_label', - 'matcherGroup': { - 'matchers': [ - { - 'matcherType': 'WHITELIST', - 'whitelistMatcherData': { - 'whitelist': ['k1', 'k2', 'k3'] - }, - 'negate': False, - } - ], - 'combiner': 'AND' - } - } - ] - }] - def read_feature_flags_from_json_file(*args, **kwargs): - return splits, till + return self.splits, till split_synchronizer = LocalSplitSynchronizer("split.json", storage, LocalhostMode.JSON) split_synchronizer._read_feature_flags_from_json_file = read_feature_flags_from_json_file split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert isinstance(inserted_split, Split) assert inserted_split.name == 'some_name' # Should sync when changenumber is not changed - splits[0]['killed'] = True + self.splits[0]['killed'] = True split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert inserted_split.killed # Should not sync when changenumber is less than stored till = 122 - splits[0]['killed'] = False + self.splits[0]['killed'] = False split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert inserted_split.killed # Should sync when changenumber is higher than stored till = 124 split_synchronizer._current_json_sha = "-1" split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert inserted_split.killed == False # Should sync when till is default (-1) till = -1 split_synchronizer._current_json_sha = "-1" - splits[0]['killed'] = True + self.splits[0]['killed'] = True split_synchronizer.synchronize_splits() - inserted_split = storage.get(splits[0]['name']) + inserted_split = storage.get(self.splits[0]['name']) assert inserted_split.killed == True + def test_sync_flag_sets_with_config_sets(self, mocker): + """Test split sync with flag sets.""" + storage = InMemorySplitStorage(['set1', 'set2']) + + split = self.splits[0].copy() + split['name'] = 'second' + splits1 = [self.splits[0].copy(), split] + splits2 = self.splits.copy() + splits3 = self.splits.copy() + splits4 = self.splits.copy() + + self.called = 0 + def read_feature_flags_from_json_file(*args, **kwargs): + self.called += 1 + if self.called == 1: + return splits1, 123 + elif self.called == 2: + splits2[0]['sets'] = ['set3'] + return splits2, 124 + elif self.called == 3: + splits3[0]['sets'] = ['set1'] + return splits3, 12434 + splits4[0]['sets'] = ['set6'] + splits4[0]['name'] = 'new_split' + return splits4, 12438 + + split_synchronizer = LocalSplitSynchronizer("split.json", storage, LocalhostMode.JSON) + split_synchronizer._read_feature_flags_from_json_file = read_feature_flags_from_json_file + + split_synchronizer.synchronize_splits() + assert isinstance(storage.get('some_name'), Split) + + split_synchronizer.synchronize_splits(124) + assert storage.get('some_name') == None + + split_synchronizer.synchronize_splits(12434) + assert isinstance(storage.get('some_name'), Split) + + split_synchronizer.synchronize_splits(12438) + assert storage.get('new_name') == None + + def test_sync_flag_sets_without_config_sets(self, mocker): + """Test split sync with flag sets.""" + storage = InMemorySplitStorage() + + split = self.splits[0].copy() + split['name'] = 'second' + splits1 = [self.splits[0].copy(), split] + splits2 = self.splits.copy() + splits3 = self.splits.copy() + splits4 = self.splits.copy() + + self.called = 0 + def read_feature_flags_from_json_file(*args, **kwargs): + self.called += 1 + if self.called == 1: + return splits1, 123 + elif self.called == 2: + splits2[0]['sets'] = ['set3'] + return splits2, 124 + elif self.called == 3: + splits3[0]['sets'] = ['set1'] + return splits3, 12434 + splits4[0]['sets'] = ['set6'] + splits4[0]['name'] = 'third_split' + return splits4, 12438 + + split_synchronizer = LocalSplitSynchronizer("split.json", storage, LocalhostMode.JSON) + split_synchronizer._read_feature_flags_from_json_file = read_feature_flags_from_json_file + + split_synchronizer.synchronize_splits() + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(124) + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(12434) + assert isinstance(storage.get('new_split'), Split) + + split_synchronizer.synchronize_splits(12438) + assert isinstance(storage.get('third_split'), Split) + def test_reading_json(self, mocker): """Test reading json file.""" f = open("./splits.json", "w") @@ -430,7 +482,8 @@ def test_reading_json(self, mocker): 'combiner': 'AND' } } - ] + ], + 'sets': ['set1'] }], "till":1675095324253, "since":-1, diff --git a/tests/sync/test_telemetry.py b/tests/sync/test_telemetry.py index 7884bd96..9d901713 100644 --- a/tests/sync/test_telemetry.py +++ b/tests/sync/test_telemetry.py @@ -1,6 +1,8 @@ """Telemetry Worker tests.""" import unittest.mock as mock import json +import pytest + from splitio.sync.telemetry import TelemetrySynchronizer, InMemoryTelemetrySubmitter from splitio.engine.telemetry import TelemetryEvaluationConsumer, TelemetryInitConsumer, TelemetryRuntimeConsumer, TelemetryStorageConsumer from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemorySegmentStorage, InMemorySplitStorage @@ -51,6 +53,10 @@ def test_synchronize_telemetry(self, mocker): telemetry_storage._method_exceptions._treatments = 1 telemetry_storage._method_exceptions._treatment_with_config = 5 telemetry_storage._method_exceptions._treatments_with_config = 1 + telemetry_storage._method_exceptions._treatments_by_flag_set = 2 + telemetry_storage._method_exceptions._treatments_by_flag_sets = 3 + telemetry_storage._method_exceptions._treatments_with_config_by_flag_set = 4 + telemetry_storage._method_exceptions._treatments_with_config_by_flag_sets = 6 telemetry_storage._method_exceptions._track = 3 telemetry_storage._last_synchronization._split = 5 @@ -76,6 +82,10 @@ def test_synchronize_telemetry(self, mocker): telemetry_storage._method_latencies._treatments = [0] * 23 telemetry_storage._method_latencies._treatment_with_config = [0] * 23 telemetry_storage._method_latencies._treatments_with_config = [0] * 23 + telemetry_storage._method_latencies._treatments_by_flag_set = [1] + [0] * 22 + telemetry_storage._method_latencies._treatments_by_flag_sets = [0] * 23 + telemetry_storage._method_latencies._treatments_with_config_by_flag_set = [1] + [0] * 22 + telemetry_storage._method_latencies._treatments_with_config_by_flag_sets = [0] * 23 telemetry_storage._method_latencies._track = [0] * 23 telemetry_storage._http_latencies._split = [1] + [0] * 22 @@ -130,8 +140,8 @@ def record_stats(*args, **kwargs): "tR": 3, "sE": [], "sL": 3, - "mE": {"t": 10, "ts": 1, "tc": 5, "tcs": 1, "tr": 3}, - "mL": {"t": [1] + [0] * 22, "ts": [0] * 23, "tc": [0] * 23, "tcs": [0] * 23, "tr": [0] * 23}, + "mE": {"t": 10, "ts": 1, "tc": 5, "tcs": 1, "tf": 2, "tfs": 3, "tcf": 4, "tcfs": 6, "tr": 3}, + "mL": {"t": [1] + [0] * 22, "ts": [0] * 23, "tc": [0] * 23, "tcs": [0] * 23, "tf": [1] + [0] * 22, "tfs": [0] * 23, "tcf": [1] + [0] * 22, "tcfs": [0] * 23, "tr": [0] * 23}, "spC": 1, "seC": 1, "skC": 0, diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index e59e9a4d..97fb2fd7 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -5,7 +5,7 @@ from splitio.storage.inmemmory import InMemorySplitStorage from splitio.models import splits from splitio.models.flag_sets import FlagSetsFilter -from tests.sync.test_splits_synchronizer import splits as split_sample +from tests.sync.test_splits_synchronizer import splits_raw as split_sample class StorageHelperTests(object): From c15e33764ff5d5599053a1bc14969dcef98dba8c Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 14 Sep 2023 16:56:27 -0700 Subject: [PATCH 58/77] polish --- splitio/models/telemetry.py | 78 ++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index 3e9af26f..7b19a747 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -27,7 +27,7 @@ class CounterConstants(Enum): EVENTS_QUEUED = 'eventsQueued' EVENTS_DROPPED = 'eventsDropped' -class ConfigParams(Enum): +class _ConfigParams(Enum): """Config parameters constants""" SPLITS_REFRESH_RATE = 'featuresRefreshRate' SEGMENTS_REFRESH_RATE = 'segmentsRefreshRate' @@ -42,7 +42,7 @@ class ConfigParams(Enum): IMPRESSIONS_MODE = 'impressionsMode' IMPRESSIONS_LISTENER = 'impressionListener' -class ExtraConfig(Enum): +class _ExtraConfig(Enum): """Extra config constants""" ACTIVE_FACTORY_COUNT = 'activeFactoryCount' REDUNDANT_FACTORY_COUNT = 'redundantFactoryCount' @@ -53,7 +53,7 @@ class ExtraConfig(Enum): HTTP_PROXY = 'httpProxy' HTTPS_PROXY_ENV = 'HTTPS_PROXY' -class ApiURLs(Enum): +class _ApiURLs(Enum): """Api URL constants""" SDK_URL = 'sdk_url' EVENTS_URL = 'events_url' @@ -88,7 +88,7 @@ class MethodExceptionsAndLatencies(Enum): TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 'treatments_with_config_by_flag_sets' TRACK = 'track' -class LastSynchronizationConstants(Enum): +class _LastSynchronizationConstants(Enum): """Last sync constants""" LAST_SYNCHRONIZATIONS = 'lastSynchronizations' @@ -108,7 +108,7 @@ class SSESyncMode(Enum): STREAMING = 0 POLLING = 1 -class StreamingEventsConstant(Enum): +class _StreamingEventsConstant(Enum): """Storage types constant""" STREAMING_EVENTS = 'streamingEvents' @@ -426,7 +426,7 @@ def get_all(self): :rtype: dict """ with self._lock: - return {LastSynchronizationConstants.LAST_SYNCHRONIZATIONS.value: {HTTPExceptionsAndLatencies.SPLIT.value: self._split, HTTPExceptionsAndLatencies.SEGMENT.value: self._segment, HTTPExceptionsAndLatencies.IMPRESSION.value: self._impression, + return {_LastSynchronizationConstants.LAST_SYNCHRONIZATIONS.value: {HTTPExceptionsAndLatencies.SPLIT.value: self._split, HTTPExceptionsAndLatencies.SEGMENT.value: self._segment, HTTPExceptionsAndLatencies.IMPRESSION.value: self._impression, HTTPExceptionsAndLatencies.IMPRESSION_COUNT.value: self._impression_count, HTTPExceptionsAndLatencies.EVENT.value: self._event, HTTPExceptionsAndLatencies.TELEMETRY.value: self._telemetry, HTTPExceptionsAndLatencies.TOKEN.value: self._token} } @@ -760,7 +760,7 @@ def pop_streaming_events(self): with self._lock: streaming_events = self._streaming_events self._streaming_events = [] - return {StreamingEventsConstant.STREAMING_EVENTS.value: [{'e': streaming_event.type, 'd': streaming_event.data, + return {_StreamingEventsConstant.STREAMING_EVENTS.value: [{'e': streaming_event.type, 'd': streaming_event.data, 't': streaming_event.time} for streaming_event in streaming_events]} class TelemetryConfig(object): @@ -782,10 +782,10 @@ def _reset_all(self): self._operation_mode = None self._storage_type = None self._streaming_enabled = None - self._refresh_rate = {ConfigParams.SPLITS_REFRESH_RATE.value: 0, ConfigParams.SEGMENTS_REFRESH_RATE.value: 0, - ConfigParams.IMPRESSIONS_REFRESH_RATE.value: 0, ConfigParams.EVENTS_REFRESH_RATE.value: 0, ConfigParams.TELEMETRY_REFRESH_RATE.value: 0} - self._url_override = {ApiURLs.SDK_URL.value: False, ApiURLs.EVENTS_URL.value: False, ApiURLs.AUTH_URL.value: False, - ApiURLs.STREAMING_URL.value: False, ApiURLs.TELEMETRY_URL.value: False} + self._refresh_rate = {_ConfigParams.SPLITS_REFRESH_RATE.value: 0, _ConfigParams.SEGMENTS_REFRESH_RATE.value: 0, + _ConfigParams.IMPRESSIONS_REFRESH_RATE.value: 0, _ConfigParams.EVENTS_REFRESH_RATE.value: 0, _ConfigParams.TELEMETRY_REFRESH_RATE.value: 0} + self._url_override = {_ApiURLs.SDK_URL.value: False, _ApiURLs.EVENTS_URL.value: False, _ApiURLs.AUTH_URL.value: False, + _ApiURLs.STREAMING_URL.value: False, _ApiURLs.TELEMETRY_URL.value: False} self._impressions_queue_size = 0 self._events_queue_size = 0 self._impressions_mode = None @@ -818,15 +818,15 @@ def record_config(self, config, extra_config): :type config: dict """ with self._lock: - self._operation_mode = self._get_operation_mode(config[ConfigParams.OPERATION_MODE.value]) - self._storage_type = self._get_storage_type(config[ConfigParams.OPERATION_MODE.value], config[ConfigParams.STORAGE_TYPE.value]) - self._streaming_enabled = config[ConfigParams.STREAMING_ENABLED.value] + self._operation_mode = self._get_operation_mode(config[_ConfigParams.OPERATION_MODE.value]) + self._storage_type = self._get_storage_type(config[_ConfigParams.OPERATION_MODE.value], config[_ConfigParams.STORAGE_TYPE.value]) + self._streaming_enabled = config[_ConfigParams.STREAMING_ENABLED.value] self._refresh_rate = self._get_refresh_rates(config) self._url_override = self._get_url_overrides(extra_config) - self._impressions_queue_size = config[ConfigParams.IMPRESSIONS_QUEUE_SIZE.value] - self._events_queue_size = config[ConfigParams.EVENTS_QUEUE_SIZE.value] - self._impressions_mode = self._get_impressions_mode(config[ConfigParams.IMPRESSIONS_MODE.value]) - self._impression_listener = True if config[ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False + self._impressions_queue_size = config[_ConfigParams.IMPRESSIONS_QUEUE_SIZE.value] + self._events_queue_size = config[_ConfigParams.EVENTS_QUEUE_SIZE.value] + self._impressions_mode = self._get_impressions_mode(config[_ConfigParams.IMPRESSIONS_MODE.value]) + self._impression_listener = True if config[_ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False self._http_proxy = self._check_if_proxy_detected() def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): @@ -913,16 +913,16 @@ def get_stats(self): 'oM': self._operation_mode, 'sT': self._storage_type, 'sE': self._streaming_enabled, - 'rR': {'sp': self._refresh_rate[ConfigParams.SPLITS_REFRESH_RATE.value], - 'se': self._refresh_rate[ConfigParams.SEGMENTS_REFRESH_RATE.value], - 'im': self._refresh_rate[ConfigParams.IMPRESSIONS_REFRESH_RATE.value], - 'ev': self._refresh_rate[ConfigParams.EVENTS_REFRESH_RATE.value], - 'te': self._refresh_rate[ConfigParams.TELEMETRY_REFRESH_RATE.value]}, - 'uO': {'s': self._url_override[ApiURLs.SDK_URL.value], - 'e': self._url_override[ApiURLs.EVENTS_URL.value], - 'a': self._url_override[ApiURLs.AUTH_URL.value], - 'st': self._url_override[ApiURLs.STREAMING_URL.value], - 't': self._url_override[ApiURLs.TELEMETRY_URL.value]}, + 'rR': {'sp': self._refresh_rate[_ConfigParams.SPLITS_REFRESH_RATE.value], + 'se': self._refresh_rate[_ConfigParams.SEGMENTS_REFRESH_RATE.value], + 'im': self._refresh_rate[_ConfigParams.IMPRESSIONS_REFRESH_RATE.value], + 'ev': self._refresh_rate[_ConfigParams.EVENTS_REFRESH_RATE.value], + 'te': self._refresh_rate[_ConfigParams.TELEMETRY_REFRESH_RATE.value]}, + 'uO': {'s': self._url_override[_ApiURLs.SDK_URL.value], + 'e': self._url_override[_ApiURLs.EVENTS_URL.value], + 'a': self._url_override[_ApiURLs.AUTH_URL.value], + 'st': self._url_override[_ApiURLs.STREAMING_URL.value], + 't': self._url_override[_ApiURLs.TELEMETRY_URL.value]}, 'iQ': self._impressions_queue_size, 'eQ': self._events_queue_size, 'iM': self._impressions_mode, @@ -981,11 +981,11 @@ def _get_refresh_rates(self, config): """ with self._lock: return { - ConfigParams.SPLITS_REFRESH_RATE.value: config[ConfigParams.SPLITS_REFRESH_RATE.value], - ConfigParams.SEGMENTS_REFRESH_RATE.value: config[ConfigParams.SEGMENTS_REFRESH_RATE.value], - ConfigParams.IMPRESSIONS_REFRESH_RATE.value: config[ConfigParams.IMPRESSIONS_REFRESH_RATE.value], - ConfigParams.EVENTS_REFRESH_RATE.value: config[ConfigParams.EVENTS_REFRESH_RATE.value], - ConfigParams.TELEMETRY_REFRESH_RATE.value: config[ConfigParams.TELEMETRY_REFRESH_RATE.value] + _ConfigParams.SPLITS_REFRESH_RATE.value: config[_ConfigParams.SPLITS_REFRESH_RATE.value], + _ConfigParams.SEGMENTS_REFRESH_RATE.value: config[_ConfigParams.SEGMENTS_REFRESH_RATE.value], + _ConfigParams.IMPRESSIONS_REFRESH_RATE.value: config[_ConfigParams.IMPRESSIONS_REFRESH_RATE.value], + _ConfigParams.EVENTS_REFRESH_RATE.value: config[_ConfigParams.EVENTS_REFRESH_RATE.value], + _ConfigParams.TELEMETRY_REFRESH_RATE.value: config[_ConfigParams.TELEMETRY_REFRESH_RATE.value] } def _get_url_overrides(self, config): @@ -1000,11 +1000,11 @@ def _get_url_overrides(self, config): """ with self._lock: return { - ApiURLs.SDK_URL.value: True if ApiURLs.SDK_URL.value in config else False, - ApiURLs.EVENTS_URL.value: True if ApiURLs.EVENTS_URL.value in config else False, - ApiURLs.AUTH_URL.value: True if ApiURLs.AUTH_URL.value in config else False, - ApiURLs.STREAMING_URL.value: True if ApiURLs.STREAMING_URL.value in config else False, - ApiURLs.TELEMETRY_URL.value: True if ApiURLs.TELEMETRY_URL.value in config else False + _ApiURLs.SDK_URL.value: True if _ApiURLs.SDK_URL.value in config else False, + _ApiURLs.EVENTS_URL.value: True if _ApiURLs.EVENTS_URL.value in config else False, + _ApiURLs.AUTH_URL.value: True if _ApiURLs.AUTH_URL.value in config else False, + _ApiURLs.STREAMING_URL.value: True if _ApiURLs.STREAMING_URL.value in config else False, + _ApiURLs.TELEMETRY_URL.value: True if _ApiURLs.TELEMETRY_URL.value in config else False } def _get_impressions_mode(self, imp_mode): @@ -1034,6 +1034,6 @@ def _check_if_proxy_detected(self): """ with self._lock: for x in os.environ: - if x.upper() == ExtraConfig.HTTPS_PROXY_ENV.value: + if x.upper() == _ExtraConfig.HTTPS_PROXY_ENV.value: return True return False \ No newline at end of file From aa2a2fe2edea9878d0c4672fa248cfe62cafbaf9 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 15 Sep 2023 10:47:22 -0700 Subject: [PATCH 59/77] added character length for regex check --- splitio/client/input_validator.py | 10 +++++----- tests/client/test_input_validator.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 0a65f310..06b8b9c1 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -80,7 +80,7 @@ def _check_string_not_empty(value, name, operation): return True -def _check_string_matches(value, operation, pattern, name): +def _check_string_matches(value, operation, pattern, name, length): """ Check if value is adhere to a regular expression passed. @@ -98,9 +98,9 @@ def _check_string_matches(value, operation, pattern, name): '%s: you passed %s, event_type must ' + 'adhere to the regular expression %s. ' + 'This means %s must be alphanumeric, cannot be more ' + - 'than 80 characters long, and can only include a dash, underscore, ' + + 'than %s characters long, and can only include a dash, underscore, ' + 'period, or colon as separators of alphanumeric characters.', - operation, value, pattern, name + operation, value, pattern, name, length ) return False return True @@ -323,7 +323,7 @@ def validate_event_type(event_type): if (not _check_not_null(event_type, 'event_type', 'track')) or \ (not _check_is_string(event_type, 'event_type', 'track')) or \ (not _check_string_not_empty(event_type, 'event_type', 'track')) or \ - (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN, 'an event name')): + (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN, 'an event name', 80)): return None return event_type @@ -591,7 +591,7 @@ def validate_flag_sets(flag_sets, method_name): flag_set = _remove_empty_spaces(flag_set, 'flag set', method_name) flag_set = _convert_str_to_lower(flag_set, 'flag set', method_name) - if not _check_string_matches(flag_set, method_name, _FLAG_SETS_REGEX, 'a flag set'): + if not _check_string_matches(flag_set, method_name, _FLAG_SETS_REGEX, 'a flag set', 50): continue sanitized_flag_sets.add(flag_set) diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index abe3f5c4..8323625e 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -666,10 +666,10 @@ def test_track(self, mocker): assert _logger.error.mock_calls == [ mocker.call("%s: you passed %s, event_type must adhere to the regular " "expression %s. This means " - "%s must be alphanumeric, cannot be more than 80 " + "%s must be alphanumeric, cannot be more than %s " "characters long, and can only include a dash, underscore, " "period, or colon as separators of alphanumeric characters.", - 'track', '@@', '^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$', 'an event name') + 'track', '@@', '^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$', 'an event name', 80) ] _logger.reset_mock() From 7c6b1ebc1667a39d606e1bacecfc558d0fc872b3 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 15 Sep 2023 13:12:19 -0700 Subject: [PATCH 60/77] moved flagset and flagset filter classes to storage --- splitio/models/flag_sets.py | 124 ------------------------- splitio/storage/__init__.py | 40 ++++++++ splitio/storage/inmemmory.py | 85 ++++++++++++++++- splitio/storage/pluggable.py | 2 +- splitio/storage/redis.py | 2 +- tests/models/test_flag_sets.py | 59 ------------ tests/storage/test_flag_sets.py | 18 ++++ tests/storage/test_inmemory_storage.py | 17 +--- tests/storage/test_pluggable.py | 2 +- tests/storage/test_redis.py | 2 +- tests/util/test_storage_helper.py | 2 +- 11 files changed, 149 insertions(+), 204 deletions(-) delete mode 100644 splitio/models/flag_sets.py delete mode 100644 tests/models/test_flag_sets.py create mode 100644 tests/storage/test_flag_sets.py diff --git a/splitio/models/flag_sets.py b/splitio/models/flag_sets.py deleted file mode 100644 index a01de740..00000000 --- a/splitio/models/flag_sets.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Flagsets classes.""" -import threading - -class FlagSetsFilter(object): - """Config Flagsets Filter storage.""" - - def __init__(self, flag_sets=[]): - """Constructor.""" - self.flag_sets = set(flag_sets) - self.should_filter = any(flag_sets) - - def set_exist(self, flag_set): - """ - Check if a flagset exist in flagset filter - - :param flag_set: set name - :type flag_set: str - - :rtype: bool - """ - if not self.should_filter: - return True - if not isinstance(flag_set, str) or flag_set == '': - return False - - return any(self.flag_sets.intersection(set([flag_set]))) - - def intersect(self, flag_sets): - """ - Check if a set exist in config flagset filter - - :param flag_set: set of flagsets - :type flag_set: set - - :rtype: bool - """ - if not self.should_filter: - return True - if not isinstance(flag_sets, set) or len(flag_sets) == 0: - return False - return any(self.flag_sets.intersection(flag_sets)) - - -class FlagSets(object): - """InMemory Flagsets storage.""" - - def __init__(self, flag_sets=[]): - """Constructor.""" - self._lock = threading.RLock() - self.sets_feature_flag_map = {} - for flag_set in flag_sets: - self.sets_feature_flag_map[flag_set] = set() - - def flag_set_exist(self, flag_set): - """ - Check if a flagset exist in stored flagset - - :param flag_set: set name - :type flag_set: str - - :rtype: bool - """ - with self._lock: - return flag_set in self.sets_feature_flag_map.keys() - - def get_flag_set(self, flag_set): - """ - fetch feature flags stored in a flag set - - :param flag_set: set name - :type flag_set: str - - :rtype: list(str) - """ - with self._lock: - return self.sets_feature_flag_map.get(flag_set) - - def add_flag_set(self, flag_set): - """ - Add new flag set to storage - - :param flag_set: set name - :type flag_set: str - """ - with self._lock: - if not self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set] = set() - - def remove_flag_set(self, flag_set): - """ - Remove existing flag set from storage - - :param flag_set: set name - :type flag_set: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - del self.sets_feature_flag_map[flag_set] - - def add_feature_flag_to_flag_set(self, flag_set, feature_flag): - """ - Add a feature flag to existing flag set - - :param flag_set: set name - :type flag_set: str - :param feature_flag: feature flag name - :type feature_flag: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set].add(feature_flag) - - def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): - """ - Remove a feature flag from existing flag set - - :param flag_set: set name - :type flag_set: str - :param feature_flag: feature flag name - :type feature_flag: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set].remove(feature_flag) diff --git a/splitio/storage/__init__.py b/splitio/storage/__init__.py index 4930a95e..bf64d980 100644 --- a/splitio/storage/__init__.py +++ b/splitio/storage/__init__.py @@ -1,5 +1,6 @@ """Base storage interfaces.""" import abc +import threading class SplitStorage(object, metaclass=abc.ABCMeta): @@ -315,3 +316,42 @@ def record_bur_time_out(self): """ pass + +class FlagSetsFilter(object): + """Config Flagsets Filter storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self.flag_sets = set(flag_sets) + self.should_filter = any(flag_sets) + + def set_exist(self, flag_set): + """ + Check if a flagset exist in flagset filter + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + if not self.should_filter: + return True + if not isinstance(flag_set, str) or flag_set == '': + return False + + return any(self.flag_sets.intersection(set([flag_set]))) + + def intersect(self, flag_sets): + """ + Check if a set exist in config flagset filter + + :param flag_set: set of flagsets + :type flag_set: set + + :rtype: bool + """ + if not self.should_filter: + return True + if not isinstance(flag_sets, set) or len(flag_sets) == 0: + return False + return any(self.flag_sets.intersection(flag_sets)) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index acbae771..d9b6b7ed 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -6,7 +6,7 @@ from splitio.models.segments import Segment from splitio.models.telemetry import HTTPErrors, HTTPLatencies, MethodExceptions, MethodLatencies, LastSynchronization, StreamingEvents, TelemetryConfig, TelemetryCounters, CounterConstants -from splitio.models.flag_sets import FlagSets, FlagSetsFilter +from splitio.storage import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage MAX_SIZE_BYTES = 5 * 1024 * 1024 @@ -14,6 +14,89 @@ _LOGGER = logging.getLogger(__name__) +class FlagSets(object): + """InMemory Flagsets storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self._lock = threading.RLock() + self.sets_feature_flag_map = {} + for flag_set in flag_sets: + self.sets_feature_flag_map[flag_set] = set() + + def flag_set_exist(self, flag_set): + """ + Check if a flagset exist in stored flagset + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + with self._lock: + return flag_set in self.sets_feature_flag_map.keys() + + def get_flag_set(self, flag_set): + """ + fetch feature flags stored in a flag set + + :param flag_set: set name + :type flag_set: str + + :rtype: list(str) + """ + with self._lock: + return self.sets_feature_flag_map.get(flag_set) + + def add_flag_set(self, flag_set): + """ + Add new flag set to storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if not self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set] = set() + + def remove_flag_set(self, flag_set): + """ + Remove existing flag set from storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + del self.sets_feature_flag_map[flag_set] + + def add_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Add a feature flag to existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].add(feature_flag) + + def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Remove a feature flag from existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].remove(feature_flag) + + class InMemorySplitStorage(SplitStorage): """InMemory implementation of a feature flag storage.""" diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 4ed7c9e9..6e8d0ae2 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -7,7 +7,7 @@ from splitio.models import splits, segments from splitio.models.impressions import Impression from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MAX_TAGS, get_latency_bucket_index -from splitio.models.flag_sets import FlagSetsFilter +from splitio.storage import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage from splitio.util.storage_helper import get_valid_flag_sets, combine_valid_flag_sets diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 6cadc212..a2a5c157 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -6,7 +6,7 @@ from splitio.models.impressions import Impression from splitio.models import splits, segments from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, get_latency_bucket_index -from splitio.models.flag_sets import FlagSetsFilter +from splitio.storage import FlagSetsFilter from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, \ ImpressionPipelinedStorage, TelemetryStorage from splitio.storage.adapters.redis import RedisAdapterException diff --git a/tests/models/test_flag_sets.py b/tests/models/test_flag_sets.py deleted file mode 100644 index fddff1c6..00000000 --- a/tests/models/test_flag_sets.py +++ /dev/null @@ -1,59 +0,0 @@ -from splitio.models.flag_sets import FlagSets, FlagSetsFilter - -class FlagSetsFilterTests(object): - """Flag sets filter storage tests.""" - def test_without_initial_set(self): - flag_set = FlagSets() - assert flag_set.sets_feature_flag_map == {} - - flag_set.add_flag_set('set1') - assert flag_set.get_flag_set('set1') == set({}) - assert flag_set.flag_set_exist('set1') == True - assert flag_set.flag_set_exist('set2') == False - - flag_set.add_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split1'} - flag_set.add_feature_flag_to_flag_set('set1', 'split2') - assert flag_set.get_flag_set('set1') == {'split1', 'split2'} - flag_set.remove_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split2'} - flag_set.remove_flag_set('set2') - assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} - flag_set.remove_flag_set('set1') - assert flag_set.sets_feature_flag_map == {} - assert flag_set.flag_set_exist('set1') == False - - def test_with_initial_set(self): - flag_set = FlagSets(['set1', 'set2']) - assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} - - flag_set.add_flag_set('set1') - assert flag_set.get_flag_set('set1') == set({}) - assert flag_set.flag_set_exist('set1') == True - assert flag_set.flag_set_exist('set2') == True - - flag_set.add_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split1'} - flag_set.add_feature_flag_to_flag_set('set1', 'split2') - assert flag_set.get_flag_set('set1') == {'split1', 'split2'} - flag_set.remove_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split2'} - flag_set.remove_flag_set('set2') - assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} - flag_set.remove_flag_set('set1') - assert flag_set.sets_feature_flag_map == {} - assert flag_set.flag_set_exist('set1') == False - - def test_flag_set_filter(self): - flag_set_filter = FlagSetsFilter() - assert flag_set_filter.flag_sets == set() - assert not flag_set_filter.should_filter - - flag_set_filter = FlagSetsFilter(['set1', 'set2']) - assert flag_set_filter.flag_sets == set({'set1', 'set2'}) - assert flag_set_filter.should_filter - assert flag_set_filter.intersect(set({'set1', 'set2'})) - assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) - assert not flag_set_filter.intersect(set({'set4'})) - assert not flag_set_filter.set_exist('set4') - assert flag_set_filter.set_exist('set1') diff --git a/tests/storage/test_flag_sets.py b/tests/storage/test_flag_sets.py new file mode 100644 index 00000000..d723d19a --- /dev/null +++ b/tests/storage/test_flag_sets.py @@ -0,0 +1,18 @@ +from splitio.storage import FlagSetsFilter + +class FlagSetsFilterTests(object): + """Flag sets filter storage tests.""" + + def test_flag_set_filter(self): + flag_set_filter = FlagSetsFilter() + assert flag_set_filter.flag_sets == set() + assert not flag_set_filter.should_filter + + flag_set_filter = FlagSetsFilter(['set1', 'set2']) + assert flag_set_filter.flag_sets == set({'set1', 'set2'}) + assert flag_set_filter.should_filter + assert flag_set_filter.intersect(set({'set1', 'set2'})) + assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) + assert not flag_set_filter.intersect(set({'set4'})) + assert not flag_set_filter.set_exist('set4') + assert flag_set_filter.set_exist('set1') diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index a4816329..f758a536 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -8,9 +8,10 @@ from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper import splitio.models.telemetry as ModelTelemetry +from splitio.storage import FlagSetsFilter from splitio.engine.telemetry import TelemetryStorageProducer from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ - InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, FlagSets, FlagSetsFilter + InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, FlagSets class FlagSetsFilterTests(object): @@ -57,20 +58,6 @@ def test_with_initial_set(self): assert flag_set.sets_feature_flag_map == {} assert flag_set.flag_set_exist('set1') == False - def test_flag_set_filter(self): - flag_set_filter = FlagSetsFilter() - assert flag_set_filter.flag_sets == set() - assert not flag_set_filter.should_filter - - flag_set_filter = FlagSetsFilter(['set1', 'set2']) - assert flag_set_filter.flag_sets == set({'set1', 'set2'}) - assert flag_set_filter.should_filter - assert flag_set_filter.intersect(set({'set1', 'set2'})) - assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) - assert not flag_set_filter.intersect(set({'set4'})) - assert not flag_set_filter.set_exist('set4') - assert flag_set_filter.set_exist('set1') - class InMemorySplitStorageTests(object): """In memory split storage test cases.""" diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index aa381791..ba18e205 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -8,7 +8,7 @@ from splitio.models.segments import Segment from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper -from splitio.models.flag_sets import FlagSetsFilter +from splitio.storage import FlagSetsFilter from splitio.storage.pluggable import PluggableSplitStorage, PluggableSegmentStorage, PluggableImpressionsStorage, PluggableEventsStorage, PluggableTelemetryStorage from splitio.client.util import get_metadata, SdkMetadata from splitio.models.telemetry import MAX_TAGS, MethodExceptionsAndLatencies, OperationMode diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 0413ca8b..125071c2 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -14,7 +14,7 @@ from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MethodExceptionsAndLatencies -from splitio.models.flag_sets import FlagSetsFilter +from splitio.storage import FlagSetsFilter class RedisSplitStorageTests(object): """Redis split storage test cases.""" diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index 97fb2fd7..b60750fe 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -4,7 +4,7 @@ from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets from splitio.storage.inmemmory import InMemorySplitStorage from splitio.models import splits -from splitio.models.flag_sets import FlagSetsFilter +from splitio.storage import FlagSetsFilter from tests.sync.test_splits_synchronizer import splits_raw as split_sample class StorageHelperTests(object): From d7833651f930cc24d575cff6d8406e127f32a94f Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 15 Sep 2023 14:11:05 -0700 Subject: [PATCH 61/77] polishing --- splitio/models/flag_sets.py | 124 -------------------------------- splitio/storage/inmemmory.py | 83 +++++++++++++++++++++ tests/models/test_flag_sets.py | 59 --------------- tests/storage/test_flag_sets.py | 42 +++++++++++ 4 files changed, 125 insertions(+), 183 deletions(-) delete mode 100644 splitio/models/flag_sets.py delete mode 100644 tests/models/test_flag_sets.py diff --git a/splitio/models/flag_sets.py b/splitio/models/flag_sets.py deleted file mode 100644 index a01de740..00000000 --- a/splitio/models/flag_sets.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Flagsets classes.""" -import threading - -class FlagSetsFilter(object): - """Config Flagsets Filter storage.""" - - def __init__(self, flag_sets=[]): - """Constructor.""" - self.flag_sets = set(flag_sets) - self.should_filter = any(flag_sets) - - def set_exist(self, flag_set): - """ - Check if a flagset exist in flagset filter - - :param flag_set: set name - :type flag_set: str - - :rtype: bool - """ - if not self.should_filter: - return True - if not isinstance(flag_set, str) or flag_set == '': - return False - - return any(self.flag_sets.intersection(set([flag_set]))) - - def intersect(self, flag_sets): - """ - Check if a set exist in config flagset filter - - :param flag_set: set of flagsets - :type flag_set: set - - :rtype: bool - """ - if not self.should_filter: - return True - if not isinstance(flag_sets, set) or len(flag_sets) == 0: - return False - return any(self.flag_sets.intersection(flag_sets)) - - -class FlagSets(object): - """InMemory Flagsets storage.""" - - def __init__(self, flag_sets=[]): - """Constructor.""" - self._lock = threading.RLock() - self.sets_feature_flag_map = {} - for flag_set in flag_sets: - self.sets_feature_flag_map[flag_set] = set() - - def flag_set_exist(self, flag_set): - """ - Check if a flagset exist in stored flagset - - :param flag_set: set name - :type flag_set: str - - :rtype: bool - """ - with self._lock: - return flag_set in self.sets_feature_flag_map.keys() - - def get_flag_set(self, flag_set): - """ - fetch feature flags stored in a flag set - - :param flag_set: set name - :type flag_set: str - - :rtype: list(str) - """ - with self._lock: - return self.sets_feature_flag_map.get(flag_set) - - def add_flag_set(self, flag_set): - """ - Add new flag set to storage - - :param flag_set: set name - :type flag_set: str - """ - with self._lock: - if not self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set] = set() - - def remove_flag_set(self, flag_set): - """ - Remove existing flag set from storage - - :param flag_set: set name - :type flag_set: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - del self.sets_feature_flag_map[flag_set] - - def add_feature_flag_to_flag_set(self, flag_set, feature_flag): - """ - Add a feature flag to existing flag set - - :param flag_set: set name - :type flag_set: str - :param feature_flag: feature flag name - :type feature_flag: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set].add(feature_flag) - - def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): - """ - Remove a feature flag from existing flag set - - :param flag_set: set name - :type flag_set: str - :param feature_flag: feature flag name - :type feature_flag: str - """ - with self._lock: - if self.flag_set_exist(flag_set): - self.sets_feature_flag_map[flag_set].remove(feature_flag) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 68fbe0d5..d9b6b7ed 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -14,6 +14,89 @@ _LOGGER = logging.getLogger(__name__) +class FlagSets(object): + """InMemory Flagsets storage.""" + + def __init__(self, flag_sets=[]): + """Constructor.""" + self._lock = threading.RLock() + self.sets_feature_flag_map = {} + for flag_set in flag_sets: + self.sets_feature_flag_map[flag_set] = set() + + def flag_set_exist(self, flag_set): + """ + Check if a flagset exist in stored flagset + + :param flag_set: set name + :type flag_set: str + + :rtype: bool + """ + with self._lock: + return flag_set in self.sets_feature_flag_map.keys() + + def get_flag_set(self, flag_set): + """ + fetch feature flags stored in a flag set + + :param flag_set: set name + :type flag_set: str + + :rtype: list(str) + """ + with self._lock: + return self.sets_feature_flag_map.get(flag_set) + + def add_flag_set(self, flag_set): + """ + Add new flag set to storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if not self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set] = set() + + def remove_flag_set(self, flag_set): + """ + Remove existing flag set from storage + + :param flag_set: set name + :type flag_set: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + del self.sets_feature_flag_map[flag_set] + + def add_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Add a feature flag to existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].add(feature_flag) + + def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): + """ + Remove a feature flag from existing flag set + + :param flag_set: set name + :type flag_set: str + :param feature_flag: feature flag name + :type feature_flag: str + """ + with self._lock: + if self.flag_set_exist(flag_set): + self.sets_feature_flag_map[flag_set].remove(feature_flag) + + class InMemorySplitStorage(SplitStorage): """InMemory implementation of a feature flag storage.""" diff --git a/tests/models/test_flag_sets.py b/tests/models/test_flag_sets.py deleted file mode 100644 index fddff1c6..00000000 --- a/tests/models/test_flag_sets.py +++ /dev/null @@ -1,59 +0,0 @@ -from splitio.models.flag_sets import FlagSets, FlagSetsFilter - -class FlagSetsFilterTests(object): - """Flag sets filter storage tests.""" - def test_without_initial_set(self): - flag_set = FlagSets() - assert flag_set.sets_feature_flag_map == {} - - flag_set.add_flag_set('set1') - assert flag_set.get_flag_set('set1') == set({}) - assert flag_set.flag_set_exist('set1') == True - assert flag_set.flag_set_exist('set2') == False - - flag_set.add_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split1'} - flag_set.add_feature_flag_to_flag_set('set1', 'split2') - assert flag_set.get_flag_set('set1') == {'split1', 'split2'} - flag_set.remove_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split2'} - flag_set.remove_flag_set('set2') - assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} - flag_set.remove_flag_set('set1') - assert flag_set.sets_feature_flag_map == {} - assert flag_set.flag_set_exist('set1') == False - - def test_with_initial_set(self): - flag_set = FlagSets(['set1', 'set2']) - assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} - - flag_set.add_flag_set('set1') - assert flag_set.get_flag_set('set1') == set({}) - assert flag_set.flag_set_exist('set1') == True - assert flag_set.flag_set_exist('set2') == True - - flag_set.add_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split1'} - flag_set.add_feature_flag_to_flag_set('set1', 'split2') - assert flag_set.get_flag_set('set1') == {'split1', 'split2'} - flag_set.remove_feature_flag_to_flag_set('set1', 'split1') - assert flag_set.get_flag_set('set1') == {'split2'} - flag_set.remove_flag_set('set2') - assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} - flag_set.remove_flag_set('set1') - assert flag_set.sets_feature_flag_map == {} - assert flag_set.flag_set_exist('set1') == False - - def test_flag_set_filter(self): - flag_set_filter = FlagSetsFilter() - assert flag_set_filter.flag_sets == set() - assert not flag_set_filter.should_filter - - flag_set_filter = FlagSetsFilter(['set1', 'set2']) - assert flag_set_filter.flag_sets == set({'set1', 'set2'}) - assert flag_set_filter.should_filter - assert flag_set_filter.intersect(set({'set1', 'set2'})) - assert flag_set_filter.intersect(set({'set1', 'set2', 'set5'})) - assert not flag_set_filter.intersect(set({'set4'})) - assert not flag_set_filter.set_exist('set4') - assert flag_set_filter.set_exist('set1') diff --git a/tests/storage/test_flag_sets.py b/tests/storage/test_flag_sets.py index d723d19a..e03f21a2 100644 --- a/tests/storage/test_flag_sets.py +++ b/tests/storage/test_flag_sets.py @@ -1,7 +1,49 @@ from splitio.storage import FlagSetsFilter +from splitio.storage.inmemmory import FlagSets class FlagSetsFilterTests(object): """Flag sets filter storage tests.""" + def test_without_initial_set(self): + flag_set = FlagSets() + assert flag_set.sets_feature_flag_map == {} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == False + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False + + def test_with_initial_set(self): + flag_set = FlagSets(['set1', 'set2']) + assert flag_set.sets_feature_flag_map == {'set1': set(), 'set2': set()} + + flag_set.add_flag_set('set1') + assert flag_set.get_flag_set('set1') == set({}) + assert flag_set.flag_set_exist('set1') == True + assert flag_set.flag_set_exist('set2') == True + + flag_set.add_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split1'} + flag_set.add_feature_flag_to_flag_set('set1', 'split2') + assert flag_set.get_flag_set('set1') == {'split1', 'split2'} + flag_set.remove_feature_flag_to_flag_set('set1', 'split1') + assert flag_set.get_flag_set('set1') == {'split2'} + flag_set.remove_flag_set('set2') + assert flag_set.sets_feature_flag_map == {'set1': set({'split2'})} + flag_set.remove_flag_set('set1') + assert flag_set.sets_feature_flag_map == {} + assert flag_set.flag_set_exist('set1') == False def test_flag_set_filter(self): flag_set_filter = FlagSetsFilter() From 9f22a208c21d2dc9bbcc8f98d59365eee9de7f80 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 19 Sep 2023 10:56:55 -0700 Subject: [PATCH 62/77] Added flagsets total and invalid count for telemetry config --- splitio/client/factory.py | 34 ++++++++++++++++++++++++-- splitio/engine/telemetry.py | 4 +++ splitio/models/telemetry.py | 22 ++++++++++++++++- splitio/storage/inmemmory.py | 8 ++++++ splitio/storage/pluggable.py | 12 ++++++++- splitio/storage/redis.py | 10 ++++++++ tests/client/test_factory.py | 20 +++++++++++++++ tests/engine/test_telemetry.py | 6 +++-- tests/models/test_telemetry_model.py | 6 ++++- tests/storage/test_inmemory_storage.py | 7 ++++-- tests/storage/test_pluggable.py | 4 ++- tests/storage/test_redis.py | 4 ++- 12 files changed, 126 insertions(+), 11 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 86f74fe9..1a69a193 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -69,6 +69,9 @@ _INSTANTIATED_FACTORIES_LOCK = threading.RLock() _MIN_DEFAULT_DATA_SAMPLING_ALLOWED = 0.1 # 10% _MAX_RETRY_SYNC_ALL = 3 +_FLAG_SETS_LOCK = threading.RLock() +_TOTAL_FLAG_SETS = 0 +_INVALID_FLAG_SETS = 0 class Status(Enum): @@ -417,6 +420,9 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl ) telemetry_init_producer.record_config(cfg, extra_cfg) + total_flag_sets, invalid_flag_sets = _get_total_and_invalid_flag_sets() + telemetry_init_producer.record_flag_sets(total_flag_sets) + telemetry_init_producer.record_invalid_flag_sets(invalid_flag_sets) if preforked_initialization: synchronizer.sync_all(max_retry_attempts=_MAX_RETRY_SYNC_ALL) @@ -508,7 +514,10 @@ def _build_redis_factory(api_key, cfg): telemetry_init_producer=telemetry_init_producer ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() + total_flag_sets, invalid_flag_sets = _get_total_and_invalid_flag_sets() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) + storages['telemetry'].record_flag_sets(total_flag_sets) + storages['telemetry'].record_invalid_flag_sets(invalid_flag_sets) telemetry_submitter.synchronize_config() return split_factory @@ -586,7 +595,10 @@ def _build_pluggable_factory(api_key, cfg): telemetry_init_producer=telemetry_init_producer ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() + total_flag_sets, invalid_flag_sets = _get_total_and_invalid_flag_sets() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) + storages['telemetry'].record_flag_sets(total_flag_sets) + storages['telemetry'].record_invalid_flag_sets(invalid_flag_sets) telemetry_submitter.synchronize_config() return split_factory @@ -684,7 +696,16 @@ def get_factory(api_key, **kwargs): _INSTANTIATED_FACTORIES.update([api_key]) _INSTANTIATED_FACTORIES_LOCK.release() - config = sanitize_config(api_key, kwargs.get('config', {})) + config_raw = kwargs.get('config', {}) + if config_raw.get('flagSetsFilter') is not None and isinstance(config_raw.get('flagSetsFilter'), list): + global _TOTAL_FLAG_SETS + global _INVALID_FLAG_SETS + _FLAG_SETS_LOCK.acquire() + _TOTAL_FLAG_SETS = len(config_raw.get('flagSetsFilter')) + _INVALID_FLAG_SETS = _TOTAL_FLAG_SETS - len(input_validator.validate_flag_sets(config_raw.get('flagSetsFilter'), 'Telemetry Init')) + _FLAG_SETS_LOCK.release() + + config = sanitize_config(api_key, config_raw) if config['operationMode'] == 'localhost': split_factory = _build_localhost_factory(config) @@ -712,4 +733,13 @@ def _get_active_and_redundant_count(): redundant_factory_count += _INSTANTIATED_FACTORIES[item] - 1 active_factory_count += _INSTANTIATED_FACTORIES[item] _INSTANTIATED_FACTORIES_LOCK.release() - return redundant_factory_count, active_factory_count \ No newline at end of file + return redundant_factory_count, active_factory_count + +def _get_total_and_invalid_flag_sets(): + total_flag_sets = 0 + invalid_flag_sets = 0 + _FLAG_SETS_LOCK.acquire() + total_flag_sets = _TOTAL_FLAG_SETS + invalid_flag_sets = _INVALID_FLAG_SETS + _FLAG_SETS_LOCK.release() + return total_flag_sets, invalid_flag_sets diff --git a/splitio/engine/telemetry.py b/splitio/engine/telemetry.py index afb8cf2d..7471bc47 100644 --- a/splitio/engine/telemetry.py +++ b/splitio/engine/telemetry.py @@ -52,6 +52,10 @@ def record_flag_sets(self, flag_sets): """Record flag sets.""" self._telemetry_storage.record_flag_sets(flag_sets) + def record_invalid_flag_sets(self, flag_sets): + """Record invalid flag sets.""" + self._telemetry_storage.record_invalid_flag_sets(flag_sets) + def record_bur_time_out(self): """Record block until ready timeout.""" self._telemetry_storage.record_bur_time_out() diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index 7b19a747..ff43ace3 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -794,6 +794,7 @@ def _reset_all(self): self._active_factory_count = 0 self._redundant_factory_count = 0 self._flag_sets = 0 + self._flag_sets_invalid = 0 def record_config(self, config, extra_config): """ @@ -844,6 +845,16 @@ def record_flag_sets(self, flag_sets): with self._lock: self._flag_sets = flag_sets + def record_invalid_flag_sets(self, flag_sets): + """ + Record invalid flag sets + + :param flag_sets: flag sets count + :type flag_sets: int + """ + with self._lock: + self._flag_sets_invalid = flag_sets + def record_ready_time(self, ready_time): """ Record ready time. @@ -878,6 +889,14 @@ def get_flag_sets(self): with self._lock: return self._flag_sets + def get_invalid_flag_sets(self): + """ + Get invalid flag sets + + """ + with self._lock: + return self._flag_sets_invalid + def get_bur_time_outs(self): """ Get block until ready timeout. @@ -930,7 +949,8 @@ def get_stats(self): 'hp': self._http_proxy, 'aF': self._active_factory_count, 'rF': self._redundant_factory_count, - 'fS': self._flag_sets + 'fsT': self._flag_sets, + 'fsI': self._flag_sets_invalid } def _get_operation_mode(self, op_mode): diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index d9b6b7ed..e9291577 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -658,6 +658,10 @@ def record_flag_sets(self, flag_sets): """Record flag sets.""" self._tel_config.record_flag_sets(flag_sets) + def record_invalid_flag_sets(self, flag_sets): + """Record invalid flag sets.""" + self._tel_config.record_invalid_flag_sets(flag_sets) + def add_tag(self, tag): """Record tag string.""" with self._lock: @@ -730,6 +734,10 @@ def get_flag_sets(self): """Get flag sets.""" self._tel_config.get_flag_sets() + def get_invalid_flag_sets(self): + """Get invalid flag sets.""" + self._tel_config.get_invalid_flag_sets() + def get_bur_time_outs(self): """Get block until ready timeout.""" return self._tel_config.get_bur_time_outs() diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index de59499e..257b9e1c 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -806,6 +806,14 @@ def record_config(self, config, extra_config): """ self._tel_config.record_config(config, extra_config) + def record_flag_sets(self, flag_sets): + """Record flag sets.""" + self._tel_config.record_flag_sets(flag_sets) + + def record_invalid_flag_sets(self, flag_sets): + """Record invalid flag sets.""" + self._tel_config.record_invalid_flag_sets(flag_sets) + def pop_config_tags(self): """Get and reset configs.""" with self._lock: @@ -825,7 +833,9 @@ def _format_config_stats(self): 'rF': config_stats['rF'], 'sT': config_stats['sT'], 'oM': config_stats['oM'], - 't': self.pop_config_tags() + 't': self.pop_config_tags(), + 'fsT': self._tel_config.get_flag_sets(), + 'fsI': self._tel_config.get_invalid_flag_sets() }) def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index c4f7544e..92b3f16f 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -671,6 +671,14 @@ def record_config(self, config, extra_config): """ self._tel_config.record_config(config, extra_config) + def record_flag_sets(self, flag_sets): + """Record flag sets.""" + self._tel_config.record_flag_sets(flag_sets) + + def record_invalid_flag_sets(self, flag_sets): + """Record invalid flag sets.""" + self._tel_config.record_invalid_flag_sets(flag_sets) + def pop_config_tags(self): """Get and reset tags.""" with self._lock: @@ -692,6 +700,8 @@ def _format_config_stats(self): 'rF': config_stats['rF'], 'sT': config_stats['sT'], 'oM': config_stats['oM'], + 'fsT': self._tel_config.get_flag_sets(), + 'fsI': self._tel_config.get_invalid_flag_sets(), 't': self.pop_config_tags() }) diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index cc778f1b..644fe6fd 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -29,6 +29,26 @@ class SplitFactoryTests(object): """Split factory test cases.""" + def test_flag_sets_counts(self): + factory = get_factory("none", config={ + 'flagSetsFilter': ['set1', 'set2', 'set3'] + }) + + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 0 + + factory = get_factory("none", config={ + 'flagSetsFilter': ['s#et1', 'set2', 'set3'] + }) + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 1 + + factory = get_factory("none", config={ + 'flagSetsFilter': ['s#et1', 22, 'set3'] + }) + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 + assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 2 + def test_inmemory_client_creation_streaming_false(self, mocker): """Test that a client with in-memory storage is created correctly.""" diff --git a/tests/engine/test_telemetry.py b/tests/engine/test_telemetry.py index 79bcd744..5cc4b022 100644 --- a/tests/engine/test_telemetry.py +++ b/tests/engine/test_telemetry.py @@ -37,7 +37,8 @@ def test_record_config(self, mocker): } telemetry_init_producer.record_config(config, {}) telemetry_init_producer.record_active_and_redundant_factories(1, 0) - telemetry_init_producer.record_flag_sets(2) + telemetry_init_producer.record_flag_sets(5) + telemetry_init_producer.record_invalid_flag_sets(2) assert(telemetry_storage._tel_config.get_stats() == {'oM': 0, 'sT': telemetry_storage._tel_config._get_storage_type(config['operationMode'], config['storageType']), @@ -54,7 +55,8 @@ def test_record_config(self, mocker): 'nR': 0, 'aF': 1, 'rF': 0, - 'fS': 2} + 'fsT': 5, + 'fsI': 2} ) def test_record_ready_time(self, mocker): diff --git a/tests/models/test_telemetry_model.py b/tests/models/test_telemetry_model.py index d5dda172..beb2598a 100644 --- a/tests/models/test_telemetry_model.py +++ b/tests/models/test_telemetry_model.py @@ -316,7 +316,8 @@ def test_telemetry_config(self): 'bT': 0, 'aF': 0, 'rF': 0, - 'fS': 0} + 'fsT': 0, + 'fsI': 0} ) telemetry_config.record_ready_time(10) @@ -331,6 +332,9 @@ def test_telemetry_config(self): [telemetry_config.record_not_ready_usage() for i in range(5)] assert(telemetry_config.get_non_ready_usage() == 5) + telemetry_config.record_invalid_flag_sets(2) + assert(telemetry_config._flag_sets_invalid == 2) + os.environ["https_proxy"] = "some_host_ip" assert(telemetry_config._check_if_proxy_detected() == True) diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index f758a536..9344dd3f 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -617,7 +617,8 @@ def test_resets(self): 'hp': None, 'aF': 0, 'rF': 0, - 'fS': 0 + 'fsT': 0, + 'fsI': 0 }) assert(storage._streaming_events.pop_streaming_events() == {'streamingEvents': []}) assert(storage._tags == []) @@ -643,6 +644,7 @@ def test_record_config(self): storage.record_config(config, {}) storage.record_active_and_redundant_factories(1, 0) storage.record_flag_sets(2) + storage.record_invalid_flag_sets(1) assert(storage._tel_config.get_stats() == {'oM': 0, 'sT': storage._tel_config._get_storage_type(config['operationMode'], config['storageType']), 'sE': config['streamingEnabled'], @@ -658,7 +660,8 @@ def test_record_config(self): 'nR': 0, 'aF': 1, 'rF': 0, - 'fS': 2} + 'fsT': 2, + 'fsI': 1} ) def test_record_counters(self): diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index ba18e205..ace92762 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -767,5 +767,7 @@ def test_push_config_stats(self): }, {} ) pluggable_telemetry_storage.record_active_and_redundant_factories(2, 1) + pluggable_telemetry_storage.record_flag_sets(3) + pluggable_telemetry_storage.record_invalid_flag_sets(1) pluggable_telemetry_storage.push_config_stats() - assert(self.mock_adapter._keys[pluggable_telemetry_storage._telemetry_config_key + "::" + pluggable_telemetry_storage._sdk_metadata] == '{"aF": 2, "rF": 1, "sT": "memory", "oM": 0, "t": []}') + assert(self.mock_adapter._keys[pluggable_telemetry_storage._telemetry_config_key + "::" + pluggable_telemetry_storage._sdk_metadata] == '{"aF": 2, "rF": 1, "sT": "memory", "oM": 0, "t": [], "fsT": 3, "fsI": 1}') diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 125071c2..8969f5d9 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -432,7 +432,9 @@ def test_format_config_stats(self, mocker): 'rF': stats['rF'], 'sT': stats['sT'], 'oM': stats['oM'], - 't': redis_telemetry.pop_config_tags() + 'fsT': redis_telemetry._tel_config.get_flag_sets(), + 'fsI': redis_telemetry._tel_config.get_invalid_flag_sets(), + 't': redis_telemetry.pop_config_tags(), })) def test_record_active_and_redundant_factories(self, mocker): From 065badf9f064a973f206cc526f3bfa0ba7e053ec Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 19 Sep 2023 12:55:57 -0700 Subject: [PATCH 63/77] added property to flagset filter to provide sorted flagsets --- splitio/client/client.py | 4 ++-- splitio/client/input_validator.py | 2 +- splitio/storage/__init__.py | 2 +- splitio/sync/split.py | 2 +- splitio/util/storage_helper.py | 21 --------------------- tests/client/test_input_validator.py | 6 +++--- tests/storage/test_flag_sets.py | 3 +++ tests/sync/test_splits_synchronizer.py | 4 ++++ tests/sync/test_synchronizer.py | 3 +++ tests/tasks/test_split_sync.py | 3 +-- 10 files changed, 19 insertions(+), 31 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index c9a75516..8a638a0b 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -404,9 +404,9 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets, method) + feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets, method.value) if feature_flags_names == []: - _LOGGER.warning("%s: No valid Flag set or no feature flags found for evaluating treatments" % (method)) + _LOGGER.warning("%s: No valid Flag set or no feature flags found for evaluating treatments" % (method.value)) return {} if 'config' in method.value: diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index c754b38b..fa6a0dbc 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -597,4 +597,4 @@ def validate_flag_sets(flag_sets, method_name): sanitized_flag_sets.add(flag_set) - return sorted(list(sanitized_flag_sets)) + return list(sanitized_flag_sets) diff --git a/splitio/storage/__init__.py b/splitio/storage/__init__.py index bf64d980..bb8c2f81 100644 --- a/splitio/storage/__init__.py +++ b/splitio/storage/__init__.py @@ -2,7 +2,6 @@ import abc import threading - class SplitStorage(object, metaclass=abc.ABCMeta): """Split storage interface implemented as an abstract class.""" @@ -324,6 +323,7 @@ def __init__(self, flag_sets=[]): """Constructor.""" self.flag_sets = set(flag_sets) self.should_filter = any(flag_sets) + self.sorted_flag_sets = sorted(flag_sets) def set_exist(self, flag_set): """ diff --git a/splitio/sync/split.py b/splitio/sync/split.py index ff42286b..91143e53 100644 --- a/splitio/sync/split.py +++ b/splitio/sync/split.py @@ -123,7 +123,7 @@ def _get_config_sets(self): """ if self._feature_flag_storage.flag_set_filter.flag_sets == set({}): return None - return ','.join(self._feature_flag_storage.flag_set_filter.flag_sets) + return ','.join(self._feature_flag_storage.flag_set_filter.sorted_flag_sets) def synchronize_splits(self, till=None): """ diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index d3f44a23..bd270bc0 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -70,27 +70,6 @@ def combine_valid_flag_sets(result_sets): to_return.update(result_set) return to_return -def _check_flag_sets(feature_flag_storage, feature_flag): - """ - 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 - - :param flag_sets: Flag sets array - :type flag_sets: list(str) - :param config_flag_sets: Config flag sets array - :type config_flag_sets: list(str) - - :return: array of flag sets - :rtype: list(str) - """ - sets_to_fetch = [] - for flag_set in flag_sets: - if not flag_set_filter.set_exist(flag_set) and flag_set_filter.should_filter: - _LOGGER.warning("Flag set %s is not part of the configured flag set list, ignoring the request." % (flag_set)) - continue - sets_to_fetch.append(flag_set) - - return sets_to_fetch - def combine_valid_flag_sets(result_sets): """ Check each flag set in given array of sets, combine all flag sets in one unique set diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index b71af0f6..4bb1e417 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1296,19 +1296,19 @@ def test_validate_pluggable_adapter(self): def test_sanitize_flag_sets(self): """Test sanitization for flag sets.""" flag_sets = input_validator.validate_flag_sets([' set1', 'set2 ', 'set3'], 'm') - assert flag_sets == ['set1', 'set2', 'set3'] + assert sorted(flag_sets) == ['set1', 'set2', 'set3'] flag_sets = input_validator.validate_flag_sets(['1set', '_set2'], 'm') assert flag_sets == ['1set'] flag_sets = input_validator.validate_flag_sets(['Set1', 'SET2'], 'm') - assert flag_sets == ['set1', 'set2'] + assert sorted(flag_sets) == ['set1', 'set2'] flag_sets = input_validator.validate_flag_sets(['se\t1', 's/et2', 's*et3', 's!et4', 'se@t5', 'se#t5', 'se$t5', 'se^t5', 'se%t5', 'se&t5'], 'm') assert flag_sets == [] flag_sets = input_validator.validate_flag_sets(['set4', 'set1', 'set3', 'set1'], 'm') - assert flag_sets == ['set1', 'set3', 'set4'] + assert sorted(flag_sets) == ['set1', 'set3', 'set4'] flag_sets = input_validator.validate_flag_sets(['w' * 50, 's' * 51], 'm') assert flag_sets == ['w' * 50] diff --git a/tests/storage/test_flag_sets.py b/tests/storage/test_flag_sets.py index e03f21a2..f4258bd5 100644 --- a/tests/storage/test_flag_sets.py +++ b/tests/storage/test_flag_sets.py @@ -58,3 +58,6 @@ def test_flag_set_filter(self): assert not flag_set_filter.intersect(set({'set4'})) assert not flag_set_filter.set_exist('set4') assert flag_set_filter.set_exist('set1') + + flag_set_filter = FlagSetsFilter(['set5', 'set2', 'set6', 'set1']) + assert flag_set_filter.sorted_flag_sets == ['set1', 'set2', 'set5', 'set6'] \ No newline at end of file diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index 643fb144..17c88a38 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -74,6 +74,7 @@ def intersect(sets): return True storage.flag_set_filter = flag_set_filter storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] split_synchronizer = SplitSynchronizer(api, storage) @@ -100,6 +101,7 @@ def intersect(sets): return True storage.flag_set_filter = flag_set_filter storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] api = mocker.Mock() def get_changes(*args, **kwargs): @@ -143,6 +145,7 @@ def intersect(sets): return True storage.flag_set_filter = flag_set_filter storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] def change_number_mock(): return 2 @@ -206,6 +209,7 @@ def intersect(sets): storage.flag_set_filter = flag_set_filter storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] split_synchronizer = SplitSynchronizer(api, storage) split_synchronizer._backoff = Backoff(1, 1) diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index c74638a2..592543fd 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -30,6 +30,7 @@ def intersect(sets): return True storage.flag_set_filter = flag_set_filter storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] def run(x, c): raise APIException("something broke") @@ -55,6 +56,7 @@ def intersect(sets): return True storage.flag_set_filter = flag_set_filter storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] def run(x, c): raise APIException("something broke", 414) @@ -182,6 +184,7 @@ def intersect(sets): return True split_storage.flag_set_filter = flag_set_filter split_storage.flag_set_filter.flag_sets = {} + split_storage.flag_set_filter.sorted_flag_sets = [] split_api = mocker.Mock() split_api.fetch_splits.return_value = {'splits': self.splits, 'since': 123, diff --git a/tests/tasks/test_split_sync.py b/tests/tasks/test_split_sync.py index f42daa7e..104bbccc 100644 --- a/tests/tasks/test_split_sync.py +++ b/tests/tasks/test_split_sync.py @@ -33,6 +33,7 @@ def intersect(sets): return True storage.flag_set_filter = flag_set_filter storage.flag_set_filter.flag_sets = {} + storage.flag_set_filter.sorted_flag_sets = [] api = mocker.Mock() splits = [{ @@ -102,8 +103,6 @@ def get_changes(*args, **kwargs): assert api.fetch_splits.mock_calls[0][1][1].cache_control_headers == True assert api.fetch_splits.mock_calls[1][1][0] == 123 assert api.fetch_splits.mock_calls[1][1][1].cache_control_headers == True -# assert mocker.call(-1, fetch_options) in api.fetch_splits.mock_calls -# assert mocker.call(123, fetch_options) in api.fetch_splits.mock_calls inserted_split = storage.update.mock_calls[0][1][0][0] assert isinstance(inserted_split, Split) From f55128cba5baa550b15999fec4c10b51933a01f6 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Fri, 22 Sep 2023 09:33:33 -0700 Subject: [PATCH 64/77] Update version.py --- splitio/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/version.py b/splitio/version.py index e974d2b9..757fe38a 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '9.5.0' +__version__ = '9.6.1-rc1' From 1add90768e71f8efa7363e05a49575fb065b67ae Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 12 Oct 2023 13:21:14 -0700 Subject: [PATCH 65/77] fixed exception when new flagset detected --- splitio/storage/inmemmory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index e9291577..b6b317f5 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -92,6 +92,8 @@ def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): :param feature_flag: feature flag name :type feature_flag: str """ + _LOGGER.debug("remove_feature_flag_to_flag_set") + _LOGGER.debug(flag_set) with self._lock: if self.flag_set_exist(flag_set): self.sets_feature_flag_map[flag_set].remove(feature_flag) @@ -200,7 +202,7 @@ def _remove_from_flag_sets(self, feature_flag): if feature_flag.sets is not None: for flag_set in feature_flag.sets: self.flag_set.remove_feature_flag_to_flag_set(flag_set, feature_flag.name) - if len(self.flag_set.get_flag_set(flag_set)) == 0 and not self.flag_set_filter.should_filter: + if self.is_flag_set_exist(flag_set) and len(self.flag_set.get_flag_set(flag_set)) == 0 and not self.flag_set_filter.should_filter: self.flag_set.remove_flag_set(flag_set) def get_feature_flags_by_sets(self, sets): From 576a0f418885e4279c2762e50794201817b449a8 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 12 Oct 2023 13:27:14 -0700 Subject: [PATCH 66/77] cleanup --- splitio/storage/inmemmory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index b6b317f5..a31cddd4 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -92,8 +92,6 @@ def remove_feature_flag_to_flag_set(self, flag_set, feature_flag): :param feature_flag: feature flag name :type feature_flag: str """ - _LOGGER.debug("remove_feature_flag_to_flag_set") - _LOGGER.debug(flag_set) with self._lock: if self.flag_set_exist(flag_set): self.sets_feature_flag_map[flag_set].remove(feature_flag) From f1ae8b9ffd99a750c6b6ad7b903c99f3aca18cef Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 16 Oct 2023 10:36:44 -0700 Subject: [PATCH 67/77] added warning when not ready --- splitio/client/client.py | 2 ++ tests/client/test_client.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 8a638a0b..45c16677 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -61,6 +61,7 @@ def destroyed(self): def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=None): if not self.ready: + _LOGGER.warning("The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", feature) self._telemetry_init_producer.record_not_ready_usage() return { 'treatment': CONTROL, @@ -214,6 +215,7 @@ def _make_evaluations(self, key, feature_flags, attributes, method_name, metric_ def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, attributes=None): if not self.ready: + _LOGGER.warning("The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", ', '.join([feature for feature in feature_flags])) self._telemetry_init_producer.record_not_ready_usage() return { feature_flag: { diff --git a/tests/client/test_client.py b/tests/client/test_client.py index fcddbf79..8287cc2a 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -83,6 +83,7 @@ def test_get_treatment(self, mocker): assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -159,6 +160,7 @@ def test_get_treatment_with_config(self, mocker): [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -236,6 +238,7 @@ def test_get_treatments(self, mocker): assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -310,6 +313,7 @@ def test_get_treatments_with_config(self, mocker): assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -400,6 +404,7 @@ def evaluate_features(feature_flag_names, matching_key, bucketing_key, attribute assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -465,7 +470,6 @@ def evaluate_features(feature_flag_names, matching_key, bucketing_key, attribute client._evaluator.evaluate_features = evaluate_features _logger = mocker.Mock() client._send_impression_to_listener = mocker.Mock() -# pytest.set_trace() assert client.get_treatments_by_flag_sets('key', ['set1', 'set2']) == {'f1': 'on', 'f2': 'on'} impressions_called = impmanager.process_impressions.mock_calls[0][1][0] @@ -488,6 +492,7 @@ def evaluate_features(feature_flag_names, matching_key, bucketing_key, attribute assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -581,6 +586,7 @@ def evaluate_features(feature_flag_names, matching_key, bucketing_key, attribute assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -677,6 +683,7 @@ def evaluate_features(feature_flag_names, matching_key, bucketing_key, attribute assert mocker.call( [(Impression('some_key', 'some_feature', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY), {'some_attribute': 1})] ) in impmanager.process_impressions.mock_calls + assert _logger.call(["The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", 'some_feature']) # Test with exception: ready_property.return_value = True @@ -748,6 +755,7 @@ def test_track(self, mocker): telemetry_producer.get_telemetry_init_producer(), mocker.Mock() ) + _logger = mocker.Mock() destroyed_mock = mocker.PropertyMock() destroyed_mock.return_value = False @@ -762,6 +770,7 @@ def test_track(self, mocker): size=1024 ) ]) in event_storage.put.mock_calls + assert _logger.call("track: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method") def test_evaluations_before_running_post_fork(self, mocker): destroyed_property = mocker.PropertyMock() From 6636c840b43161154978cec5f60f9f9890902335 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 16 Oct 2023 10:47:18 -0700 Subject: [PATCH 68/77] added default_treatment to split view --- splitio/models/splits.py | 3 ++- tests/models/test_splits.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/splitio/models/splits.py b/splitio/models/splits.py index 5ab32953..0a10dd87 100644 --- a/splitio/models/splits.py +++ b/splitio/models/splits.py @@ -7,7 +7,7 @@ SplitView = namedtuple( 'SplitView', - ['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'sets'] + ['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'default_treatment', 'sets'] ) @@ -200,6 +200,7 @@ def to_split_view(self): list(set(part.treatment for cond in self.conditions for part in cond.partitions)), self.change_number, self._configurations if self._configurations is not None else {}, + self._default_treatment, list(self._sets) if self._sets is not None else [] ) diff --git a/tests/models/test_splits.py b/tests/models/test_splits.py index d56e6f77..23688d9e 100644 --- a/tests/models/test_splits.py +++ b/tests/models/test_splits.py @@ -117,4 +117,5 @@ def test_to_split_view(self): assert as_split_view.killed == self.raw['killed'] assert as_split_view.traffic_type == self.raw['trafficTypeName'] assert set(as_split_view.treatments) == set(['on', 'off']) + assert as_split_view.default_treatment == self.raw['defaultTreatment'] assert sorted(as_split_view.sets) == sorted(list(self.raw['sets'])) From c6c0f11239b13c520556300b89398a1eddad5c86 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 17 Oct 2023 09:11:47 -0700 Subject: [PATCH 69/77] polishing --- splitio/client/client.py | 12 ++++++------ tests/client/test_client.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 45c16677..35030595 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -59,9 +59,9 @@ def destroyed(self): """Return whether the factory holding this client has been destroyed.""" return self._factory.destroyed - def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=None): + def _evaluate_if_ready(self, matching_key, bucketing_key, feature, method, attributes=None): if not self.ready: - _LOGGER.warning("The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", feature) + _LOGGER.warning("%s: The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", method, feature) self._telemetry_init_producer.record_not_ready_usage() return { 'treatment': CONTROL, @@ -103,7 +103,7 @@ def _make_evaluation(self, key, feature_flag, attributes, method_name, metric_na or not input_validator.validate_attributes(attributes, method_name): return CONTROL, None - result = self._evaluate_if_ready(matching_key, bucketing_key, feature_flag, attributes) + result = self._evaluate_if_ready(matching_key, bucketing_key, feature_flag, method_name, attributes) impression = self._build_impression( matching_key, @@ -168,7 +168,7 @@ def _make_evaluations(self, key, feature_flags, attributes, method_name, metric_ try: evaluations = self._evaluate_features_if_ready(matching_key, bucketing_key, - list(feature_flags), attributes) + list(feature_flags), method_name, attributes) for feature_flag in feature_flags: try: @@ -213,9 +213,9 @@ def _make_evaluations(self, key, feature_flags, attributes, method_name, metric_ _LOGGER.debug('Error: ', exc_info=True) return input_validator.generate_control_treatments(list(feature_flags), method_name) - def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, attributes=None): + def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, method, attributes=None): if not self.ready: - _LOGGER.warning("The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", ', '.join([feature for feature in feature_flags])) + _LOGGER.warning("%s: The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", method, ', '.join([feature for feature in feature_flags])) self._telemetry_init_producer.record_not_ready_usage() return { feature_flag: { diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 8287cc2a..6341142c 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -846,7 +846,7 @@ def test_telemetry_not_ready(self, mocker): ) client = Client(factory, mocker.Mock()) client.ready = False - client._evaluate_if_ready('matching_key','matching_key', 'feature') + client._evaluate_if_ready('matching_key','matching_key', 'method', 'feature') assert(telemetry_storage._tel_config._not_ready == 1) client.track('key', 'tt', 'ev') assert(telemetry_storage._tel_config._not_ready == 2) From 878b0a6da3882dd329d3cbbb4dcbf410ab8f23db Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 30 Oct 2023 13:55:32 -0700 Subject: [PATCH 70/77] Updated redis storage to reflect flagset filter --- splitio/storage/redis.py | 19 +++++++++++++-- tests/storage/test_redis.py | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 92b3f16f..97e9122d 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -89,7 +89,14 @@ def get(self, feature_flag_name): # pylint: disable=method-hidden raw = self._redis.get(self._get_key(feature_flag_name)) _LOGGER.debug("Fetchting Feature flag [%s] from redis" % feature_flag_name) _LOGGER.debug(raw) - return splits.from_raw(json.loads(raw)) if raw is not None else None + if raw is None: + return None + + feature_flag = splits.from_raw(json.loads(raw)) + if self.flag_set_filter.intersect(feature_flag.sets): + return feature_flag + + return None except RedisAdapterException: _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) @@ -206,6 +213,9 @@ def get_split_names(self): :return: List of feature flag names. :rtype: list(str) """ + if self.flag_set_filter.should_filter: + return self.get_feature_flags_by_sets(self.flag_set_filter.flag_sets) + try: keys = self._redis.keys(self._get_key('*')) _LOGGER.debug("Fetchting feature flag names from redis: %s" % keys) @@ -229,7 +239,12 @@ def get_all_splits(self): :return: List of all feature flags in cache. :rtype: list(splitio.models.splits.Split) """ - keys = self._redis.keys(self._get_key('*')) + if self.flag_set_filter.should_filter: + keys = self.get_feature_flags_by_sets(self.flag_set_filter.flag_sets) + else: + keys = self._redis.keys(self._get_key('*')) + if keys == []: + return [] to_return = [] try: _LOGGER.debug("Fetchting all feature flags from redis: %s" % keys) diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 8969f5d9..1a6d1058 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -15,6 +15,8 @@ from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MethodExceptionsAndLatencies from splitio.storage import FlagSetsFilter +from tests.integration import splits_json + class RedisSplitStorageTests(object): """Redis split storage test cases.""" @@ -187,6 +189,52 @@ def test_flag_sets(self, mocker): storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) assert storage2.flag_set_filter.flag_sets == set({'set2', 'set3'}) + def test_fetching_split_with_flag_set(self, mocker): + """Test retrieving a split works.""" + adapter = mocker.Mock(spec=RedisAdapter) + adapter.get.return_value = json.dumps(splits_json["splitChange1_1"]["splits"][0]) + adapter.keys.return_value = ['SPLIT_1', 'SPLIT_2'] + + def mget(keys): + if keys == ['SPLIT_2']: + return [json.dumps(splits_json["splitChange1_1"]["splits"][0])] + if keys == ['SPLIT_2', 'SPLIT_1']: + return [json.dumps(splits_json["splitChange1_1"]["splits"][0]), json.dumps(splits_json["splitChange1_1"]["splits"][1])] + adapter.mget = mget + + storage = RedisSplitStorage(adapter, config_flag_sets=['set_1']) + + def get_feature_flags_by_sets(flag_sets): + if flag_sets=={'set_1'}: + return [] + if flag_sets=={'set2'}: + return ['SPLIT_2'] + if flag_sets=={'set2', 'set1'}: + return ['SPLIT_2', 'SPLIT_1'] + storage.get_feature_flags_by_sets = get_feature_flags_by_sets + + assert storage.get('SPLIT_2') == None + assert storage.get_split_names() == [] + assert storage.get_all_splits() == [] + + storage = RedisSplitStorage(adapter, config_flag_sets=['set2']) + storage.get_feature_flags_by_sets = get_feature_flags_by_sets + assert storage.get('SPLIT_2').name == 'SPLIT_2' + assert storage.get_split_names() == ['SPLIT_2'] + splits = storage.get_all_splits() + assert splits[0].name == 'SPLIT_2' + assert len(splits) == 1 + + storage = RedisSplitStorage(adapter, config_flag_sets=['set2', 'set1']) + storage.get_feature_flags_by_sets = get_feature_flags_by_sets + assert storage.get('SPLIT_2').name == 'SPLIT_2' + assert storage.get_split_names() == ['SPLIT_2', 'SPLIT_1'] + splits = storage.get_all_splits() + assert splits[0].name == 'SPLIT_2' + assert splits[1].name == 'SPLIT_1' + assert len(splits) == 2 + + class RedisSegmentStorageTests(object): """Redis segment storage test cases.""" From 6994602d3d3b36a10869fc499a8540c33aece3ea Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 31 Oct 2023 11:26:35 -0700 Subject: [PATCH 71/77] removed raising exception at posting config data --- splitio/api/telemetry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/splitio/api/telemetry.py b/splitio/api/telemetry.py index 4c182a4e..722bb75d 100644 --- a/splitio/api/telemetry.py +++ b/splitio/api/telemetry.py @@ -76,7 +76,6 @@ def record_init(self, configs): 'Error posting init config because an exception was raised by the HTTPClient' ) _LOGGER.debug('Error: ', exc_info=True) - raise APIException('Init config data not flushed properly.') from exc def record_stats(self, stats): """ From 7e316c9d6d54618c4a92a10aeb30b652817217ad Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:29:29 -0700 Subject: [PATCH 72/77] Revert "Updated redis storage to reflect flagset filter" --- splitio/api/telemetry.py | 1 + splitio/storage/redis.py | 19 ++------------- tests/storage/test_redis.py | 48 ------------------------------------- 3 files changed, 3 insertions(+), 65 deletions(-) diff --git a/splitio/api/telemetry.py b/splitio/api/telemetry.py index 722bb75d..4c182a4e 100644 --- a/splitio/api/telemetry.py +++ b/splitio/api/telemetry.py @@ -76,6 +76,7 @@ def record_init(self, configs): 'Error posting init config because an exception was raised by the HTTPClient' ) _LOGGER.debug('Error: ', exc_info=True) + raise APIException('Init config data not flushed properly.') from exc def record_stats(self, stats): """ diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 97e9122d..92b3f16f 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -89,14 +89,7 @@ def get(self, feature_flag_name): # pylint: disable=method-hidden raw = self._redis.get(self._get_key(feature_flag_name)) _LOGGER.debug("Fetchting Feature flag [%s] from redis" % feature_flag_name) _LOGGER.debug(raw) - if raw is None: - return None - - feature_flag = splits.from_raw(json.loads(raw)) - if self.flag_set_filter.intersect(feature_flag.sets): - return feature_flag - - return None + return splits.from_raw(json.loads(raw)) if raw is not None else None except RedisAdapterException: _LOGGER.error('Error fetching feature flag from storage') _LOGGER.debug('Error: ', exc_info=True) @@ -213,9 +206,6 @@ def get_split_names(self): :return: List of feature flag names. :rtype: list(str) """ - if self.flag_set_filter.should_filter: - return self.get_feature_flags_by_sets(self.flag_set_filter.flag_sets) - try: keys = self._redis.keys(self._get_key('*')) _LOGGER.debug("Fetchting feature flag names from redis: %s" % keys) @@ -239,12 +229,7 @@ def get_all_splits(self): :return: List of all feature flags in cache. :rtype: list(splitio.models.splits.Split) """ - if self.flag_set_filter.should_filter: - keys = self.get_feature_flags_by_sets(self.flag_set_filter.flag_sets) - else: - keys = self._redis.keys(self._get_key('*')) - if keys == []: - return [] + keys = self._redis.keys(self._get_key('*')) to_return = [] try: _LOGGER.debug("Fetchting all feature flags from redis: %s" % keys) diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 1a6d1058..8969f5d9 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -15,8 +15,6 @@ from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import MethodExceptions, MethodLatencies, TelemetryConfig, MethodExceptionsAndLatencies from splitio.storage import FlagSetsFilter -from tests.integration import splits_json - class RedisSplitStorageTests(object): """Redis split storage test cases.""" @@ -189,52 +187,6 @@ def test_flag_sets(self, mocker): storage2 = RedisSplitStorage(adapter, True, 1, ['set2', 'set3']) assert storage2.flag_set_filter.flag_sets == set({'set2', 'set3'}) - def test_fetching_split_with_flag_set(self, mocker): - """Test retrieving a split works.""" - adapter = mocker.Mock(spec=RedisAdapter) - adapter.get.return_value = json.dumps(splits_json["splitChange1_1"]["splits"][0]) - adapter.keys.return_value = ['SPLIT_1', 'SPLIT_2'] - - def mget(keys): - if keys == ['SPLIT_2']: - return [json.dumps(splits_json["splitChange1_1"]["splits"][0])] - if keys == ['SPLIT_2', 'SPLIT_1']: - return [json.dumps(splits_json["splitChange1_1"]["splits"][0]), json.dumps(splits_json["splitChange1_1"]["splits"][1])] - adapter.mget = mget - - storage = RedisSplitStorage(adapter, config_flag_sets=['set_1']) - - def get_feature_flags_by_sets(flag_sets): - if flag_sets=={'set_1'}: - return [] - if flag_sets=={'set2'}: - return ['SPLIT_2'] - if flag_sets=={'set2', 'set1'}: - return ['SPLIT_2', 'SPLIT_1'] - storage.get_feature_flags_by_sets = get_feature_flags_by_sets - - assert storage.get('SPLIT_2') == None - assert storage.get_split_names() == [] - assert storage.get_all_splits() == [] - - storage = RedisSplitStorage(adapter, config_flag_sets=['set2']) - storage.get_feature_flags_by_sets = get_feature_flags_by_sets - assert storage.get('SPLIT_2').name == 'SPLIT_2' - assert storage.get_split_names() == ['SPLIT_2'] - splits = storage.get_all_splits() - assert splits[0].name == 'SPLIT_2' - assert len(splits) == 1 - - storage = RedisSplitStorage(adapter, config_flag_sets=['set2', 'set1']) - storage.get_feature_flags_by_sets = get_feature_flags_by_sets - assert storage.get('SPLIT_2').name == 'SPLIT_2' - assert storage.get_split_names() == ['SPLIT_2', 'SPLIT_1'] - splits = storage.get_all_splits() - assert splits[0].name == 'SPLIT_2' - assert splits[1].name == 'SPLIT_1' - assert len(splits) == 2 - - class RedisSegmentStorageTests(object): """Redis segment storage test cases.""" From e6119de090cb343d6f0819ce144977a29d249294 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 1 Nov 2023 12:03:28 -0700 Subject: [PATCH 73/77] 1- Added flagset filter check with consumer mode 2- Updated changes.txt 3- Removed exception when telemetry post config fails --- CHANGES.txt | 10 ++++++++++ splitio/api/telemetry.py | 1 - splitio/client/config.py | 6 +++++- tests/client/test_config.py | 12 ++++++++---- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6ea03dfc..1a128006 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,13 @@ +9.6.1 (Nov 3, 2023) +- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. + - getTreatmentsByFlagSet and getTreatmentsByFlagSets + - getTreatmentWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets +- Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. + - Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init. +- Updated the following SDK manager methods to expose flag sets on flag views. +- Removed raising an exception when Telemetry post config data fails, SDK will only log the error. + 9.5.1 (Sep 5, 2023) - Exclude tests from when building the package - Fixed exception when fetching telemetry stats if no SSE Feature flags update events are stored diff --git a/splitio/api/telemetry.py b/splitio/api/telemetry.py index 4c182a4e..722bb75d 100644 --- a/splitio/api/telemetry.py +++ b/splitio/api/telemetry.py @@ -76,7 +76,6 @@ def record_init(self, configs): 'Error posting init config because an exception was raised by the HTTPClient' ) _LOGGER.debug('Error: ', exc_info=True) - raise APIException('Init config data not flushed properly.') from exc def record_stats(self, stats): """ diff --git a/splitio/client/config.py b/splitio/client/config.py index 429861b8..437df62e 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -143,6 +143,10 @@ def sanitize(sdk_key, config): _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.') processed['metricsRefreshRate'] = 3600 - processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None + if config['operationMode'] == 'consumer' and config.get('flagSetsFilter') is not None: + processed['flagSetsFilter'] = None + _LOGGER.warning('config: FlagSets filter is not applicable for Consumer modes where the SDK does keep rollout data in sync. FlagSet filter was discarded.') + else: + processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None return processed diff --git a/tests/client/test_config.py b/tests/client/test_config.py index ebd10c71..b4b9d9e9 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -65,8 +65,12 @@ def test_sanitize_imp_mode(self): def test_sanitize(self): """Test sanitization.""" - configs = {} - processed = config.sanitize('some', configs) - + processed = config.sanitize('some', {}) assert processed['redisLocalCacheEnabled'] # check default is True - assert processed['flagSetsFilter'] is None \ No newline at end of file + assert processed['flagSetsFilter'] is None + + processed = config.sanitize('some', {'redisHost': 'x', 'flagSetsFilter': ['set']}) + assert processed['flagSetsFilter'] is None + + processed = config.sanitize('some', {'storageType': 'pluggable', 'flagSetsFilter': ['set']}) + assert processed['flagSetsFilter'] is None From 55f39a051d7614625fd159be08d5faef899bd961 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 1 Nov 2023 13:15:03 -0700 Subject: [PATCH 74/77] cleanup --- CHANGES.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1a128006..5e464588 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,8 +1,8 @@ 9.6.1 (Nov 3, 2023) - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. - - getTreatmentsByFlagSet and getTreatmentsByFlagSets - - getTreatmentWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets + - get_treatments_by_flag_set and get_treatments_by_flag_sets + - get_treatments_with_config_by_flag_set and get_treatments_with_config_by_flag_sets - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. - Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init. - Updated the following SDK manager methods to expose flag sets on flag views. From e70a4407e8a2064e6c15fab7ad0fddf78fdca32c Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 1 Nov 2023 14:12:49 -0700 Subject: [PATCH 75/77] fixed version --- splitio/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/version.py b/splitio/version.py index c02fe413..17781f45 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '9.6.1' +__version__ = '9.6.0' From 2a1df384626d708f582378cd06a46b73a539ea60 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 1 Nov 2023 14:14:10 -0700 Subject: [PATCH 76/77] fixed version in changes --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5e464588..eee840fd 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -9.6.1 (Nov 3, 2023) +9.6.0 (Nov 3, 2023) - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. - get_treatments_by_flag_set and get_treatments_by_flag_sets From ed3dae4fbaa85aa0e16bf59b73f2eafb8448f43e Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 1 Nov 2023 15:22:53 -0700 Subject: [PATCH 77/77] polishing --- splitio/client/config.py | 1 - splitio/client/factory.py | 44 +++++++------------------- splitio/engine/telemetry.py | 4 +-- splitio/models/telemetry.py | 40 ++--------------------- splitio/storage/__init__.py | 1 - splitio/storage/inmemmory.py | 20 ++---------- splitio/storage/pluggable.py | 16 ++-------- splitio/storage/redis.py | 14 ++------ splitio/util/storage_helper.py | 18 +---------- tests/engine/test_telemetry.py | 4 +-- tests/models/test_telemetry_model.py | 12 ++----- tests/storage/test_inmemory_storage.py | 4 +-- tests/storage/test_pluggable.py | 10 +++--- tests/storage/test_redis.py | 4 +-- tests/sync/test_telemetry.py | 2 +- 15 files changed, 36 insertions(+), 158 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index 437df62e..92388edf 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -1,7 +1,6 @@ """Default settings for the Split.IO SDK Python client.""" import os.path import logging -import re from splitio.engine.impressions import ImpressionsMode from splitio.client.input_validator import validate_flag_sets diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 1a69a193..67c57e68 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -69,9 +69,6 @@ _INSTANTIATED_FACTORIES_LOCK = threading.RLock() _MIN_DEFAULT_DATA_SAMPLING_ALLOWED = 0.1 # 10% _MAX_RETRY_SYNC_ALL = 3 -_FLAG_SETS_LOCK = threading.RLock() -_TOTAL_FLAG_SETS = 0 -_INVALID_FLAG_SETS = 0 class Status(Enum): @@ -315,7 +312,8 @@ def _wrap_impression_listener(listener, metadata): def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-locals - auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None): + auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None, + total_flag_sets=0, invalid_flag_sets=0): """Build and return a split factory tailored to the supplied config.""" if not input_validator.validate_factory_instantiation(api_key): return None @@ -419,10 +417,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl telemetry_evaluation_producer ) - telemetry_init_producer.record_config(cfg, extra_cfg) - total_flag_sets, invalid_flag_sets = _get_total_and_invalid_flag_sets() - telemetry_init_producer.record_flag_sets(total_flag_sets) - telemetry_init_producer.record_invalid_flag_sets(invalid_flag_sets) + telemetry_init_producer.record_config(cfg, extra_cfg, total_flag_sets, invalid_flag_sets) if preforked_initialization: synchronizer.sync_all(max_retry_attempts=_MAX_RETRY_SYNC_ALL) @@ -501,7 +496,7 @@ def _build_redis_factory(api_key, cfg): initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() - telemetry_init_producer.record_config(cfg, {}) + telemetry_init_producer.record_config(cfg, {}, 0, 0) split_factory = SplitFactory( api_key, @@ -514,10 +509,7 @@ def _build_redis_factory(api_key, cfg): telemetry_init_producer=telemetry_init_producer ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() - total_flag_sets, invalid_flag_sets = _get_total_and_invalid_flag_sets() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) - storages['telemetry'].record_flag_sets(total_flag_sets) - storages['telemetry'].record_invalid_flag_sets(invalid_flag_sets) telemetry_submitter.synchronize_config() return split_factory @@ -582,7 +574,7 @@ def _build_pluggable_factory(api_key, cfg): initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() - telemetry_init_producer.record_config(cfg, {}) + telemetry_init_producer.record_config(cfg, {}, 0, 0) split_factory = SplitFactory( api_key, @@ -595,10 +587,7 @@ def _build_pluggable_factory(api_key, cfg): telemetry_init_producer=telemetry_init_producer ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() - total_flag_sets, invalid_flag_sets = _get_total_and_invalid_flag_sets() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) - storages['telemetry'].record_flag_sets(total_flag_sets) - storages['telemetry'].record_invalid_flag_sets(invalid_flag_sets) telemetry_submitter.synchronize_config() return split_factory @@ -697,13 +686,11 @@ def get_factory(api_key, **kwargs): _INSTANTIATED_FACTORIES_LOCK.release() config_raw = kwargs.get('config', {}) + total_flag_sets = 0 + invalid_flag_sets = 0 if config_raw.get('flagSetsFilter') is not None and isinstance(config_raw.get('flagSetsFilter'), list): - global _TOTAL_FLAG_SETS - global _INVALID_FLAG_SETS - _FLAG_SETS_LOCK.acquire() - _TOTAL_FLAG_SETS = len(config_raw.get('flagSetsFilter')) - _INVALID_FLAG_SETS = _TOTAL_FLAG_SETS - len(input_validator.validate_flag_sets(config_raw.get('flagSetsFilter'), 'Telemetry Init')) - _FLAG_SETS_LOCK.release() + total_flag_sets = len(config_raw.get('flagSetsFilter')) + invalid_flag_sets = total_flag_sets - len(input_validator.validate_flag_sets(config_raw.get('flagSetsFilter'), 'Telemetry Init')) config = sanitize_config(api_key, config_raw) @@ -721,7 +708,9 @@ def get_factory(api_key, **kwargs): kwargs.get('events_api_base_url'), kwargs.get('auth_api_base_url'), kwargs.get('streaming_api_base_url'), - kwargs.get('telemetry_api_base_url')) + kwargs.get('telemetry_api_base_url'), + total_flag_sets, + invalid_flag_sets) return split_factory @@ -734,12 +723,3 @@ def _get_active_and_redundant_count(): active_factory_count += _INSTANTIATED_FACTORIES[item] _INSTANTIATED_FACTORIES_LOCK.release() return redundant_factory_count, active_factory_count - -def _get_total_and_invalid_flag_sets(): - total_flag_sets = 0 - invalid_flag_sets = 0 - _FLAG_SETS_LOCK.acquire() - total_flag_sets = _TOTAL_FLAG_SETS - invalid_flag_sets = _INVALID_FLAG_SETS - _FLAG_SETS_LOCK.release() - return total_flag_sets, invalid_flag_sets diff --git a/splitio/engine/telemetry.py b/splitio/engine/telemetry.py index 7471bc47..55afa320 100644 --- a/splitio/engine/telemetry.py +++ b/splitio/engine/telemetry.py @@ -36,9 +36,9 @@ def __init__(self, telemetry_storage): """Constructor.""" self._telemetry_storage = telemetry_storage - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets=0, invalid_flag_sets=0): """Record configurations.""" - self._telemetry_storage.record_config(config, extra_config) + self._telemetry_storage.record_config(config, extra_config, total_flag_sets, invalid_flag_sets) current_app, app_worker_id = self._get_app_worker_id() if current_app is not None: self.add_config_tag("initilization:" + current_app) diff --git a/splitio/models/telemetry.py b/splitio/models/telemetry.py index ff43ace3..e1685b3d 100644 --- a/splitio/models/telemetry.py +++ b/splitio/models/telemetry.py @@ -796,7 +796,7 @@ def _reset_all(self): self._flag_sets = 0 self._flag_sets_invalid = 0 - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets, invalid_flag_sets): """ Record configurations. @@ -829,32 +829,14 @@ def record_config(self, config, extra_config): self._impressions_mode = self._get_impressions_mode(config[_ConfigParams.IMPRESSIONS_MODE.value]) self._impression_listener = True if config[_ConfigParams.IMPRESSIONS_LISTENER.value] is not None else False self._http_proxy = self._check_if_proxy_detected() + self._flag_sets = total_flag_sets + self._flag_sets_invalid = invalid_flag_sets def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): with self._lock: self._active_factory_count = active_factory_count self._redundant_factory_count = redundant_factory_count - def record_flag_sets(self, flag_sets): - """ - Record flag sets - - :param flag_sets: flag sets count - :type flag_sets: int - """ - with self._lock: - self._flag_sets = flag_sets - - def record_invalid_flag_sets(self, flag_sets): - """ - Record invalid flag sets - - :param flag_sets: flag sets count - :type flag_sets: int - """ - with self._lock: - self._flag_sets_invalid = flag_sets - def record_ready_time(self, ready_time): """ Record ready time. @@ -881,22 +863,6 @@ def record_not_ready_usage(self): with self._lock: self._not_ready += 1 - def get_flag_sets(self): - """ - Get flag sets - - """ - with self._lock: - return self._flag_sets - - def get_invalid_flag_sets(self): - """ - Get invalid flag sets - - """ - with self._lock: - return self._flag_sets_invalid - def get_bur_time_outs(self): """ Get block until ready timeout. diff --git a/splitio/storage/__init__.py b/splitio/storage/__init__.py index bb8c2f81..76b63070 100644 --- a/splitio/storage/__init__.py +++ b/splitio/storage/__init__.py @@ -1,6 +1,5 @@ """Base storage interfaces.""" import abc -import threading class SplitStorage(object, metaclass=abc.ABCMeta): """Split storage interface implemented as an abstract class.""" diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index a31cddd4..6d74bdad 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -642,9 +642,9 @@ def _reset_config_tags(self): with self._lock: self._config_tags = [] - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets, invalid_flag_sets): """Record configurations.""" - self._tel_config.record_config(config, extra_config) + self._tel_config.record_config(config, extra_config, total_flag_sets, invalid_flag_sets) def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): """Record active and redundant factories.""" @@ -654,14 +654,6 @@ def record_ready_time(self, ready_time): """Record ready time.""" self._tel_config.record_ready_time(ready_time) - def record_flag_sets(self, flag_sets): - """Record flag sets.""" - self._tel_config.record_flag_sets(flag_sets) - - def record_invalid_flag_sets(self, flag_sets): - """Record invalid flag sets.""" - self._tel_config.record_invalid_flag_sets(flag_sets) - def add_tag(self, tag): """Record tag string.""" with self._lock: @@ -730,14 +722,6 @@ def record_update_from_sse(self, event): """Record update from sse.""" self._counters.record_update_from_sse(event) - def get_flag_sets(self): - """Get flag sets.""" - self._tel_config.get_flag_sets() - - def get_invalid_flag_sets(self): - """Get invalid flag sets.""" - self._tel_config.get_invalid_flag_sets() - def get_bur_time_outs(self): """Get block until ready timeout.""" return self._tel_config.get_bur_time_outs() diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 257b9e1c..d1503af3 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -795,7 +795,7 @@ def add_config_tag(self, tag): if len(self._config_tags) < MAX_TAGS: self._config_tags.append(tag) - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets, invalid_flag_sets): """ initilize telemetry objects @@ -804,15 +804,7 @@ def record_config(self, config, extra_config): :param extra_config: any extra configs :type extra_config: Dict """ - self._tel_config.record_config(config, extra_config) - - def record_flag_sets(self, flag_sets): - """Record flag sets.""" - self._tel_config.record_flag_sets(flag_sets) - - def record_invalid_flag_sets(self, flag_sets): - """Record invalid flag sets.""" - self._tel_config.record_invalid_flag_sets(flag_sets) + self._tel_config.record_config(config, extra_config, total_flag_sets, invalid_flag_sets) def pop_config_tags(self): """Get and reset configs.""" @@ -833,9 +825,7 @@ def _format_config_stats(self): 'rF': config_stats['rF'], 'sT': config_stats['sT'], 'oM': config_stats['oM'], - 't': self.pop_config_tags(), - 'fsT': self._tel_config.get_flag_sets(), - 'fsI': self._tel_config.get_invalid_flag_sets() + 't': self.pop_config_tags() }) def record_active_and_redundant_factories(self, active_factory_count, redundant_factory_count): diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 92b3f16f..4e50f643 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -662,22 +662,14 @@ def add_config_tag(self, tag): if len(self._config_tags) < MAX_TAGS: self._config_tags.append(tag) - def record_config(self, config, extra_config): + def record_config(self, config, extra_config, total_flag_sets, invalid_flag_sets): """ initilize telemetry objects :param congif: factory configuration parameters :type config: splitio.client.config """ - self._tel_config.record_config(config, extra_config) - - def record_flag_sets(self, flag_sets): - """Record flag sets.""" - self._tel_config.record_flag_sets(flag_sets) - - def record_invalid_flag_sets(self, flag_sets): - """Record invalid flag sets.""" - self._tel_config.record_invalid_flag_sets(flag_sets) + self._tel_config.record_config(config, extra_config, total_flag_sets, invalid_flag_sets) def pop_config_tags(self): """Get and reset tags.""" @@ -700,8 +692,6 @@ def _format_config_stats(self): 'rF': config_stats['rF'], 'sT': config_stats['sT'], 'oM': config_stats['oM'], - 'fsT': self._tel_config.get_flag_sets(), - 'fsI': self._tel_config.get_invalid_flag_sets(), 't': self.pop_config_tags() }) diff --git a/splitio/util/storage_helper.py b/splitio/util/storage_helper.py index bd270bc0..d281c438 100644 --- a/splitio/util/storage_helper.py +++ b/splitio/util/storage_helper.py @@ -68,20 +68,4 @@ def combine_valid_flag_sets(result_sets): for result_set in result_sets: if isinstance(result_set, set) and len(result_set) > 0: to_return.update(result_set) - return to_return - -def combine_valid_flag_sets(result_sets): - """ - Check each flag set in given array of sets, combine all flag sets in one unique set - - :param result_sets: Flag sets set - :type flag_sets: list(set) - - :return: flag sets set - :rtype: set - """ - to_return = set() - for result_set in result_sets: - if isinstance(result_set, set) and len(result_set) > 0: - to_return.update(result_set) - return to_return + return to_return \ No newline at end of file diff --git a/tests/engine/test_telemetry.py b/tests/engine/test_telemetry.py index 5cc4b022..45b05551 100644 --- a/tests/engine/test_telemetry.py +++ b/tests/engine/test_telemetry.py @@ -35,10 +35,8 @@ def test_record_config(self, mocker): 'metricsRefreshRate': 10, 'storageType': None } - telemetry_init_producer.record_config(config, {}) + telemetry_init_producer.record_config(config, {}, 5, 2) telemetry_init_producer.record_active_and_redundant_factories(1, 0) - telemetry_init_producer.record_flag_sets(5) - telemetry_init_producer.record_invalid_flag_sets(2) assert(telemetry_storage._tel_config.get_stats() == {'oM': 0, 'sT': telemetry_storage._tel_config._get_storage_type(config['operationMode'], config['storageType']), diff --git a/tests/models/test_telemetry_model.py b/tests/models/test_telemetry_model.py index dd46ae80..5ff98d72 100644 --- a/tests/models/test_telemetry_model.py +++ b/tests/models/test_telemetry_model.py @@ -316,7 +316,7 @@ def test_telemetry_config(self): 'storageType': None, 'flagSetsFilter': None } - telemetry_config.record_config(config, {}) + telemetry_config.record_config(config, {}, 5, 2) assert(telemetry_config.get_stats() == {'oM': 0, 'sT': telemetry_config._get_storage_type(config['operationMode'], config['storageType']), 'sE': config['streamingEnabled'], @@ -332,16 +332,13 @@ def test_telemetry_config(self): 'bT': 0, 'aF': 0, 'rF': 0, - 'fsT': 0, - 'fsI': 0} + 'fsT': 5, + 'fsI': 2} ) telemetry_config.record_ready_time(10) assert(telemetry_config._time_until_ready == 10) - telemetry_config.record_flag_sets(5) - assert(telemetry_config._flag_sets == 5) - assert(telemetry_config.get_bur_time_outs() == 0) [telemetry_config.record_bur_time_out() for i in range(2)] assert(telemetry_config.get_bur_time_outs() == 2) @@ -350,9 +347,6 @@ def test_telemetry_config(self): [telemetry_config.record_not_ready_usage() for i in range(5)] assert(telemetry_config.get_non_ready_usage() == 5) - telemetry_config.record_invalid_flag_sets(2) - assert(telemetry_config._flag_sets_invalid == 2) - os.environ["https_proxy"] = "some_host_ip" assert(telemetry_config._check_if_proxy_detected() == True) diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 9344dd3f..2c44bd2d 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -641,10 +641,8 @@ def test_record_config(self): 'metricsRefreshRate': 10, 'storageType': None } - storage.record_config(config, {}) + storage.record_config(config, {}, 2, 1) storage.record_active_and_redundant_factories(1, 0) - storage.record_flag_sets(2) - storage.record_invalid_flag_sets(1) assert(storage._tel_config.get_stats() == {'oM': 0, 'sT': storage._tel_config._get_storage_type(config['operationMode'], config['storageType']), 'sE': config['streamingEnabled'], diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index ace92762..b5772b56 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -673,12 +673,12 @@ def test_record_config(self): pluggable_telemetry_storage = PluggableTelemetryStorage(self.mock_adapter, self.sdk_metadata, prefix=sprefix) self.config = {} self.extra_config = {} - def record_config_mock(config, extra_config): + def record_config_mock(config, extra_config, fs, ifs): self.config = config self.extra_config = extra_config pluggable_telemetry_storage.record_config = record_config_mock - pluggable_telemetry_storage.record_config({'item': 'value'}, {'item2': 'value2'}) + pluggable_telemetry_storage.record_config({'item': 'value'}, {'item2': 'value2'}, 0, 0) assert(self.config == {'item': 'value'}) assert(self.extra_config == {'item2': 'value2'}) @@ -764,10 +764,8 @@ def test_push_config_stats(self): 'eventsPushRate': 60, 'metricsRefreshRate': 10, 'storageType': None - }, {} + }, {}, 0, 0 ) pluggable_telemetry_storage.record_active_and_redundant_factories(2, 1) - pluggable_telemetry_storage.record_flag_sets(3) - pluggable_telemetry_storage.record_invalid_flag_sets(1) pluggable_telemetry_storage.push_config_stats() - assert(self.mock_adapter._keys[pluggable_telemetry_storage._telemetry_config_key + "::" + pluggable_telemetry_storage._sdk_metadata] == '{"aF": 2, "rF": 1, "sT": "memory", "oM": 0, "t": [], "fsT": 3, "fsI": 1}') + assert(self.mock_adapter._keys[pluggable_telemetry_storage._telemetry_config_key + "::" + pluggable_telemetry_storage._sdk_metadata] == '{"aF": 2, "rF": 1, "sT": "memory", "oM": 0, "t": []}') diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 8969f5d9..1c54a8aa 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -413,7 +413,7 @@ def test_init(self, mocker): @mock.patch('splitio.models.telemetry.TelemetryConfig.record_config') def test_record_config(self, mocker): redis_telemetry = RedisTelemetryStorage(mocker.Mock(), mocker.Mock()) - redis_telemetry.record_config(mocker.Mock(), mocker.Mock()) + redis_telemetry.record_config(mocker.Mock(), mocker.Mock(), 0, 0) assert(mocker.called) @mock.patch('splitio.storage.adapters.redis.RedisAdapter.hset') @@ -432,8 +432,6 @@ def test_format_config_stats(self, mocker): 'rF': stats['rF'], 'sT': stats['sT'], 'oM': stats['oM'], - 'fsT': redis_telemetry._tel_config.get_flag_sets(), - 'fsI': redis_telemetry._tel_config.get_invalid_flag_sets(), 't': redis_telemetry.pop_config_tags(), })) diff --git a/tests/sync/test_telemetry.py b/tests/sync/test_telemetry.py index 9d901713..9ce82cc7 100644 --- a/tests/sync/test_telemetry.py +++ b/tests/sync/test_telemetry.py @@ -111,7 +111,7 @@ def test_synchronize_telemetry(self, mocker): 'activeFactoryCount': 1, 'notReady': 0, 'timeUntilReady': 1 - }, {} + }, {}, 0, 0 ) self.formatted_config = "" def record_init(*args, **kwargs):