From ce37aba4d7ee7e952aaa9db74097ab67702343c2 Mon Sep 17 00:00:00 2001 From: Noelia Melina Urruchua Date: Tue, 2 Dec 2025 14:57:25 -0300 Subject: [PATCH 01/26] Replace SonarSource/sonarcloud-github-action --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52cfda4f..df28cd54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,12 +24,12 @@ jobs: - 6379:6379 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v6 with: python-version: '3.7.16' @@ -48,7 +48,7 @@ jobs: - name: SonarQube Scan (Push) if: github.event_name == 'push' - uses: SonarSource/sonarcloud-github-action@v1.9 + uses: SonarSource/sonarqube-scan-action@v6 env: SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -60,7 +60,7 @@ jobs: - name: SonarQube Scan (Pull Request) if: github.event_name == 'pull_request' - uses: SonarSource/sonarcloud-github-action@v1.9 + uses: SonarSource/sonarqube-scan-action@v6 env: SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c65fd014fa7d971e50c1ac6c5da81ddb91aaddc7 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 7 Jan 2026 13:17:53 -0800 Subject: [PATCH 02/26] added models, events config data and events metadata --- splitio/events/__init__.py | 0 splitio/events/events_manager_config.py | 124 +++++++++++++++++++++ splitio/events/events_metadata.py | 46 ++++++++ splitio/models/events.py | 22 +++- tests/events/test_events_manager_config.py | 43 +++++++ tests/events/test_events_metadata.py | 28 +++++ 6 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 splitio/events/__init__.py create mode 100644 splitio/events/events_manager_config.py create mode 100644 splitio/events/events_metadata.py create mode 100644 tests/events/test_events_manager_config.py create mode 100644 tests/events/test_events_metadata.py diff --git a/splitio/events/__init__.py b/splitio/events/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/splitio/events/events_manager_config.py b/splitio/events/events_manager_config.py new file mode 100644 index 00000000..891d17a5 --- /dev/null +++ b/splitio/events/events_manager_config.py @@ -0,0 +1,124 @@ +"""Events Manager Configuration.""" +from splitio.models.events import SdkEvent, SdkInternalEvent + +class EventsManagerConfig(object): + """Events Manager Configurations class.""" + + def __init__(self): + """ + Construct Events Manager Configuration instance. + """ + self._require_all = self._get_require_all() + self._prerequisites = self._get_prerequisites() + self._require_any = self._get_require_any() + self._suppressed_by = self._get_suppressed_by() + self._execution_limits = self._get_execution_limits() + self._evaluation_order = self._get_sorted_events() + + @property + def require_all(self): + """Return require all dict""" + return self._require_all + + @property + def prerequisites(self): + """Return prerequisites dict""" + return self._prerequisites + + @property + def require_any(self): + """Return require_any dict""" + return self._require_any + + @property + def suppressed_by(self): + """Return suppressed_by dict""" + return self._suppressed_by + + @property + def execution_limits(self): + """Return execution_limits dict""" + return self._execution_limits + + @property + def prerequisites(self): + """Return require all dict""" + return self._prerequisites + + @property + def evaluation_order(self): + """Return evaluation_order dict""" + return self._evaluation_order + + @property + def sorted_events(self): + """Return sorted_events dict""" + return self._sorted_events + + def _get_require_all(self): + """Return require all dict""" + return { + SdkEvent.SDK_READY: {SdkInternalEvent.SDK_READY} + } + + def _get_prerequisites(self): + """Return prerequisites dict""" + return { + SdkEvent.SDK_UPDATE: {SdkEvent.SDK_READY} + } + + def _get_require_any(self): + """Return require_any dict""" + return { + SdkEvent.SDK_UPDATE: {SdkInternalEvent.FLAG_KILLED_NOTIFICATION, SdkInternalEvent.FLAGS_UPDATED, + SdkInternalEvent.RB_SEGMENTS_UPDATED, SdkInternalEvent.SEGMENTS_UPDATED}, + SdkEvent.SDK_READY_TIMED_OUT: {SdkInternalEvent.SDK_TIMED_OUT} + } + + def _get_suppressed_by(self): + """Return suppressed_by dict""" + return { + SdkEvent.SDK_READY_TIMED_OUT: {SdkEvent.SDK_READY} + } + + def _get_execution_limits(self): + """Return execution_limits dict""" + return { + SdkEvent.SDK_READY: 1, + SdkEvent.SDK_READY_TIMED_OUT: -1, + SdkEvent.SDK_UPDATE: -1 + } + + def _get_sorted_events(self): + """Return dorted events set""" + sorted_events = [] + for sdk_event in [SdkEvent.SDK_READY, SdkEvent.SDK_READY_TIMED_OUT, SdkEvent.SDK_UPDATE]: + sorted_events = self._dfs_recursive(sdk_event, sorted_events) + + return sorted_events + + + def _dfs_recursive(self, sdk_event, added): + """Return sorted events set based on the dependency rules""" + if sdk_event in added: + return added + + for dependent_event in self._get_dependencies(sdk_event): + added = self._dfs_recursive(dependent_event, added) + + added.append(sdk_event) + return added + + def _get_dependencies(self, sdk_event): + """Return dependencies set from prerequisites and suppressed events for a given event""" + dependencies = set() + for prerequisites_event_name, prerequisites_event_value in self.prerequisites.items(): + if prerequisites_event_name == sdk_event: + for prereq_event in prerequisites_event_value: + dependencies.add(prereq_event) + + for suppressed_event_name, suppressed_event_value in self.suppressed_by.items(): + if sdk_event in suppressed_event_value: + dependencies.add(suppressed_event_name) + + return dependencies diff --git a/splitio/events/events_metadata.py b/splitio/events/events_metadata.py new file mode 100644 index 00000000..3e024b66 --- /dev/null +++ b/splitio/events/events_metadata.py @@ -0,0 +1,46 @@ +"""Events Metadata.""" +from splitio.models.events import SdkEvent, SdkInternalEvent + +class EventsMetadata(object): + """Events Metadata class.""" + + def __init__(self, metadata): + """ + Construct Events Metadata instance. + """ + self._metadata = self._sanitize(metadata) + + def get_data(self): + """Return metadata dict""" + return self._metadata + + def get_keys(self): + """Return metadata dict keys""" + return self._metadata.keys() + + def get_values(self): + """Return metadata dict values""" + return self._metadata.values() + + def contain_key(self, key): + """Return True if key is contained in metadata""" + return key in self._metadata.keys() + + def _sanitize(self, data): + """Return sanitized metadata dict with values either int, bool, str or list """ + santized_data = {} + for item_name, item_value in data.items(): + if self._value_is_valid(item_value): + santized_data[item_name] = item_value + + return santized_data + + def _value_is_valid(self, value): + """Return bool if values is int, bool, str or list[str] """ + if (value is not None) and (isinstance(value, int) or isinstance(value, bool) or isinstance(value, str)): + return True + + if isinstance(value, set): + return any([isinstance(item, str) for item in value]) + + return False \ No newline at end of file diff --git a/splitio/models/events.py b/splitio/models/events.py index b924417b..efcd3ef1 100644 --- a/splitio/models/events.py +++ b/splitio/models/events.py @@ -4,7 +4,7 @@ The dto is implemented as a namedtuple for performance matters. """ from collections import namedtuple - +from enum import Enum Event = namedtuple('Event', [ 'key', @@ -19,3 +19,23 @@ 'event', 'size', ]) + +class SdkEvent(Enum): + """Public SDK events""" + + SDK_READY = 'SDK_READY' + SDK_READY_TIMED_OUT = 'SDK_READY_TIMED_OUT' + SDK_UPDATE = 'SDK_UPDATE' + +class SdkInternalEvent(Enum): + """Internal SDK events""" + + SDK_READY = 'SDK_READY' + SDK_TIMED_OUT = 'SDK_TIMED_OUT' + FLAGS_UPDATED = 'FLAGS_UPDATED' + FLAG_KILLED_NOTIFICATION = 'FLAG_KILLED_NOTIFICATION' + SEGMENTS_UPDATED = 'SEGMENTS_UPDATED' + RB_SEGMENTS_UPDATED = 'RB_SEGMENTS_UPDATED' + LARGE_SEGMENTS_UPDATED = 'LARGE_SEGMENTS_UPDATED' + + diff --git a/tests/events/test_events_manager_config.py b/tests/events/test_events_manager_config.py new file mode 100644 index 00000000..5c9748c0 --- /dev/null +++ b/tests/events/test_events_manager_config.py @@ -0,0 +1,43 @@ +"""EventsManagerConfig test module.""" +import pytest + +from splitio.events.events_manager_config import EventsManagerConfig +from splitio.models.events import SdkEvent, SdkInternalEvent + +class EventsManagerConfigTests(object): + """Tests for EventsManagerConfig.""" + + def test_build_instance(self): + config = EventsManagerConfig() + + assert len(config.require_all[SdkEvent.SDK_READY]) == 1 + assert SdkInternalEvent.SDK_READY in config.require_all[SdkEvent.SDK_READY] + + assert SdkEvent.SDK_READY in config.prerequisites[SdkEvent.SDK_UPDATE] + + assert config.execution_limits[SdkEvent.SDK_READY_TIMED_OUT] == -1 + assert config.execution_limits[SdkEvent.SDK_UPDATE] == -1 + assert config.execution_limits[SdkEvent.SDK_READY] == 1 + + assert len(config.require_any[SdkEvent.SDK_READY_TIMED_OUT]) == 1 + assert SdkInternalEvent.SDK_TIMED_OUT in config.require_any[SdkEvent.SDK_READY_TIMED_OUT] + + assert len(config.require_any[SdkEvent.SDK_UPDATE]) == 4 + assert SdkInternalEvent.FLAG_KILLED_NOTIFICATION in config.require_any[SdkEvent.SDK_UPDATE] + assert SdkInternalEvent.FLAGS_UPDATED in config.require_any[SdkEvent.SDK_UPDATE] + assert SdkInternalEvent.RB_SEGMENTS_UPDATED in config.require_any[SdkEvent.SDK_UPDATE] + assert SdkInternalEvent.SEGMENTS_UPDATED in config.require_any[SdkEvent.SDK_UPDATE] + + assert len(config.suppressed_by[SdkEvent.SDK_READY_TIMED_OUT]) == 1 + assert SdkEvent.SDK_READY in config.suppressed_by[SdkEvent.SDK_READY_TIMED_OUT] + + order = 0 + assert len(config.evaluation_order) == 3 + for sdk_event in config.evaluation_order: + order += 1 + if order == 1: + assert sdk_event == SdkEvent.SDK_READY_TIMED_OUT + if order == 2: + assert sdk_event == SdkEvent.SDK_READY + if order == 3: + assert sdk_event == SdkEvent.SDK_UPDATE \ No newline at end of file diff --git a/tests/events/test_events_metadata.py b/tests/events/test_events_metadata.py new file mode 100644 index 00000000..0d321ca2 --- /dev/null +++ b/tests/events/test_events_metadata.py @@ -0,0 +1,28 @@ +"""EventsMetadata test module.""" +import pytest + +from splitio.events.events_metadata import EventsMetadata +from splitio.models.events import SdkEvent, SdkInternalEvent + +class EventsMetadataTests(object): + """Tests for EventsMetadata.""" + + def test_build_instance(self): + data = { "updatedFlags": { "feature1" }, "sdkTimeout": 10 , "boolValue": True, "strValue": "value" } + metadata = EventsMetadata(data) + + assert len(metadata.get_keys()) == 4 + assert metadata.get_data()["updatedFlags"].pop() == "feature1" + assert len(metadata.get_data()["updatedFlags"]) == 0 + assert metadata.get_data()["sdkTimeout"] == 10 + assert metadata.get_data()["boolValue"] == True + assert metadata.get_data()["strValue"] == "value" + assert metadata.contain_key("updatedFlags") + assert not metadata.contain_key("not_exist") + assert len(metadata.get_values()) == 4 + + def test_sanitize_none_input(self): + data = { "updatedFlags": { "feature1" }, "sdkTimeout": None, "strValue": [1, 2, 3] } + metadata = EventsMetadata(data) + assert len(metadata.get_keys()) == 1 + assert metadata.get_data()["updatedFlags"].pop() == "feature1" \ No newline at end of file From 661d248723872cf5f519fd3e21e09fcd8600aaa5 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 8 Jan 2026 14:46:10 -0800 Subject: [PATCH 03/26] updated metadata to recent spec --- splitio/events/events_metadata.py | 55 +++++++++++----------------- tests/events/test_events_metadata.py | 27 +++++--------- 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/splitio/events/events_metadata.py b/splitio/events/events_metadata.py index 3e024b66..5d6f4961 100644 --- a/splitio/events/events_metadata.py +++ b/splitio/events/events_metadata.py @@ -1,46 +1,35 @@ """Events Metadata.""" -from splitio.models.events import SdkEvent, SdkInternalEvent +from enum import Enum + +class SdkEventType(Enum): + """Public event types""" + + FLAG_UPDATE = 'FLAG_UPDATE' + SEGMENT_UPDATE = 'SEGMENT_UPDATE' class EventsMetadata(object): """Events Metadata class.""" - def __init__(self, metadata): + def __init__(self, type, names): """ Construct Events Metadata instance. """ - self._metadata = self._sanitize(metadata) + self._type = type + self._names = self._sanitize(names) - def get_data(self): - """Return metadata dict""" - return self._metadata + def get_type(self): + """Return type""" + return self._type - def get_keys(self): - """Return metadata dict keys""" - return self._metadata.keys() - - def get_values(self): - """Return metadata dict values""" - return self._metadata.values() - - def contain_key(self, key): - """Return True if key is contained in metadata""" - return key in self._metadata.keys() + def get_names(self): + """Return names""" + return self._names - def _sanitize(self, data): - """Return sanitized metadata dict with values either int, bool, str or list """ - santized_data = {} - for item_name, item_value in data.items(): - if self._value_is_valid(item_value): - santized_data[item_name] = item_value + def _sanitize(self, names): + """Return sanitized names list with values str""" + santized_data = set() + for name in names: + if isinstance(name, str): + santized_data.add(name) return santized_data - - def _value_is_valid(self, value): - """Return bool if values is int, bool, str or list[str] """ - if (value is not None) and (isinstance(value, int) or isinstance(value, bool) or isinstance(value, str)): - return True - - if isinstance(value, set): - return any([isinstance(item, str) for item in value]) - - return False \ No newline at end of file diff --git a/tests/events/test_events_metadata.py b/tests/events/test_events_metadata.py index 0d321ca2..3ce90d0f 100644 --- a/tests/events/test_events_metadata.py +++ b/tests/events/test_events_metadata.py @@ -2,27 +2,20 @@ import pytest from splitio.events.events_metadata import EventsMetadata -from splitio.models.events import SdkEvent, SdkInternalEvent +from splitio.events.events_metadata import SdkEventType class EventsMetadataTests(object): """Tests for EventsMetadata.""" def test_build_instance(self): - data = { "updatedFlags": { "feature1" }, "sdkTimeout": 10 , "boolValue": True, "strValue": "value" } - metadata = EventsMetadata(data) - - assert len(metadata.get_keys()) == 4 - assert metadata.get_data()["updatedFlags"].pop() == "feature1" - assert len(metadata.get_data()["updatedFlags"]) == 0 - assert metadata.get_data()["sdkTimeout"] == 10 - assert metadata.get_data()["boolValue"] == True - assert metadata.get_data()["strValue"] == "value" - assert metadata.contain_key("updatedFlags") - assert not metadata.contain_key("not_exist") - assert len(metadata.get_values()) == 4 + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1" }) + assert len(metadata.get_names()) == 1 + assert metadata.get_names().pop() == "feature1" + assert len(metadata.get_names()) == 0 + assert metadata.get_type() == SdkEventType.FLAG_UPDATE def test_sanitize_none_input(self): - data = { "updatedFlags": { "feature1" }, "sdkTimeout": None, "strValue": [1, 2, 3] } - metadata = EventsMetadata(data) - assert len(metadata.get_keys()) == 1 - assert metadata.get_data()["updatedFlags"].pop() == "feature1" \ No newline at end of file + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1", None, 123, False }) + assert len(metadata.get_names()) == 1 + assert metadata.get_names().pop() == "feature1" + assert len(metadata.get_names()) == 0 From 5d814b57b660776dda2a897e174687cacaa653ca Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 9 Jan 2026 10:34:56 -0800 Subject: [PATCH 04/26] added events manager and events delivery --- splitio/events/__init__.py | 25 +++++ splitio/events/events_delivery.py | 21 ++++ splitio/events/events_manager.py | 152 +++++++++++++++++++++++++++ splitio/events/events_metadata.py | 55 ++++------ tests/events/test_events_delivery.py | 27 +++++ tests/events/test_events_manager.py | 100 ++++++++++++++++++ tests/events/test_events_metadata.py | 27 ++--- 7 files changed, 357 insertions(+), 50 deletions(-) create mode 100644 splitio/events/events_delivery.py create mode 100644 splitio/events/events_manager.py create mode 100644 tests/events/test_events_delivery.py create mode 100644 tests/events/test_events_manager.py diff --git a/splitio/events/__init__.py b/splitio/events/__init__.py index e69de29b..cee5543e 100644 --- a/splitio/events/__init__.py +++ b/splitio/events/__init__.py @@ -0,0 +1,25 @@ +"""Base storage interfaces.""" +import abc + +class EventsManagerInterface(object, metaclass=abc.ABCMeta): + """Events manager interface implemented as an abstract class.""" + + @abc.abstractmethod + def register(self, sdk_event, event_handler): + pass + + @abc.abstractmethod + def unregister(self, sdk_event): + pass + + @abc.abstractmethod + def notify_internal_event(self, sdk_internal_event, event_metadata): + pass + + +class EventsDeliveryInterface(object, metaclass=abc.ABCMeta): + """Events Delivery interface.""" + + @abc.abstractmethod + def deliver(self, sdk_event, event_metadata, event_handler): + pass \ No newline at end of file diff --git a/splitio/events/events_delivery.py b/splitio/events/events_delivery.py new file mode 100644 index 00000000..129c14dc --- /dev/null +++ b/splitio/events/events_delivery.py @@ -0,0 +1,21 @@ +"""Events Manager.""" +import logging + +from splitio.events import EventsDeliveryInterface + +_LOGGER = logging.getLogger(__name__) + +class EventsDelivery(EventsDeliveryInterface): + """Events Manager class.""" + + def __init__(self): + """ + Construct Events Manager instance. + """ + + def deliver(self, sdk_event, event_metadata, event_handler): + try: + event_handler(event_metadata) + except Exception as ex: + _LOGGER.error("Exception when calling handler for Sdk Event %s", sdk_event) + _LOGGER.error(ex) diff --git a/splitio/events/events_manager.py b/splitio/events/events_manager.py new file mode 100644 index 00000000..077b2370 --- /dev/null +++ b/splitio/events/events_manager.py @@ -0,0 +1,152 @@ +"""Events Manager.""" +import threading +import logging +from collections import namedtuple +import pytest + +from splitio.events import EventsManagerInterface + +_LOGGER = logging.getLogger(__name__) + +ValidSdkEvent = namedtuple('ValidSdkEvent', ['sdk_event', 'valid']) +ActiveSubscriptions = namedtuple('ActiveSubscriptions', ['triggered', 'handler']) + +class EventsManager(EventsManagerInterface): + """Events Manager class.""" + + def __init__(self, events_configurations, events_delivery): + """ + Construct Events Manager instance. + """ + self._active_subscriptions = {} + self._internal_events_status = {} + self._events_delivery = events_delivery + self._manager_config = events_configurations + self._lock = threading.RLock() + + def register(self, sdk_event, event_handler): + if self._active_subscriptions.get(sdk_event) != None: + return + + with self._lock: + self._active_subscriptions[sdk_event] = ActiveSubscriptions(False, event_handler) + + def unregister(self, sdk_event): + if self._active_subscriptions.get(sdk_event) == None: + return + + with self._lock: + del self._active_subscriptions[sdk_event] + + def notify_internal_event(self, sdk_internal_event, event_metadata): + with self._lock: + for sorted_event in self._manager_config.evaluation_order: + if sorted_event in self._get_sdk_event_if_applicable(sdk_internal_event): + _LOGGER.debug("EventsManager: Firing Sdk event %s", sorted_event) + if self._get_event_handler(sorted_event) != None: + notify_event = threading.Thread(target=self._events_delivery.deliver, args=[sorted_event, event_metadata, self._get_event_handler(sorted_event)], + name='SplitSDKEventNotify', daemon=True) + notify_event.start() + self._set_sdk_event_triggered(sorted_event) + + def _event_already_triggered(self, sdk_event): + if self._active_subscriptions.get(sdk_event) != None: + return self._active_subscriptions.get(sdk_event).triggered + + return False + + def _get_internal_event_status(self, sdk_internal_event): + if self._internal_events_status.get(sdk_internal_event) != None: + return self._internal_events_status[sdk_internal_event] + + return False + + def _update_internal_event_status(self, sdk_internal_event, status): + with self._lock: + self._internal_events_status[sdk_internal_event] = status + + def _set_sdk_event_triggered(self, sdk_event): + if self._active_subscriptions.get(sdk_event) == None: + return + + if self._active_subscriptions.get(sdk_event).triggered == True: + return + + self._active_subscriptions[sdk_event] = self._active_subscriptions[sdk_event]._replace(triggered = True) + + def _get_event_handler(self, sdk_event): + if self._active_subscriptions.get(sdk_event) == None: + return None + + return self._active_subscriptions.get(sdk_event).handler + + def _get_sdk_event_if_applicable(self, sdk_internal_event): + final_sdk_event = ValidSdkEvent(None, False) + self._update_internal_event_status(sdk_internal_event, True) + + events_to_fire = [] + require_any_sdk_event = self._check_require_any(sdk_internal_event) + if require_any_sdk_event.valid: + if (not self._set_sdk_event_triggered(require_any_sdk_event.sdk_event) and + self._execution_limit(require_any_sdk_event.sdk_event) == 1) or \ + self._execution_limit(require_any_sdk_event.sdk_event) == -1: + final_sdk_event = final_sdk_event._replace(sdk_event = require_any_sdk_event.sdk_event, + valid = self._check_prerequisites(require_any_sdk_event.sdk_event) and \ + self._check_suppressed_by(require_any_sdk_event.sdk_event)) + + if final_sdk_event.valid: + events_to_fire.append(final_sdk_event.sdk_event) + + [events_to_fire.append(sdk_event) for sdk_event in self._check_require_all()] + + return events_to_fire + + def _check_require_all(self): + events = [] + for require_name, require_value in self._manager_config.require_all.items(): + final_status = True + for val in require_value: + final_status &= self._get_internal_event_status(val) + + if final_status and \ + self._check_prerequisites(require_name) and \ + ((not self._event_already_triggered(require_name) and + self._execution_limit(require_name) == 1) or \ + self._execution_limit(require_name) == -1) and \ + len(require_value) > 0: + + events.append(require_name) + + return events + + def _check_prerequisites(self, sdk_event): + for name, value in self._manager_config.prerequisites.items(): + for val in value: + if name == sdk_event and not self._event_already_triggered(val): + return False + + return True + + def _check_suppressed_by(self, sdk_event): + for name, value in self._manager_config.suppressed_by.items(): + for val in value: + if name == sdk_event and self._event_already_triggered(val): + return False + + return True + + def _execution_limit(self, sdk_event): + limit = self._manager_config.execution_limits.get(sdk_event) + if limit == None: + return -1 + + return limit + + def _check_require_any(self, sdk_internal_event): + valid_sdk_event = ValidSdkEvent(None, False) + for name, val in self._manager_config.require_any.items(): + if sdk_internal_event in val: + valid_sdk_event = valid_sdk_event._replace(valid = True, sdk_event = name) + return valid_sdk_event + + return valid_sdk_event \ No newline at end of file diff --git a/splitio/events/events_metadata.py b/splitio/events/events_metadata.py index 3e024b66..5d6f4961 100644 --- a/splitio/events/events_metadata.py +++ b/splitio/events/events_metadata.py @@ -1,46 +1,35 @@ """Events Metadata.""" -from splitio.models.events import SdkEvent, SdkInternalEvent +from enum import Enum + +class SdkEventType(Enum): + """Public event types""" + + FLAG_UPDATE = 'FLAG_UPDATE' + SEGMENT_UPDATE = 'SEGMENT_UPDATE' class EventsMetadata(object): """Events Metadata class.""" - def __init__(self, metadata): + def __init__(self, type, names): """ Construct Events Metadata instance. """ - self._metadata = self._sanitize(metadata) + self._type = type + self._names = self._sanitize(names) - def get_data(self): - """Return metadata dict""" - return self._metadata + def get_type(self): + """Return type""" + return self._type - def get_keys(self): - """Return metadata dict keys""" - return self._metadata.keys() - - def get_values(self): - """Return metadata dict values""" - return self._metadata.values() - - def contain_key(self, key): - """Return True if key is contained in metadata""" - return key in self._metadata.keys() + def get_names(self): + """Return names""" + return self._names - def _sanitize(self, data): - """Return sanitized metadata dict with values either int, bool, str or list """ - santized_data = {} - for item_name, item_value in data.items(): - if self._value_is_valid(item_value): - santized_data[item_name] = item_value + def _sanitize(self, names): + """Return sanitized names list with values str""" + santized_data = set() + for name in names: + if isinstance(name, str): + santized_data.add(name) return santized_data - - def _value_is_valid(self, value): - """Return bool if values is int, bool, str or list[str] """ - if (value is not None) and (isinstance(value, int) or isinstance(value, bool) or isinstance(value, str)): - return True - - if isinstance(value, set): - return any([isinstance(item, str) for item in value]) - - return False \ No newline at end of file diff --git a/tests/events/test_events_delivery.py b/tests/events/test_events_delivery.py new file mode 100644 index 00000000..fc2d5464 --- /dev/null +++ b/tests/events/test_events_delivery.py @@ -0,0 +1,27 @@ +"""EventsManager test module.""" +from splitio.models.events import SdkEvent, SdkInternalEvent +from splitio.events.events_metadata import EventsMetadata +from splitio.events.events_delivery import EventsDelivery +from splitio.events.events_metadata import SdkEventType + +class EventsDeliveryTests(object): + """Tests for EventsManager.""" + + sdk_ready_flag = False + metadata = None + + def test_firing_events(self): + events_delivery = EventsDelivery() + + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1" }) + events_delivery.deliver(SdkEvent.SDK_READY, metadata, self._sdk_ready_callback) + assert self.sdk_ready_flag + self._verify_metadata(metadata) + + def _sdk_ready_callback(self, metadata): + self.sdk_ready_flag = True + self.metadata = metadata + + def _verify_metadata(self, metadata): + assert metadata.get_type() == self.metadata.get_type() + assert metadata.get_names() == self.metadata.get_names() \ No newline at end of file diff --git a/tests/events/test_events_manager.py b/tests/events/test_events_manager.py new file mode 100644 index 00000000..48c6fa45 --- /dev/null +++ b/tests/events/test_events_manager.py @@ -0,0 +1,100 @@ +"""EventsManager test module.""" +import pytest +from splitio.models.events import SdkEvent, SdkInternalEvent +from splitio.events.events_metadata import EventsMetadata +from splitio.events.events_manager_config import EventsManagerConfig +from splitio.events.events_delivery import EventsDelivery +from splitio.events.events_manager import EventsManager +from splitio.events.events_metadata import SdkEventType + +class EventsManagerTests(object): + """Tests for EventsManager.""" + + sdk_ready_flag = False + sdk_timed_out_flag = False + sdk_update_flag = False + metadata = None + + def test_firing_events(self): + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + events_manager.register(SdkEvent.SDK_READY, self._sdk_ready_callback) + events_manager.register(SdkEvent.SDK_UPDATE, self._sdk_update_callback) + + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1" }) + events_manager.notify_internal_event(SdkInternalEvent.FLAGS_UPDATED, metadata) + events_manager.notify_internal_event(SdkInternalEvent.FLAG_KILLED_NOTIFICATION, metadata) + events_manager.notify_internal_event(SdkInternalEvent.RB_SEGMENTS_UPDATED, metadata) + events_manager.notify_internal_event(SdkInternalEvent.SEGMENTS_UPDATED, metadata) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert not self.sdk_update_flag + + self._reset_flags() + events_manager.notify_internal_event(SdkInternalEvent.SDK_TIMED_OUT, metadata) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag # not registered yet + assert not self.sdk_update_flag + + events_manager.register(SdkEvent.SDK_READY_TIMED_OUT, self._sdk_timeout_callback) + events_manager.notify_internal_event(SdkInternalEvent.SDK_TIMED_OUT, metadata) + assert not self.sdk_ready_flag + assert self.sdk_timed_out_flag + assert not self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + events_manager.notify_internal_event(SdkInternalEvent.SDK_READY, metadata) + assert self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert not self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + events_manager.notify_internal_event(SdkInternalEvent.RB_SEGMENTS_UPDATED, metadata) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + events_manager.notify_internal_event(SdkInternalEvent.FLAG_KILLED_NOTIFICATION, metadata) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + events_manager.notify_internal_event(SdkInternalEvent.FLAGS_UPDATED, metadata) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + events_manager.notify_internal_event(SdkInternalEvent.SEGMENTS_UPDATED, metadata) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert self.sdk_update_flag + self._verify_metadata(metadata) + + def _reset_flags(self): + self.sdk_ready_flag = False + self.sdk_timed_out_flag = False + self.sdk_update_flag = False + self.metadata = None + + def _sdk_ready_callback(self, metadata): + self.sdk_ready_flag = True + self.metadata = metadata + + def _sdk_update_callback(self, metadata): + self.sdk_update_flag = True + self.metadata = metadata + + def _sdk_timeout_callback(self, metadata): + self.sdk_timed_out_flag = True + self.metadata = metadata + + def _verify_metadata(self, metadata): + assert metadata.get_type() == self.metadata.get_type() + assert metadata.get_names() == self.metadata.get_names() \ No newline at end of file diff --git a/tests/events/test_events_metadata.py b/tests/events/test_events_metadata.py index 0d321ca2..3ce90d0f 100644 --- a/tests/events/test_events_metadata.py +++ b/tests/events/test_events_metadata.py @@ -2,27 +2,20 @@ import pytest from splitio.events.events_metadata import EventsMetadata -from splitio.models.events import SdkEvent, SdkInternalEvent +from splitio.events.events_metadata import SdkEventType class EventsMetadataTests(object): """Tests for EventsMetadata.""" def test_build_instance(self): - data = { "updatedFlags": { "feature1" }, "sdkTimeout": 10 , "boolValue": True, "strValue": "value" } - metadata = EventsMetadata(data) - - assert len(metadata.get_keys()) == 4 - assert metadata.get_data()["updatedFlags"].pop() == "feature1" - assert len(metadata.get_data()["updatedFlags"]) == 0 - assert metadata.get_data()["sdkTimeout"] == 10 - assert metadata.get_data()["boolValue"] == True - assert metadata.get_data()["strValue"] == "value" - assert metadata.contain_key("updatedFlags") - assert not metadata.contain_key("not_exist") - assert len(metadata.get_values()) == 4 + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1" }) + assert len(metadata.get_names()) == 1 + assert metadata.get_names().pop() == "feature1" + assert len(metadata.get_names()) == 0 + assert metadata.get_type() == SdkEventType.FLAG_UPDATE def test_sanitize_none_input(self): - data = { "updatedFlags": { "feature1" }, "sdkTimeout": None, "strValue": [1, 2, 3] } - metadata = EventsMetadata(data) - assert len(metadata.get_keys()) == 1 - assert metadata.get_data()["updatedFlags"].pop() == "feature1" \ No newline at end of file + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1", None, 123, False }) + assert len(metadata.get_names()) == 1 + assert metadata.get_names().pop() == "feature1" + assert len(metadata.get_names()) == 0 From a37d3b9bd6d5c38ffaf186c7a935a532d0ec31cc Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 12 Jan 2026 09:55:45 -0800 Subject: [PATCH 05/26] added internal sdk task --- splitio/events/events_task.py | 82 ++++++++++++++++++++++++++++++++ splitio/models/notification.py | 23 +++++++++ tests/events/test_events_task.py | 74 ++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 splitio/events/events_task.py create mode 100644 tests/events/test_events_task.py diff --git a/splitio/events/events_task.py b/splitio/events/events_task.py new file mode 100644 index 00000000..c403bdbe --- /dev/null +++ b/splitio/events/events_task.py @@ -0,0 +1,82 @@ +"""sdk internal events task.""" +import logging +import threading +import abc + +_LOGGER = logging.getLogger(__name__) + +class EventsTaskBase(object, metaclass=abc.ABCMeta): + """task template.""" + + @abc.abstractmethod + def is_running(self): + """Return whether the task is running.""" + + @abc.abstractmethod + def start(self): + """Start task.""" + + @abc.abstractmethod + def stop(self): + """Stop task.""" + +class EventsTask(EventsTaskBase): + """sdk internal events processing task.""" + + _centinel = object() + + def __init__(self, notify_internal_events, internal_events_queue): + """ + Class constructor. + + :param synchronize_segment: handler to perform segment synchronization on incoming event + :type synchronize_segment: function + + :param segment_queue: queue with segment updates notifications + :type segment_queue: queue + """ + self._internal_events_queue = internal_events_queue + self._handler = notify_internal_events + self._running = False + self._worker = None + + def is_running(self): + """Return whether the working is running.""" + return self._running + + def _run(self): + """Run worker handler.""" + while self.is_running(): + event = self._internal_events_queue.get() + if not self.is_running(): + break + + if event == self._centinel: + continue + + _LOGGER.debug('Processing sdk internal event: %s', event.internal_event) + try: + self._handler(event.internal_event, event.metadata) + except Exception: + _LOGGER.error('Exception raised in events manager') + _LOGGER.debug('Exception information: ', exc_info=True) + + def start(self): + """Start worker.""" + if self.is_running(): + _LOGGER.debug('Worker is already running') + return + self._running = True + + _LOGGER.debug('Starting Event Task worker') + self._worker = threading.Thread(target=self._run, name='EventsTaskWorker', daemon=True) + self._worker.start() + + def stop(self): + """Stop worker.""" + _LOGGER.debug('Stopping Event Task worker') + if not self.is_running(): + _LOGGER.debug('Worker is not running. Ignoring.') + return + self._running = False + self._internal_events_queue.put(self._centinel) \ No newline at end of file diff --git a/splitio/models/notification.py b/splitio/models/notification.py index de28a90a..60b629e1 100644 --- a/splitio/models/notification.py +++ b/splitio/models/notification.py @@ -170,6 +170,29 @@ def notification_type(self): def split_name(self): return self._split_name +class SdkInternalEventNotification(object): # pylint: disable=too-many-instance-attributes + """SdkInternalEventNotification model object.""" + + def __init__(self, internal_event, metadata): + """ + Class constructor. + + :param internal_event: internal event object + :type channel: SdkInternalEvent + :param metadata: metadata associated with event + :type change_number: EventsMetadata + + """ + self._internal_event = internal_event + self._metadata = metadata + + @property + def internal_event(self): + return self._internal_event + + @property + def metadata(self): + return self._metadata _NOTIFICATION_MAPPERS = { Type.SPLIT_UPDATE: lambda c, d: SplitChangeNotification(c, Type.SPLIT_UPDATE, d['changeNumber']), diff --git a/tests/events/test_events_task.py b/tests/events/test_events_task.py new file mode 100644 index 00000000..17d23bec --- /dev/null +++ b/tests/events/test_events_task.py @@ -0,0 +1,74 @@ +"""EventsManager test module.""" +import pytest +import queue +import time + +from splitio.models.events import SdkInternalEvent +from splitio.models.notification import SdkInternalEventNotification +from splitio.events.events_metadata import EventsMetadata +from splitio.events.events_metadata import SdkEventType +from splitio.events.events_task import EventsTask + + +class EventsTaskTests(object): + """Tests for EventsManager.""" + + internal_event = None + metadata = None + + def test_firing_events(self): + events_queue = queue.Queue() + events_task = EventsTask(self._event_callback, events_queue) + + events_task.start() + assert events_task.is_running() + + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1" }) + events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_READY, metadata)) + time.sleep(.5) + assert self.internal_event == SdkInternalEvent.SDK_READY + self._verify_metadata(metadata) + + self._reset_flags() + events_queue.put(SdkInternalEventNotification(SdkInternalEvent.RB_SEGMENTS_UPDATED, metadata)) + time.sleep(.5) + assert self.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED + self._verify_metadata(metadata) + + events_task.stop() + time.sleep(.5) + assert not events_task.is_running() + + def test_on_error(self): + events_queue = queue.Queue() + + def handler_sync(internal_event, metadata): + raise Exception('some') + + events_task = EventsTask(handler_sync, events_queue) + events_task.start() + assert events_task.is_running() + + events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_READY, None)) + + with pytest.raises(Exception): + events_task._handler() + + assert events_task.is_running() + events_task.stop() + time.sleep(1) + assert not events_task.is_running() + + def _reset_flags(self): + self.internal_event = None + self.metadata = None + + def _event_callback(self, internal_event, metadata): + self.internal_event = internal_event + self.metadata = metadata + + def _verify_metadata(self, metadata): + assert metadata.get_type() == self.metadata.get_type() + assert metadata.get_names() == self.metadata.get_names() + + \ No newline at end of file From e019bb3b18bad8efdffe90c13fa2a5e4f7c49f07 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 13 Jan 2026 08:14:13 -0800 Subject: [PATCH 06/26] polish --- splitio/events/events_manager_config.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/splitio/events/events_manager_config.py b/splitio/events/events_manager_config.py index 891d17a5..de50c05f 100644 --- a/splitio/events/events_manager_config.py +++ b/splitio/events/events_manager_config.py @@ -39,22 +39,12 @@ def suppressed_by(self): def execution_limits(self): """Return execution_limits dict""" return self._execution_limits - - @property - def prerequisites(self): - """Return require all dict""" - return self._prerequisites - + @property def evaluation_order(self): """Return evaluation_order dict""" return self._evaluation_order - - @property - def sorted_events(self): - """Return sorted_events dict""" - return self._sorted_events - + def _get_require_all(self): """Return require all dict""" return { From 59a5530c0397c46776d3d71943c7d0d1689123d5 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 13 Jan 2026 08:16:53 -0800 Subject: [PATCH 07/26] polish --- splitio/events/events_manager_config.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/splitio/events/events_manager_config.py b/splitio/events/events_manager_config.py index 891d17a5..de50c05f 100644 --- a/splitio/events/events_manager_config.py +++ b/splitio/events/events_manager_config.py @@ -39,22 +39,12 @@ def suppressed_by(self): def execution_limits(self): """Return execution_limits dict""" return self._execution_limits - - @property - def prerequisites(self): - """Return require all dict""" - return self._prerequisites - + @property def evaluation_order(self): """Return evaluation_order dict""" return self._evaluation_order - - @property - def sorted_events(self): - """Return sorted_events dict""" - return self._sorted_events - + def _get_require_all(self): """Return require all dict""" return { From 7c9198617c7ba2477ab228ff81c1e7e9cf2085bd Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 13 Jan 2026 08:18:10 -0800 Subject: [PATCH 08/26] polish --- splitio/events/events_manager_config.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/splitio/events/events_manager_config.py b/splitio/events/events_manager_config.py index 891d17a5..de50c05f 100644 --- a/splitio/events/events_manager_config.py +++ b/splitio/events/events_manager_config.py @@ -39,22 +39,12 @@ def suppressed_by(self): def execution_limits(self): """Return execution_limits dict""" return self._execution_limits - - @property - def prerequisites(self): - """Return require all dict""" - return self._prerequisites - + @property def evaluation_order(self): """Return evaluation_order dict""" return self._evaluation_order - - @property - def sorted_events(self): - """Return sorted_events dict""" - return self._sorted_events - + def _get_require_all(self): """Return require all dict""" return { From 6e3ea36b485ebfd125ab327d9379441992c1dd28 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 13 Jan 2026 10:50:27 -0800 Subject: [PATCH 09/26] Updated split storage --- splitio/client/factory.py | 9 ++- splitio/storage/inmemmory.py | 17 ++++- tests/client/test_client.py | 49 ++++++++----- tests/client/test_manager.py | 4 +- tests/engine/test_evaluator.py | 16 +++-- .../integration/files/split_changes_temp.json | 2 +- tests/integration/test_client_e2e.py | 16 +++-- tests/push/test_split_worker.py | 3 +- tests/storage/test_inmemory_storage.py | 69 ++++++++++++++++--- tests/sync/test_splits_synchronizer.py | 19 +++-- tests/sync/test_synchronizer.py | 7 +- tests/sync/test_telemetry.py | 4 +- 12 files changed, 163 insertions(+), 52 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 6c7ce990..42fa35a2 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -3,6 +3,7 @@ import threading from collections import Counter from enum import Enum +import queue from splitio.optional.loaders import asyncio from splitio.client.client import Client, ClientAsync @@ -546,9 +547,10 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl 'events': EventsAPI(http_client, api_key, sdk_metadata, telemetry_runtime_producer), 'telemetry': TelemetryAPI(http_client, api_key, sdk_metadata, telemetry_runtime_producer), } - + + events_queue = queue.Queue() storages = { - 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), + 'splits': InMemorySplitStorage(events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(), 'rule_based_segments': InMemoryRuleBasedSegmentStorage(), 'impressions': InMemoryImpressionStorage(cfg['impressionsQueueSize'], telemetry_runtime_producer), @@ -1096,8 +1098,9 @@ def _build_localhost_factory(cfg): telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + events_queue = queue.Queue() storages = { - 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), + 'splits': InMemorySplitStorage(events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(), # not used, just to avoid possible future errors. 'rule_based_segments': InMemoryRuleBasedSegmentStorage(), 'impressions': LocalhostImpressionsStorage(), diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index e1740b72..02cea19f 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -7,6 +7,9 @@ from splitio.models.segments import Segment from splitio.models.telemetry import HTTPErrors, HTTPLatencies, MethodExceptions, MethodLatencies, LastSynchronization, StreamingEvents, TelemetryConfig, TelemetryCounters, CounterConstants, \ HTTPErrorsAsync, HTTPLatenciesAsync, MethodExceptionsAsync, MethodLatenciesAsync, LastSynchronizationAsync, StreamingEventsAsync, TelemetryConfigAsync, TelemetryCountersAsync +from splitio.models.events import SdkInternalEvent +from splitio.events.events_metadata import EventsMetadata, SdkEventType +from splitio.models.notification import SdkInternalEventNotification from splitio.storage import FlagSetsFilter, SplitStorage, SegmentStorage, ImpressionStorage, EventStorage, TelemetryStorage, RuleBasedSegmentsStorage from splitio.optional.loaders import asyncio @@ -479,7 +482,7 @@ def _decrease_traffic_type_count(self, traffic_type_name): class InMemorySplitStorage(InMemorySplitStorageBase): """InMemory implementation of a feature flag storage.""" - def __init__(self, flag_sets=[]): + def __init__(self, internal_event_queue, flag_sets=[]): """Constructor.""" self._lock = threading.RLock() self._feature_flags = {} @@ -487,6 +490,7 @@ def __init__(self, flag_sets=[]): self._traffic_types = Counter() self.flag_set = FlagSets(flag_sets) self.flag_set_filter = FlagSetsFilter(flag_sets) + self._internal_event_queue = internal_event_queue def clear(self): """ @@ -535,6 +539,13 @@ def update(self, to_add, to_delete, new_change_number): [self._put(add_feature_flag) for add_feature_flag in to_add] [self._remove(delete_feature_flag) for delete_feature_flag in to_delete] self._set_change_number(new_change_number) + to_notify = [] + [to_notify.append(feature.name) for feature in to_add] + to_notify.extend(to_delete) + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.FLAGS_UPDATED, + EventsMetadata(SdkEventType.FLAG_UPDATE, set(to_notify)))) def _put(self, feature_flag): """ @@ -680,6 +691,10 @@ def kill_locally(self, feature_flag_name, default_treatment, change_number): return feature_flag.local_kill(default_treatment, change_number) self._put(feature_flag) + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.FLAG_KILLED_NOTIFICATION, + EventsMetadata(SdkEventType.FLAG_UPDATE, {feature_flag_name}))) def is_flag_set_exist(self, flag_set): """ diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 27ed399d..5846b169 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -6,6 +6,7 @@ import unittest.mock as mock import time import pytest +import queue from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync @@ -36,7 +37,8 @@ def test_get_treatment(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -113,7 +115,8 @@ def test_get_treatment_with_config(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -190,7 +193,8 @@ def test_get_treatments(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -270,7 +274,8 @@ def test_get_treatments_by_flag_set(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -349,7 +354,8 @@ def test_get_treatments_by_flag_sets(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -428,7 +434,8 @@ def test_get_treatments_with_config(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -511,7 +518,8 @@ def test_get_treatments_with_config_by_flag_set(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -591,7 +599,8 @@ def test_get_treatments_with_config_by_flag_sets(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -671,7 +680,8 @@ def test_impression_toggle_optimized(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -735,7 +745,8 @@ def test_impression_toggle_debug(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -799,7 +810,8 @@ def test_impression_toggle_none(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() @@ -939,7 +951,8 @@ def test_evaluations_before_running_post_fork(self, mocker): telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) @@ -1020,7 +1033,8 @@ def test_telemetry_not_ready(self, mocker): telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) @@ -1053,7 +1067,8 @@ def synchronize_config(*_): factory.destroy() def test_telemetry_record_treatment_exception(self, mocker): - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) segment_storage = mocker.Mock(spec=SegmentStorage) rb_segment_storage = InMemoryRuleBasedSegmentStorage() @@ -1158,7 +1173,8 @@ def test_telemetry_method_latency(self, mocker): impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) @@ -1270,7 +1286,8 @@ def test_impressions_properties(self, mocker): """Test get_treatment execution paths.""" telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() diff --git a/tests/client/test_manager.py b/tests/client/test_manager.py index 19e1bbb0..1582b29b 100644 --- a/tests/client/test_manager.py +++ b/tests/client/test_manager.py @@ -1,5 +1,6 @@ """SDK main manager test module.""" import pytest +import queue from splitio.client.factory import SplitFactory from splitio.client.manager import SplitManager, SplitManagerAsync, _LOGGER as _logger @@ -16,7 +17,8 @@ class SplitManagerTests(object): # pylint: disable=too-few-public-methods def test_manager_calls(self, mocker): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) factory = mocker.Mock(spec=SplitFactory) factory._storages = {'split': storage} diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index 07f79a80..bccc3f78 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -4,6 +4,7 @@ import os import pytest import copy +import queue from splitio.models.splits import Split, Status, from_raw, Prerequisites from splitio.models import segments @@ -261,7 +262,8 @@ def test_evaluate_treatment_with_rule_based_segment(self, mocker): def test_evaluate_treatment_with_rbs_in_condition(self): e = evaluator.Evaluator(splitters.Splitter()) - splits_storage = InMemorySplitStorage() + events_queue = queue.Queue() + splits_storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() segment_storage = InMemorySegmentStorage() evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage) @@ -287,7 +289,8 @@ def test_using_segment_in_excluded(self): with open(rbs_segments, 'r') as flo: data = json.loads(flo.read()) e = evaluator.Evaluator(splitters.Splitter()) - splits_storage = InMemorySplitStorage() + events_queue = queue.Queue() + splits_storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() segment_storage = InMemorySegmentStorage() evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage) @@ -311,7 +314,8 @@ def test_using_rbs_in_excluded(self): with open(rbs_segments, 'r') as flo: data = json.loads(flo.read()) e = evaluator.Evaluator(splitters.Splitter()) - splits_storage = InMemorySplitStorage() + events_queue = queue.Queue() + splits_storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() segment_storage = InMemorySegmentStorage() evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage) @@ -334,7 +338,8 @@ def test_prerequisites(self): with open(splits_load, 'r') as flo: data = json.loads(flo.read()) e = evaluator.Evaluator(splitters.Splitter()) - splits_storage = InMemorySplitStorage() + events_queue = queue.Queue() + splits_storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() segment_storage = InMemorySegmentStorage() evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage) @@ -542,7 +547,8 @@ def test_get_context(self): """Test context.""" mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [Prerequisites('split2', ['on'])]) split2 = Split('split2', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, []) - flag_storage = InMemorySplitStorage([]) + events_queue = queue.Queue() + flag_storage = InMemorySplitStorage(events_queue, []) segment_storage = InMemorySegmentStorage() rbs_segment_storage = InMemoryRuleBasedSegmentStorage() flag_storage.update([mocked_split, split2], [], -1) diff --git a/tests/integration/files/split_changes_temp.json b/tests/integration/files/split_changes_temp.json index 64575226..24d876a4 100644 --- a/tests/integration/files/split_changes_temp.json +++ b/tests/integration/files/split_changes_temp.json @@ -1 +1 @@ -{"ff": {"t": -1, "s": -1, "d": [{"changeNumber": 10, "trafficTypeName": "user", "name": "rbs_feature_flag", "trafficAllocation": 100, "trafficAllocationSeed": 1828377380, "seed": -286617921, "status": "ACTIVE", "killed": false, "defaultTreatment": "off", "algo": 2, "conditions": [{"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user"}, "matcherType": "IN_RULE_BASED_SEGMENT", "negate": false, "userDefinedSegmentMatcherData": {"segmentName": "sample_rule_based_segment"}}]}, "partitions": [{"treatment": "on", "size": 100}, {"treatment": "off", "size": 0}], "label": "in rule based segment sample_rule_based_segment"}, {"conditionType": "ROLLOUT", "matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user"}, "matcherType": "ALL_KEYS", "negate": false}]}, "partitions": [{"treatment": "on", "size": 0}, {"treatment": "off", "size": 100}], "label": "default rule"}], "configurations": {}, "sets": [], "impressionsDisabled": false}]}, "rbs": {"t": 1675259356568, "s": -1, "d": [{"changeNumber": 5, "name": "sample_rule_based_segment", "status": "ACTIVE", "trafficTypeName": "user", "excluded": {"keys": ["mauro@split.io", "gaston@split.io"], "segments": []}, "conditions": [{"matcherGroup": {"combiner": "AND", "matchers": [{"keySelector": {"trafficType": "user", "attribute": "email"}, "matcherType": "ENDS_WITH", "negate": false, "whitelistMatcherData": {"whitelist": ["@split.io"]}}]}}]}]}} \ No newline at end of file +{"ff": {"t": -1, "s": -1, "d": [{"name": "SPLIT_1", "status": "ACTIVE", "killed": false, "defaultTreatment": "off", "configurations": {}, "conditions": []}]}, "rbs": {"t": -1, "s": -1, "d": [{"changeNumber": 12, "name": "some_segment", "status": "ACTIVE", "trafficTypeName": "user", "excluded": {"keys": [], "segments": []}, "conditions": []}]}} \ No newline at end of file diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 194d86f1..018f3d42 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -6,6 +6,7 @@ import threading import time import pytest +import queue import unittest.mock as mocker from redis import StrictRedis @@ -515,7 +516,8 @@ class InMemoryDebugIntegrationTests(object): def setup_method(self): """Prepare storages with test data.""" - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() @@ -677,7 +679,8 @@ class InMemoryOptimizedIntegrationTests(object): def setup_method(self): """Prepare storages with test data.""" - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() rb_segment_storage = InMemoryRuleBasedSegmentStorage() split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') @@ -1965,7 +1968,8 @@ class InMemoryImpressionsToggleIntegrationTests(object): """InMemory storage-based impressions toggle integration tests.""" def test_optimized(self): - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), @@ -2023,7 +2027,8 @@ def test_optimized(self): assert client.get_treatment('user1', 'fallback_feature') == 'on-local' def test_debug(self): - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), @@ -2081,7 +2086,8 @@ def test_debug(self): assert client.get_treatment('user1', 'fallback_feature') == 'on-local' def test_none(self): - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), diff --git a/tests/push/test_split_worker.py b/tests/push/test_split_worker.py index 0d3ac824..22a146e3 100644 --- a/tests/push/test_split_worker.py +++ b/tests/push/test_split_worker.py @@ -262,7 +262,8 @@ def update(feature_flag_add, feature_flag_delete, change_number): def test_fetch_segment(self, mocker): q = queue.Queue() - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) segment_storage = InMemorySegmentStorage() self.segment_name = None diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 2bb113d7..7639bcb7 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -3,13 +3,16 @@ import random import pytest import copy +import queue from splitio.models.splits import Split from splitio.models.segments import Segment from splitio.models.impressions import Impression from splitio.models.events import Event, EventWrapper +from splitio.models.events import SdkInternalEvent import splitio.models.telemetry as ModelTelemetry from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageProducerAsync +from splitio.events.events_metadata import SdkEventType from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, InMemorySegmentStorageAsync, InMemorySplitStorageAsync, \ InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, InMemoryImpressionStorageAsync, InMemoryEventStorageAsync, \ InMemoryTelemetryStorageAsync, FlagSets, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync @@ -65,7 +68,8 @@ class InMemorySplitStorageTests(object): def test_storing_retrieving_splits(self, mocker): """Test storing and retrieving splits works.""" - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) split = mocker.Mock(spec=Split) name_property = mocker.PropertyMock() @@ -100,7 +104,8 @@ def test_get_splits(self, mocker): type(split2).name = name2_prop type(split2).sets = sets_property - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) storage.update([split1, split2], [], -1) splits = storage.fetch_many(['split1', 'split2', 'split3']) @@ -113,7 +118,8 @@ def test_get_splits(self, mocker): def test_store_get_changenumber(self): """Test that storing and retrieving change numbers works.""" - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) assert storage.get_change_number() == -1 storage.update([], [], 5) assert storage.get_change_number() == 5 @@ -134,7 +140,8 @@ def test_get_split_names(self, mocker): type(split2).name = name2_prop type(split2).sets = sets_property - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) storage.update([split1, split2], [], -1) assert set(storage.get_split_names()) == set(['split1', 'split2']) @@ -155,7 +162,8 @@ def test_get_all_splits(self, mocker): type(split2).name = name2_prop type(split2).sets = sets_property - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) storage.update([split1, split2], [], -1) all_splits = storage.get_all_splits() @@ -189,7 +197,8 @@ def test_is_valid_traffic_type(self, mocker): type(split2).sets = sets_property type(split3).sets = sets_property - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) storage.update([split1], [], -1) assert storage.is_valid_traffic_type('user') is True @@ -217,7 +226,8 @@ def test_is_valid_traffic_type(self, mocker): def test_traffic_type_inc_dec_logic(self, mocker): """Test that adding/removing split, handles traffic types correctly.""" - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) split1 = mocker.Mock() name1_prop = mocker.PropertyMock() @@ -253,7 +263,8 @@ def test_traffic_type_inc_dec_logic(self, mocker): def test_kill_locally(self): """Test kill local.""" - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) split = Split('some_split', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1) @@ -271,7 +282,8 @@ def test_kill_locally(self): assert storage.get('some_split').change_number == 3 def test_flag_sets_with_config_sets(self): - storage = InMemorySplitStorage(['set10', 'set02', 'set05']) + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue, ['set10', 'set02', 'set05']) assert storage.flag_set_filter.flag_sets == {'set10', 'set02', 'set05'} assert storage.flag_set_filter.should_filter @@ -316,7 +328,8 @@ def test_flag_sets_with_config_sets(self): assert not storage.is_flag_set_exist('set04') def test_flag_sets_withut_config_sets(self): - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) assert storage.flag_set_filter.flag_sets == set({}) assert not storage.flag_set_filter.should_filter @@ -358,6 +371,42 @@ def test_flag_sets_withut_config_sets(self): assert storage.get_feature_flags_by_sets(['set05']) == ['split3'] assert storage.get_feature_flags_by_sets(['set04', 'set05']) == ['split3'] + def test_internal_event_notification(self, mocker): + """Test storing and retrieving splits works.""" + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) + + split = mocker.Mock(spec=Split) + name_property = mocker.PropertyMock() + name_property.return_value = 'some_split' + type(split).name = name_property + sets_property = mocker.PropertyMock() + sets_property.return_value = ['set_1'] + type(split).sets = sets_property + + storage.update([split], [], -1) + assert storage.get('some_split') == split + assert storage.get_split_names() == ['some_split'] + assert storage.get_all_splits() == [split] + event = events_queue.get() + assert event.internal_event == SdkInternalEvent.FLAGS_UPDATED + assert event.metadata.get_type() == SdkEventType.FLAG_UPDATE + assert event.metadata.get_names() == {'some_split'} + + split2 = Split('another_split', 123456789, False, 'some', 'traffic_type', + 'ACTIVE', 1) + storage.update([split2], ['some_split'], 1) + event = events_queue.get() + assert event.internal_event == SdkInternalEvent.FLAGS_UPDATED + assert event.metadata.get_type() == SdkEventType.FLAG_UPDATE + assert event.metadata.get_names() == {'another_split', 'some_split'} + + storage.kill_locally('another_split', 'default_treatment', 3) + event = events_queue.get() + assert event.internal_event == SdkInternalEvent.FLAG_KILLED_NOTIFICATION + assert event.metadata.get_type() == SdkEventType.FLAG_UPDATE + assert event.metadata.get_names() == {'another_split'} + class InMemorySplitStorageAsyncTests(object): """In memory split storage test cases.""" diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index fd9ac585..d63b5f6a 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -4,6 +4,7 @@ import os import json import copy +import queue from splitio.util.backoff import Backoff from splitio.api import APIException @@ -401,7 +402,8 @@ def intersect(sets): def test_sync_flag_sets_with_config_sets(self, mocker): """Test split sync with flag sets.""" - storage = InMemorySplitStorage(['set1', 'set2']) + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue, ['set1', 'set2']) rbs_storage = InMemoryRuleBasedSegmentStorage() split = copy.deepcopy(self.splits[0]) @@ -447,7 +449,8 @@ def get_changes(*args, **kwargs): def test_sync_flag_sets_without_config_sets(self, mocker): """Test split sync with flag sets.""" - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() split = copy.deepcopy(self.splits[0]) split['name'] = 'second' @@ -895,7 +898,8 @@ def test_synchronize_splits_error(self, mocker): def test_synchronize_splits(self, mocker): """Test split sync.""" - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() def read_splits_from_json_file(*args, **kwargs): @@ -939,7 +943,8 @@ def read_splits_from_json_file(*args, **kwargs): def test_sync_flag_sets_with_config_sets(self, mocker): """Test split sync with flag sets.""" - storage = InMemorySplitStorage(['set1', 'set2']) + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue, ['set1', 'set2']) rbs_storage = InMemoryRuleBasedSegmentStorage() split = self.payload["ff"]["d"][0].copy() @@ -981,7 +986,8 @@ def read_feature_flags_from_json_file(*args, **kwargs): def test_sync_flag_sets_without_config_sets(self, mocker): """Test split sync with flag sets.""" - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() split = self.payload["ff"]["d"][0].copy() @@ -1026,7 +1032,8 @@ def test_reading_json(self, mocker): f = open("./splits.json", "w") f.write(json.dumps(self.payload)) f.close() - storage = InMemorySplitStorage() + events_queue = queue.Queue() + storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() split_synchronizer = LocalSplitSynchronizer("./splits.json", storage, rbs_storage, LocalhostMode.JSON) split_synchronizer.synchronize_splits() diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index 60ab7993..17a4f103 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -1,6 +1,7 @@ """Synchronizer tests.""" import unittest.mock as mock import pytest +import queue from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitTasks, SplitSynchronizers, LocalhostSynchronizer, LocalhostSynchronizerAsync, RedisSynchronizer, RedisSynchronizerAsync from splitio.tasks.split_sync import SplitSynchronizationTask, SplitSynchronizationTaskAsync @@ -124,7 +125,8 @@ def run(x, y, c): assert not sychronizer._synchronize_segments() def test_synchronize_splits(self, mocker): - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() split_api = mocker.Mock() split_api.fetch_splits.return_value = {'ff': {'d': splits, 's': 123, @@ -151,7 +153,8 @@ def test_synchronize_splits(self, mocker): assert inserted_segment.keys == {'key1', 'key2', 'key3'} def test_synchronize_splits_calling_segment_sync_once(self, mocker): - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) rbs_storage = InMemoryRuleBasedSegmentStorage() split_api = mocker.Mock() split_api.fetch_splits.return_value = {'ff': {'d': splits, 's': 123, diff --git a/tests/sync/test_telemetry.py b/tests/sync/test_telemetry.py index 898216f8..c37251af 100644 --- a/tests/sync/test_telemetry.py +++ b/tests/sync/test_telemetry.py @@ -1,6 +1,7 @@ """Telemetry Worker tests.""" import unittest.mock as mock import pytest +import queue from splitio.sync.telemetry import TelemetrySynchronizer, TelemetrySynchronizerAsync, InMemoryTelemetrySubmitter, InMemoryTelemetrySubmitterAsync from splitio.engine.telemetry import TelemetryStorageConsumer, TelemetryStorageConsumerAsync @@ -57,7 +58,8 @@ def test_synchronize_telemetry(self, mocker): api = mocker.Mock(spec=TelemetryAPI) telemetry_storage = InMemoryTelemetryStorage() telemetry_consumer = TelemetryStorageConsumer(telemetry_storage) - split_storage = InMemorySplitStorage() + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], -1) segment_storage = InMemorySegmentStorage() segment_storage.put(Segment('segment1', [], 123)) From b00410d1ca2fc891dacea6e17356a3b46ff33780 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 14 Jan 2026 10:07:35 -0800 Subject: [PATCH 10/26] updated segments and rb segments storages --- splitio/client/factory.py | 8 +-- splitio/storage/inmemmory.py | 24 +++++++-- tests/client/test_client.py | 62 +++++++++++----------- tests/engine/test_evaluator.py | 20 +++---- tests/integration/test_client_e2e.py | 20 +++---- tests/push/test_split_worker.py | 2 +- tests/storage/test_inmemory_storage.py | 66 +++++++++++++++++++++--- tests/sync/test_segments_synchronizer.py | 6 ++- tests/sync/test_splits_synchronizer.py | 13 ++--- tests/sync/test_synchronizer.py | 6 +-- tests/sync/test_telemetry.py | 2 +- tests/util/test_storage_helper.py | 4 +- 12 files changed, 152 insertions(+), 81 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 42fa35a2..da41868c 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -551,8 +551,8 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl events_queue = queue.Queue() storages = { 'splits': InMemorySplitStorage(events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), - 'segments': InMemorySegmentStorage(), - 'rule_based_segments': InMemoryRuleBasedSegmentStorage(), + 'segments': InMemorySegmentStorage(events_queue), + 'rule_based_segments': InMemoryRuleBasedSegmentStorage(events_queue), 'impressions': InMemoryImpressionStorage(cfg['impressionsQueueSize'], telemetry_runtime_producer), 'events': InMemoryEventStorage(cfg['eventsQueueSize'], telemetry_runtime_producer), } @@ -1101,8 +1101,8 @@ def _build_localhost_factory(cfg): events_queue = queue.Queue() storages = { 'splits': InMemorySplitStorage(events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), - 'segments': InMemorySegmentStorage(), # not used, just to avoid possible future errors. - 'rule_based_segments': InMemoryRuleBasedSegmentStorage(), + 'segments': InMemorySegmentStorage(events_queue), # not used, just to avoid possible future errors. + 'rule_based_segments': InMemoryRuleBasedSegmentStorage(events_queue), 'impressions': LocalhostImpressionsStorage(), 'events': LocalhostEventsStorage(), } diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 02cea19f..75097b14 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -113,11 +113,12 @@ def remove_flag_set(self, flag_sets, feature_flag_name, should_filter): class InMemoryRuleBasedSegmentStorage(RuleBasedSegmentsStorage): """InMemory implementation of a feature flag storage base.""" - def __init__(self): + def __init__(self, internal_event_queue): """Constructor.""" self._lock = threading.RLock() self._rule_based_segments = {} self._change_number = -1 + self._internal_event_queue = internal_event_queue def clear(self): """ @@ -153,6 +154,10 @@ def update(self, to_add, to_delete, new_change_number): [self._put(add_segment) for add_segment in to_add] [self._remove(delete_segment) for delete_segment in to_delete] self._set_change_number(new_change_number) + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.RB_SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) def _put(self, rule_based_segment): """ @@ -934,11 +939,12 @@ async def is_flag_set_exist(self, flag_set): class InMemorySegmentStorage(SegmentStorage): """In-memory implementation of a segment storage.""" - def __init__(self): + def __init__(self, internal_event_queue): """Constructor.""" self._segments = {} self._change_numbers = {} self._lock = threading.RLock() + self._internal_event_queue = internal_event_queue def get(self, segment_name): """ @@ -968,9 +974,14 @@ def put(self, segment): with self._lock: self._segments[segment.name] = segment + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + def update(self, segment_name, to_add, to_remove, change_number=None): """ - Update a feature flag. Create it if it doesn't exist. + Update a segment. Create it if it doesn't exist. :param segment_name: Name of the segment to update. :type segment_name: str @@ -988,6 +999,11 @@ def update(self, segment_name, to_add, to_remove, change_number=None): if change_number is not None: self._segments[segment_name].change_number = change_number + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + def get_change_number(self, segment_name): """ Retrieve latest change number for a segment. @@ -1100,7 +1116,7 @@ async def put(self, segment): async def update(self, segment_name, to_add, to_remove, change_number=None): """ - Update a feature flag. Create it if it doesn't exist. + Update a segment. Create it if it doesn't exist. :param segment_name: Name of the segment to update. :type segment_name: str diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 5846b169..a0226126 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -39,8 +39,8 @@ def test_get_treatment(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -117,8 +117,8 @@ def test_get_treatment_with_config(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) @@ -195,8 +195,8 @@ def test_get_treatments(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) @@ -276,8 +276,8 @@ def test_get_treatments_by_flag_set(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) @@ -356,8 +356,8 @@ def test_get_treatments_by_flag_sets(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) @@ -436,8 +436,8 @@ def test_get_treatments_with_config(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) @@ -520,8 +520,8 @@ def test_get_treatments_with_config_by_flag_set(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) @@ -601,8 +601,8 @@ def test_get_treatments_with_config_by_flag_sets(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) @@ -682,8 +682,8 @@ def test_impression_toggle_optimized(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -747,8 +747,8 @@ def test_impression_toggle_debug(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -812,8 +812,8 @@ def test_impression_toggle_none(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -953,8 +953,8 @@ def test_evaluations_before_running_post_fork(self, mocker): impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) destroyed_property = mocker.PropertyMock() destroyed_property.return_value = False @@ -1035,8 +1035,8 @@ def test_telemetry_not_ready(self, mocker): impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) recorder = StandardRecorder(impmanager, mocker.Mock(), mocker.Mock(), telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) factory = SplitFactory('localhost', @@ -1071,7 +1071,7 @@ def test_telemetry_record_treatment_exception(self, mocker): split_storage = InMemorySplitStorage(events_queue) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) segment_storage = mocker.Mock(spec=SegmentStorage) - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) impression_storage = mocker.Mock(spec=ImpressionStorage) event_storage = mocker.Mock(spec=EventStorage) destroyed_property = mocker.PropertyMock() @@ -1175,8 +1175,8 @@ def test_telemetry_method_latency(self, mocker): impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) destroyed_property = mocker.PropertyMock() @@ -1288,8 +1288,8 @@ def test_impressions_properties(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index bccc3f78..dc83cc36 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -264,8 +264,8 @@ def test_evaluate_treatment_with_rbs_in_condition(self): e = evaluator.Evaluator(splitters.Splitter()) events_queue = queue.Queue() splits_storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() - segment_storage = InMemorySegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) + segment_storage = InMemorySegmentStorage(events_queue) evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage) rbs_segments = os.path.join(os.path.dirname(__file__), 'files', 'rule_base_segments.json') @@ -291,8 +291,8 @@ def test_using_segment_in_excluded(self): e = evaluator.Evaluator(splitters.Splitter()) events_queue = queue.Queue() splits_storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() - segment_storage = InMemorySegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) + segment_storage = InMemorySegmentStorage(events_queue) evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage) mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, []) @@ -316,8 +316,8 @@ def test_using_rbs_in_excluded(self): e = evaluator.Evaluator(splitters.Splitter()) events_queue = queue.Queue() splits_storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() - segment_storage = InMemorySegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) + segment_storage = InMemorySegmentStorage(events_queue) evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage) mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, []) @@ -340,8 +340,8 @@ def test_prerequisites(self): e = evaluator.Evaluator(splitters.Splitter()) events_queue = queue.Queue() splits_storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() - segment_storage = InMemorySegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) + segment_storage = InMemorySegmentStorage(events_queue) evaluation_facctory = EvaluationDataFactory(splits_storage, segment_storage, rbs_storage) rbs = rule_based_segments.from_raw(data["rbs"]["d"][0]) @@ -549,8 +549,8 @@ def test_get_context(self): split2 = Split('split2', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, []) events_queue = queue.Queue() flag_storage = InMemorySplitStorage(events_queue, []) - segment_storage = InMemorySegmentStorage() - rbs_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rbs_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) flag_storage.update([mocked_split, split2], [], -1) rbs = copy.deepcopy(rbs_raw) rbs['conditions'].append( diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 018f3d42..8789aa3d 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -518,8 +518,8 @@ def setup_method(self): """Prepare storages with test data.""" events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') with open(split_fn, 'r') as flo: @@ -681,8 +681,8 @@ def setup_method(self): """Prepare storages with test data.""" events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() - rb_segment_storage = InMemoryRuleBasedSegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') with open(split_fn, 'r') as flo: data = json.loads(flo.read()) @@ -1970,7 +1970,7 @@ class InMemoryImpressionsToggleIntegrationTests(object): def test_optimized(self): events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), splits.from_raw(splits_json['splitChange1_1']['ff']['d'][1]), @@ -1985,7 +1985,7 @@ def test_optimized(self): storages = { 'splits': split_storage, 'segments': segment_storage, - 'rule_based_segments': InMemoryRuleBasedSegmentStorage(), + 'rule_based_segments': InMemoryRuleBasedSegmentStorage(events_queue), 'impressions': InMemoryImpressionStorage(5000, telemetry_runtime_producer), 'events': InMemoryEventStorage(5000, telemetry_runtime_producer), } @@ -2029,7 +2029,7 @@ def test_optimized(self): def test_debug(self): events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), splits.from_raw(splits_json['splitChange1_1']['ff']['d'][1]), @@ -2044,7 +2044,7 @@ def test_debug(self): storages = { 'splits': split_storage, 'segments': segment_storage, - 'rule_based_segments': InMemoryRuleBasedSegmentStorage(), + 'rule_based_segments': InMemoryRuleBasedSegmentStorage(events_queue), 'impressions': InMemoryImpressionStorage(5000, telemetry_runtime_producer), 'events': InMemoryEventStorage(5000, telemetry_runtime_producer), } @@ -2088,7 +2088,7 @@ def test_debug(self): def test_none(self): events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), splits.from_raw(splits_json['splitChange1_1']['ff']['d'][1]), @@ -2103,7 +2103,7 @@ def test_none(self): storages = { 'splits': split_storage, 'segments': segment_storage, - 'rule_based_segments': InMemoryRuleBasedSegmentStorage(), + 'rule_based_segments': InMemoryRuleBasedSegmentStorage(events_queue), 'impressions': InMemoryImpressionStorage(5000, telemetry_runtime_producer), 'events': InMemoryEventStorage(5000, telemetry_runtime_producer), } diff --git a/tests/push/test_split_worker.py b/tests/push/test_split_worker.py index 22a146e3..198372a7 100644 --- a/tests/push/test_split_worker.py +++ b/tests/push/test_split_worker.py @@ -264,7 +264,7 @@ def test_fetch_segment(self, mocker): q = queue.Queue() events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - segment_storage = InMemorySegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) self.segment_name = None def segment_handler_sync(segment_name, change_number): diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 7639bcb7..a37a1a4d 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -713,7 +713,8 @@ class InMemorySegmentStorageTests(object): def test_segment_storage_retrieval(self, mocker): """Test storing and retrieving segments.""" - storage = InMemorySegmentStorage() + events_queue = queue.Queue() + storage = InMemorySegmentStorage(events_queue) segment = mocker.Mock(spec=Segment) name_property = mocker.PropertyMock() name_property.return_value = 'some_segment' @@ -725,14 +726,16 @@ def test_segment_storage_retrieval(self, mocker): def test_change_number(self, mocker): """Test storing and retrieving segment changeNumber.""" - storage = InMemorySegmentStorage() + events_queue = queue.Queue() + storage = InMemorySegmentStorage(events_queue) storage.set_change_number('some_segment', 123) # Change number is not updated if segment doesn't exist assert storage.get_change_number('some_segment') is None assert storage.get_change_number('nonexistant-segment') is None # Change number is updated if segment does exist. - storage = InMemorySegmentStorage() + events_queue = queue.Queue() + storage = InMemorySegmentStorage(events_queue) segment = mocker.Mock(spec=Segment) name_property = mocker.PropertyMock() name_property.return_value = 'some_segment' @@ -743,7 +746,8 @@ def test_change_number(self, mocker): def test_segment_contains(self, mocker): """Test using storage to determine whether a key belongs to a segment.""" - storage = InMemorySegmentStorage() + events_queue = queue.Queue() + storage = InMemorySegmentStorage(events_queue) segment = mocker.Mock(spec=Segment) name_property = mocker.PropertyMock() name_property.return_value = 'some_segment' @@ -755,7 +759,8 @@ def test_segment_contains(self, mocker): def test_segment_update(self): """Test updating a segment.""" - storage = InMemorySegmentStorage() + events_queue = queue.Queue() + storage = InMemorySegmentStorage(events_queue) segment = Segment('some_segment', ['key1', 'key2', 'key3'], 123) storage.put(segment) assert storage.get('some_segment') == segment @@ -768,6 +773,22 @@ def test_segment_update(self): assert not storage.segment_contains('some_segment', 'key3') assert storage.get_change_number('some_segment') == 456 + def test_internal_event_notification(self): + """Test updating a segment.""" + events_queue = queue.Queue() + storage = InMemorySegmentStorage(events_queue) + segment = Segment('some_segment', ['key1', 'key2', 'key3'], 123) + storage.put(segment) + event = events_queue.get() + assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED + assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert len(event.metadata.get_names()) == 0 + + storage.update('some_segment', ['key4', 'key5'], ['key2', 'key3'], 456) + event = events_queue.get() + assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED + assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert len(event.metadata.get_names()) == 0 class InMemorySegmentStorageAsyncTests(object): """In memory segment storage tests.""" @@ -1865,7 +1886,8 @@ class InMemoryRuleBasedSegmentStorageTests(object): def test_storing_retrieving_segments(self, mocker): """Test storing and retrieving splits works.""" - rbs_storage = InMemoryRuleBasedSegmentStorage() + events_queue = queue.Queue() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) segment1 = mocker.Mock(spec=RuleBasedSegment) name_property = mocker.PropertyMock() @@ -1887,7 +1909,8 @@ def test_storing_retrieving_segments(self, mocker): def test_store_get_changenumber(self): """Test that storing and retrieving change numbers works.""" - storage = InMemoryRuleBasedSegmentStorage() + events_queue = queue.Queue() + storage = InMemoryRuleBasedSegmentStorage(events_queue) assert storage.get_change_number() == -1 storage.update([], [], 5) assert storage.get_change_number() == 5 @@ -1911,12 +1934,39 @@ def test_contains(self): raw3 = copy.deepcopy(raw) raw3["name"] = "segment3" segment3 = rule_based_segments.from_raw(raw3) - storage = InMemoryRuleBasedSegmentStorage() + events_queue = queue.Queue() + storage = InMemoryRuleBasedSegmentStorage(events_queue) storage.update([segment1, segment2, segment3], [], -1) assert storage.contains(["segment1"]) assert storage.contains(["segment1", "segment3"]) assert not storage.contains(["segment5"]) + def test_internal_event_notification(self, mocker): + """Test storing and retrieving splits works.""" + events_queue = queue.Queue() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) + + segment1 = mocker.Mock(spec=RuleBasedSegment) + name_property = mocker.PropertyMock() + name_property.return_value = 'some_segment' + type(segment1).name = name_property + + segment2 = mocker.Mock() + name2_prop = mocker.PropertyMock() + name2_prop.return_value = 'segment2' + type(segment2).name = name2_prop + + rbs_storage.update([segment1, segment2], [], -1) + event = events_queue.get() + assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED + assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert len(event.metadata.get_names()) == 0 + + rbs_storage.update([], ['some_segment'], -1) + assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED + assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert len(event.metadata.get_names()) == 0 + class InMemoryRuleBasedSegmentStorageAsyncTests(object): """In memory rule based segment storage test cases.""" diff --git a/tests/sync/test_segments_synchronizer.py b/tests/sync/test_segments_synchronizer.py index e88db2fa..a3657e98 100644 --- a/tests/sync/test_segments_synchronizer.py +++ b/tests/sync/test_segments_synchronizer.py @@ -504,7 +504,8 @@ def test_synchronize_segments(self, mocker): """Test the normal operation flow.""" split_storage = mocker.Mock(spec=InMemorySplitStorage) split_storage.get_segment_names.return_value = ['segmentA', 'segmentB', 'segmentC'] - storage = InMemorySegmentStorage() + events_queue = queue.Queue() + storage = InMemorySegmentStorage(events_queue) segment_a = {'name': 'segmentA', 'added': ['key1', 'key2', 'key3'], 'removed': [], 'since': -1, 'till': 123} @@ -585,7 +586,8 @@ def test_reading_json(self, mocker): f.write('{"name": "segmentA", "added": ["key1", "key2", "key3"], "removed": [],"since": -1, "till": 123}') f.close() split_storage = mocker.Mock(spec=InMemorySplitStorage) - storage = InMemorySegmentStorage() + events_queue = queue.Queue() + storage = InMemorySegmentStorage(events_queue) segments_synchronizer = LocalSegmentSynchronizer('.', split_storage, storage) assert segments_synchronizer.synchronize_segments(['segmentA']) diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index d63b5f6a..ca3daa82 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -404,7 +404,8 @@ def test_sync_flag_sets_with_config_sets(self, mocker): """Test split sync with flag sets.""" events_queue = queue.Queue() storage = InMemorySplitStorage(events_queue, ['set1', 'set2']) - rbs_storage = InMemoryRuleBasedSegmentStorage() + events_queue = queue.Queue() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) split = copy.deepcopy(self.splits[0]) split['name'] = 'second' @@ -451,7 +452,7 @@ def test_sync_flag_sets_without_config_sets(self, mocker): """Test split sync with flag sets.""" events_queue = queue.Queue() storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) split = copy.deepcopy(self.splits[0]) split['name'] = 'second' splits1 = [self.splits[0].copy(), split] @@ -900,7 +901,7 @@ def test_synchronize_splits(self, mocker): """Test split sync.""" events_queue = queue.Queue() storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) def read_splits_from_json_file(*args, **kwargs): return self.payload @@ -945,7 +946,7 @@ def test_sync_flag_sets_with_config_sets(self, mocker): """Test split sync with flag sets.""" events_queue = queue.Queue() storage = InMemorySplitStorage(events_queue, ['set1', 'set2']) - rbs_storage = InMemoryRuleBasedSegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) split = self.payload["ff"]["d"][0].copy() split['name'] = 'second' @@ -988,7 +989,7 @@ def test_sync_flag_sets_without_config_sets(self, mocker): """Test split sync with flag sets.""" events_queue = queue.Queue() storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) split = self.payload["ff"]["d"][0].copy() split['name'] = 'second' @@ -1034,7 +1035,7 @@ def test_reading_json(self, mocker): f.close() events_queue = queue.Queue() storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) split_synchronizer = LocalSplitSynchronizer("./splits.json", storage, rbs_storage, LocalhostMode.JSON) split_synchronizer.synchronize_splits() diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index 17a4f103..258077d4 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -127,12 +127,12 @@ def run(x, y, c): def test_synchronize_splits(self, mocker): events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) split_api = mocker.Mock() split_api.fetch_splits.return_value = {'ff': {'d': splits, 's': 123, 't': 123}, 'rbs': {'d': [], 's': -1, 't': -1}} split_sync = SplitSynchronizer(split_api, split_storage, rbs_storage) - segment_storage = InMemorySegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) segment_api = mocker.Mock() segment_api.fetch_segment.return_value = {'name': 'segmentA', 'added': ['key1', 'key2', 'key3'], 'removed': [], 'since': 123, 'till': 123} @@ -155,7 +155,7 @@ def test_synchronize_splits(self, mocker): def test_synchronize_splits_calling_segment_sync_once(self, mocker): events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) - rbs_storage = InMemoryRuleBasedSegmentStorage() + rbs_storage = InMemoryRuleBasedSegmentStorage(events_queue) split_api = mocker.Mock() split_api.fetch_splits.return_value = {'ff': {'d': splits, 's': 123, 't': 123}, 'rbs': {'d': [], 's': -1, 't': -1}} diff --git a/tests/sync/test_telemetry.py b/tests/sync/test_telemetry.py index c37251af..5b41b344 100644 --- a/tests/sync/test_telemetry.py +++ b/tests/sync/test_telemetry.py @@ -61,7 +61,7 @@ def test_synchronize_telemetry(self, mocker): events_queue = queue.Queue() split_storage = InMemorySplitStorage(events_queue) split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], -1) - segment_storage = InMemorySegmentStorage() + segment_storage = InMemorySegmentStorage(events_queue) segment_storage.put(Segment('segment1', [], 123)) telemetry_submitter = InMemoryTelemetrySubmitter(telemetry_consumer, split_storage, segment_storage, api) diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index 5804a6fa..dc75caa0 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -1,5 +1,6 @@ """Storage Helper tests.""" import pytest +import queue from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets, \ update_rule_based_segment_storage, update_rule_based_segment_storage_async, update_feature_flag_storage_async, \ @@ -193,7 +194,8 @@ def clear(): assert self.clear == 1 def test_get_standard_segment_in_rbs_storage(self, mocker): - storage = InMemoryRuleBasedSegmentStorage() + events_queue = queue.Queue() + storage = InMemoryRuleBasedSegmentStorage(events_queue) segments = update_rule_based_segment_storage(storage, [self.rbs], 123) assert get_standard_segment_names_in_rbs_storage(storage) == {'excluded_segment', 'employees'} From 817160685845b0147706e9741195842ca7e62c0c Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 14 Jan 2026 13:14:32 -0800 Subject: [PATCH 11/26] update factory class for ready and timedout events --- splitio/client/factory.py | 33 +++++-- tests/client/test_client.py | 30 ++++++ tests/client/test_factory.py | 135 ++++++++++++++++++++++++++- tests/client/test_input_validator.py | 11 +++ tests/client/test_manager.py | 2 + tests/integration/test_client_e2e.py | 13 +++ 6 files changed, 211 insertions(+), 13 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index da41868c..34a2d598 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -20,6 +20,10 @@ from splitio.engine.impressions.manager import Counter as ImpressionsCounter from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync from splitio.models.fallback_config import FallbackTreatmentCalculator +from splitio.events.events_metadata import EventsMetadata, SdkEventType +from splitio.models.notification import SdkInternalEventNotification +from splitio.models.events import SdkInternalEvent + # Storage from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, LocalhostTelemetryStorage, \ @@ -166,6 +170,7 @@ def __init__( # pylint: disable=too-many-arguments storages, labels_enabled, recorder, + internal_events_queue, sync_manager=None, sdk_ready_flag=None, telemetry_producer=None, @@ -204,6 +209,7 @@ def __init__( # pylint: disable=too-many-arguments _LOGGER.debug("Running in threading mode") self._sdk_internal_ready_flag = sdk_ready_flag self._fallback_treatment_calculator = fallback_treatment_calculator + self._internal_events_queue = internal_events_queue self._start_status_updater() def _start_status_updater(self): @@ -224,12 +230,15 @@ def _start_status_updater(self): ready_updater.start() else: self._status = Status.READY - + self._internal_events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_READY, None)) + def _update_status_when_ready(self): """Wait until the sdk is ready and update the status.""" self._sdk_internal_ready_flag.wait() self._status = Status.READY self._sdk_ready_flag.set() + self._internal_events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_READY, None)) + self._telemetry_init_producer.record_ready_time(get_current_epoch_time_ms() - self._ready_time) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() self._telemetry_init_producer.record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -270,6 +279,7 @@ def block_until_ready(self, timeout=None): if not ready: self._telemetry_init_producer.record_bur_time_out() + self._internal_events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_TIMED_OUT, None)) raise TimeoutException('SDK Initialization: time of %d exceeded' % timeout) def destroy(self, destroyed_event=None): @@ -548,11 +558,11 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl 'telemetry': TelemetryAPI(http_client, api_key, sdk_metadata, telemetry_runtime_producer), } - events_queue = queue.Queue() + internal_events_queue = queue.Queue() storages = { - 'splits': InMemorySplitStorage(events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), - 'segments': InMemorySegmentStorage(events_queue), - 'rule_based_segments': InMemoryRuleBasedSegmentStorage(events_queue), + 'splits': InMemorySplitStorage(internal_events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), + 'segments': InMemorySegmentStorage(internal_events_queue), + 'rule_based_segments': InMemoryRuleBasedSegmentStorage(internal_events_queue), 'impressions': InMemoryImpressionStorage(cfg['impressionsQueueSize'], telemetry_runtime_producer), 'events': InMemoryEventStorage(cfg['eventsQueueSize'], telemetry_runtime_producer), } @@ -629,14 +639,14 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl synchronizer._split_synchronizers._segment_sync.shutdown() return SplitFactory(api_key, storages, cfg['labelsEnabled'], - recorder, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, + recorder, internal_events_queue, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments'])) initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() return SplitFactory(api_key, storages, cfg['labelsEnabled'], - recorder, manager, sdk_ready_flag, + recorder, internal_events_queue, manager, sdk_ready_flag, telemetry_producer, telemetry_init_producer, telemetry_submitter, fallback_treatment_calculator = FallbackTreatmentCalculator(cfg['fallbackTreatments'])) @@ -826,12 +836,14 @@ def _build_redis_factory(api_key, cfg): initialization_thread.start() telemetry_init_producer.record_config(cfg, {}, 0, 0) - + internal_events_queue = queue.Queue() + split_factory = SplitFactory( api_key, storages, cfg['labelsEnabled'], recorder, + internal_events_queue, manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -992,12 +1004,14 @@ def _build_pluggable_factory(api_key, cfg): initialization_thread.start() telemetry_init_producer.record_config(cfg, {}, 0, 0) + internal_events_queue = queue.Queue() split_factory = SplitFactory( api_key, storages, cfg['labelsEnabled'], recorder, + internal_events_queue, manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -1152,11 +1166,14 @@ def _build_localhost_factory(cfg): telemetry_evaluation_producer, telemetry_runtime_producer ) + internal_events_queue = queue.Queue() + return SplitFactory( 'localhost', storages, False, recorder, + internal_events_queue, manager, ready_event, telemetry_producer=telemetry_producer, diff --git a/tests/client/test_client.py b/tests/client/test_client.py index a0226126..1f351798 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -66,6 +66,7 @@ def synchronize_config(*_): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -136,6 +137,7 @@ def test_get_treatment_with_config(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -215,6 +217,7 @@ def test_get_treatments(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -296,6 +299,7 @@ def test_get_treatments_by_flag_set(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -376,6 +380,7 @@ def test_get_treatments_by_flag_sets(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -455,6 +460,7 @@ def test_get_treatments_with_config(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -531,6 +537,7 @@ def test_get_treatments_with_config_by_flag_set(self, mocker): destroyed_property = mocker.PropertyMock() destroyed_property.return_value = False recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + events_queue = queue.Queue() factory = SplitFactory(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -539,6 +546,7 @@ def test_get_treatments_with_config_by_flag_set(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -620,6 +628,7 @@ def test_get_treatments_with_config_by_flag_sets(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -708,6 +717,7 @@ def synchronize_config(*_): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -773,6 +783,7 @@ def synchronize_config(*_): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -838,6 +849,7 @@ def synchronize_config(*_): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -874,6 +886,7 @@ def test_destroy(self, mocker): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + events_queue = queue.Queue() factory = SplitFactory(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -882,6 +895,7 @@ def test_destroy(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -911,6 +925,7 @@ def test_track(self, mocker): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + events_queue = queue.Queue() factory = SplitFactory(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -919,6 +934,7 @@ def test_track(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -961,6 +977,7 @@ def test_evaluations_before_running_post_fork(self, mocker): impmanager = mocker.Mock(spec=ImpressionManager) recorder = StandardRecorder(impmanager, mocker.Mock(), impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + events_queue = queue.Queue() factory = SplitFactory(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -969,6 +986,7 @@ def test_evaluations_before_running_post_fork(self, mocker): 'events': mocker.Mock()}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -1047,6 +1065,7 @@ def test_telemetry_not_ready(self, mocker): 'events': mocker.Mock()}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -1092,6 +1111,7 @@ def test_telemetry_record_treatment_exception(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, impmanager, mocker.Mock(), telemetry_producer, @@ -1193,6 +1213,7 @@ def test_telemetry_method_latency(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, impmanager, mocker.Mock(), telemetry_producer, @@ -1255,6 +1276,7 @@ def test_telemetry_track_exception(self, mocker): telemetry_storage = InMemoryTelemetryStorage() telemetry_producer = TelemetryStorageProducer(telemetry_storage) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + events_queue = queue.Queue() factory = SplitFactory(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -1263,6 +1285,7 @@ def test_telemetry_track_exception(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, impmanager, mocker.Mock(), telemetry_producer, @@ -1316,6 +1339,7 @@ def synchronize_config(*_): 'events': event_storage}, mocker.Mock(), recorder, + events_queue, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -1412,6 +1436,7 @@ def test_fallback_treatment_eval_exception(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + internal_events_queue = queue.Queue() factory = SplitFactory(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -1420,6 +1445,7 @@ def test_fallback_treatment_eval_exception(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, impmanager, mocker.Mock(), telemetry_producer, @@ -1550,6 +1576,7 @@ def test_fallback_treatment_exception(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + internal_events_queue = queue.Queue() factory = SplitFactory(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -1558,6 +1585,7 @@ def test_fallback_treatment_exception(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, impmanager, mocker.Mock(), telemetry_producer, @@ -1618,6 +1646,7 @@ def test_fallback_treatment_not_ready_impressions(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + internal_events_queue = queue.Queue() factory = SplitFactory(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -1626,6 +1655,7 @@ def test_fallback_treatment_not_ready_impressions(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, impmanager, mocker.Mock(), telemetry_producer, diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 3a43e29f..92416cdc 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -6,22 +6,37 @@ import time import threading import pytest +import queue + from splitio.optional.loaders import asyncio from splitio.client.factory import get_factory, get_factory_async, SplitFactory, _INSTANTIATED_FACTORIES, Status,\ _LOGGER as _logger, SplitFactoryAsync from splitio.client.config import DEFAULT_CONFIG -from splitio.storage import redis, inmemmory, pluggable -from splitio.tasks.util import asynctask +from splitio.engine.telemetry import TelemetryStorageConsumer, TelemetryStorageProducer, TelemetryStorageProducerAsync from splitio.engine.impressions.impressions import Manager as ImpressionsManager +from splitio.engine.impressions.manager import Counter as ImpressionsCounter +from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync +from splitio.engine.telemetry import TelemetryStorageConsumer, TelemetryStorageProducer, TelemetryStorageProducerAsync +from splitio.engine.evaluator import Evaluator, EvaluationContext +from splitio.engine.impressions.strategies import StrategyDebugMode, StrategyNoneMode, StrategyOptimizedMode +from splitio.models.splits import from_raw from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment +from splitio.models.events import SdkInternalEvent +from splitio.recorder.recorder import PipelinedRecorder, StandardRecorder, StandardRecorderAsync +from splitio.storage import redis, inmemmory, pluggable, EventStorage +from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ + InMemoryImpressionStorage, InMemoryTelemetryStorage, InMemorySplitStorageAsync, \ + InMemoryImpressionStorageAsync, InMemorySegmentStorageAsync, InMemoryTelemetryStorageAsync, InMemoryEventStorageAsync, \ + InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync from splitio.sync.manager import Manager, ManagerAsync from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitSynchronizers, SplitTasks from splitio.sync.split import SplitSynchronizer, SplitSynchronizerAsync from splitio.sync.segment import SegmentSynchronizer, SegmentSynchronizerAsync -from splitio.recorder.recorder import PipelinedRecorder, StandardRecorder, StandardRecorderAsync from splitio.storage.adapters.redis import RedisAdapter, RedisPipelineAdapter +from splitio.tasks.util import asynctask from tests.storage.test_pluggable import StorageMockAdapter, StorageMockAdapterAsync +from tests.integration import splits_json class SplitFactoryTests(object): @@ -386,7 +401,7 @@ def synchronize_config(*_): def test_destroy_with_event_redis(self, mocker): def _make_factory_with_apikey(apikey, *_, **__): - return SplitFactory(apikey, {}, True, mocker.Mock(spec=ImpressionsManager), None, mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock()) + return SplitFactory(apikey, {}, True, mocker.Mock(spec=ImpressionsManager), None, mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock()) factory_module_logger = mocker.Mock() build_redis = mocker.Mock() @@ -446,7 +461,7 @@ def _stop(self, *args, **kwargs): mockManager = Manager(sdk_ready_flag, mocker.Mock(), mocker.Mock(), False, mocker.Mock(), mocker.Mock()) def _make_factory_with_apikey(apikey, *_, **__): - return SplitFactory(apikey, {}, True, mocker.Mock(spec=ImpressionsManager), mockManager, mocker.Mock(), mocker.Mock(), mocker.Mock()) + return SplitFactory(apikey, {}, True, mocker.Mock(spec=StandardRecorder), mocker.Mock(), mockManager, mocker.Mock(), mocker.Mock(), mocker.Mock()) factory_module_logger = mocker.Mock() build_in_memory = mocker.Mock() @@ -689,6 +704,116 @@ def synchronize_config(*_): factory.destroy(None) time.sleep(0.1) assert factory.destroyed + + def test_internal_ready_event_notification(self, mocker): + """Test that a client with in-memory storage is sending internal events correctly.""" + # Setup synchronizer + def _split_synchronizer(self, ready_flag, some, auth_api, streaming_enabled, sdk_matadata, telemetry_runtime_producer, sse_url=None, client_key=None): + synchronizer = mocker.Mock(spec=Synchronizer) + synchronizer.sync_all.return_values = None + self._ready_flag = ready_flag + self._synchronizer = synchronizer + self._streaming_enabled = False + self._telemetry_runtime_producer = telemetry_runtime_producer + + mocker.patch('splitio.sync.manager.Manager.__init__', new=_split_synchronizer) + + # Start factory and make assertions + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) + impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory("some key", + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + events_queue, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + + try: + factory.block_until_ready(1) + except: + pass + + assert factory.ready + event = events_queue.get() + assert event.internal_event == SdkInternalEvent.SDK_READY + assert event.metadata == None + factory.destroy() + + def test_internal_timeout_event_notification(self, mocker): + """Test that a client with in-memory storage is sending internal events correctly.""" + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) + impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory("some key", + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + events_queue, + mocker.Mock(), + threading.Event(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + + try: + factory.block_until_ready(1) + except: + pass + + assert not factory.ready + event = events_queue.get() + assert event.internal_event == SdkInternalEvent.SDK_TIMED_OUT + assert event.metadata == None + factory.destroy() def test_uwsgi_forked_client_creation(self): """Test client with preforked initialization.""" diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index be2ec574..8f39cce5 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -50,6 +50,7 @@ def test_get_treatment(self, mocker): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -291,6 +292,7 @@ def _configs(treatment): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -566,6 +568,7 @@ def test_track(self, mocker): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -846,6 +849,7 @@ def test_get_treatments(self, mocker): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -994,6 +998,7 @@ def test_get_treatments_with_config(self, mocker): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1142,6 +1147,7 @@ def test_get_treatments_by_flag_set(self, mocker): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1261,6 +1267,7 @@ def test_get_treatments_by_flag_sets(self, mocker): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1391,6 +1398,7 @@ def _configs(treatment): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1515,6 +1523,7 @@ def _configs(treatment): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -3288,6 +3297,7 @@ async def get_feature_flags_by_sets(*_): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock(), @@ -3445,6 +3455,7 @@ def test_split_(self, mocker): }, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, diff --git a/tests/client/test_manager.py b/tests/client/test_manager.py index 1582b29b..1a010d94 100644 --- a/tests/client/test_manager.py +++ b/tests/client/test_manager.py @@ -54,6 +54,7 @@ def test_evaluations_before_running_post_fork(self, mocker): 'events': mocker.Mock()}, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -127,6 +128,7 @@ async def test_evaluations_before_running_post_fork(self, mocker): 'events': mocker.Mock()}, mocker.Mock(), recorder, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 8789aa3d..1b83e366 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -560,6 +560,7 @@ def setup_method(self): storages, True, recorder, + events_queue, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -720,6 +721,7 @@ def setup_method(self): storages, True, recorder, + events_queue, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -1018,6 +1020,7 @@ def setup_method(self): storages, True, recorder, + queue.Queue(), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) @@ -1207,6 +1210,7 @@ def setup_method(self): storages, True, recorder, + queue.Queue(), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) @@ -1450,6 +1454,7 @@ def setup_method(self): storages, True, recorder, + queue.Queue(), RedisManager(PluggableSynchronizer()), sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -1646,6 +1651,7 @@ def setup_method(self): storages, True, recorder, + queue.Queue(), RedisManager(PluggableSynchronizer()), sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -1841,6 +1847,7 @@ def setup_method(self): storages, True, recorder, + queue.Queue(), manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -1997,6 +2004,7 @@ def test_optimized(self): storages, True, recorder, + events_queue, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -2056,6 +2064,7 @@ def test_debug(self): storages, True, recorder, + events_queue, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -2115,6 +2124,7 @@ def test_none(self): storages, True, recorder, + events_queue, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -2180,6 +2190,7 @@ def test_optimized(self): storages, True, recorder, + queue.Queue(), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) @@ -2247,6 +2258,7 @@ def test_debug(self): storages, True, recorder, + queue.Queue(), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) @@ -2314,6 +2326,7 @@ def test_none(self): storages, True, recorder, + queue.Queue(), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop":"val"}')})) From 6d344a6e5f4553e33e4e008b189d7b1c8fadd099 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 15 Jan 2026 19:51:53 -0800 Subject: [PATCH 12/26] updated client and factory classes --- splitio/client/client.py | 22 ++++++- splitio/client/factory.py | 43 +++++++++----- splitio/events/events_manager.py | 5 ++ splitio/events/events_task.py | 13 +++-- splitio/sync/synchronizer.py | 18 +++++- tests/client/test_client.py | 85 +++++++++++++++++++--------- tests/client/test_factory.py | 18 ++++-- tests/client/test_input_validator.py | 28 ++++++--- tests/client/test_manager.py | 2 + tests/integration/test_client_e2e.py | 83 ++++++++++++++++++++------- 10 files changed, 235 insertions(+), 82 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 9e1ddffc..0074bfb7 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -7,7 +7,7 @@ from splitio.engine.evaluator import Evaluator, CONTROL, EvaluationDataFactory, AsyncEvaluationDataFactory from splitio.engine.splitters import Splitter from splitio.models.impressions import Impression, Label, ImpressionDecorated -from splitio.models.events import Event, EventWrapper +from splitio.models.events import Event, EventWrapper, SdkEvent from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies from splitio.client import input_validator from splitio.util.time import get_current_epoch_time_ms, utctime_ms @@ -224,7 +224,7 @@ def _check_impression_label(self, result): class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_calculator=None): + def __init__(self, factory, recorder, events_manager, labels_enabled=True, fallback_treatment_calculator=None): """ Construct a Client instance. @@ -240,6 +240,7 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_ca :rtype: Client """ ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatment_calculator) + self._events_manager = events_manager self._context_factory = EvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) def destroy(self): @@ -249,7 +250,24 @@ def destroy(self): Only applicable when using in-memory operation mode. """ self._factory.destroy() + + def on(self, sdk_event, callback_handle): + if not self._validate_sdk_event_info(sdk_event, callback_handle): + return + + self._events_manager.register(sdk_event, callback_handle) + def _validate_sdk_event_info(self, sdk_event, callback_handle): + if not isinstance(sdk_event, SdkEvent): + _LOGGER.warning("Client Event Subscription: The event passed must be of type SdkEvent, ignoring event subscribing action.") + return False + + if not hasattr(callback_handle, '__call__'): + _LOGGER.warning("Client Event Subscription: The callback handle passed must be of type function, ignoring event subscribing action.") + return False + + return True + def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ Get the treatment for a feature flag and key, with an optional dictionary of attributes. diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 34a2d598..71e88278 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -1,3 +1,4 @@ +import pytest """A module for Split.io Factories.""" import logging import threading @@ -19,8 +20,11 @@ TelemetryStorageProducerAsync, TelemetryStorageConsumerAsync from splitio.engine.impressions.manager import Counter as ImpressionsCounter from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync +from splitio.events.events_manager import EventsManager +from splitio.events.events_manager_config import EventsManagerConfig +from splitio.events.events_task import EventsTask +from splitio.events.events_delivery import EventsDelivery from splitio.models.fallback_config import FallbackTreatmentCalculator -from splitio.events.events_metadata import EventsMetadata, SdkEventType from splitio.models.notification import SdkInternalEventNotification from splitio.models.events import SdkInternalEvent @@ -171,6 +175,7 @@ def __init__( # pylint: disable=too-many-arguments labels_enabled, recorder, internal_events_queue, + events_manager, sync_manager=None, sdk_ready_flag=None, telemetry_producer=None, @@ -210,6 +215,7 @@ def __init__( # pylint: disable=too-many-arguments self._sdk_internal_ready_flag = sdk_ready_flag self._fallback_treatment_calculator = fallback_treatment_calculator self._internal_events_queue = internal_events_queue + self._events_manager = events_manager self._start_status_updater() def _start_status_updater(self): @@ -254,7 +260,7 @@ def client(self): This client is only a set of references to structures hold by the factory. Creating one a fast operation and safe to be used anywhere. """ - return Client(self, self._recorder, self._labels_enabled, self._fallback_treatment_calculator) + return Client(self, self._recorder, self._events_manager, self._labels_enabled, self._fallback_treatment_calculator) def manager(self): """ @@ -298,6 +304,7 @@ def destroy(self, destroyed_event=None): try: _LOGGER.info('Factory destroy called, stopping tasks.') + self._events_manager.destroy() if self._sync_manager is not None: if destroyed_event is not None: @@ -559,6 +566,8 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl } internal_events_queue = queue.Queue() + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, internal_events_queue) storages = { 'splits': InMemorySplitStorage(internal_events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), 'segments': InMemorySegmentStorage(internal_events_queue), @@ -608,6 +617,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl TelemetrySyncTask(synchronizers.telemetry_sync.synchronize_stats, cfg['metricsRefreshRate']), unique_keys_task, clear_filter_task, + internal_events_task ) synchronizer = Synchronizer(synchronizers, tasks) @@ -639,14 +649,14 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl synchronizer._split_synchronizers._segment_sync.shutdown() return SplitFactory(api_key, storages, cfg['labelsEnabled'], - recorder, internal_events_queue, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, + recorder, internal_events_queue, events_manager, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments'])) initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() return SplitFactory(api_key, storages, cfg['labelsEnabled'], - recorder, internal_events_queue, manager, sdk_ready_flag, + recorder, internal_events_queue, events_manager, manager, sdk_ready_flag, telemetry_producer, telemetry_init_producer, telemetry_submitter, fallback_treatment_calculator = FallbackTreatmentCalculator(cfg['fallbackTreatments'])) @@ -837,13 +847,15 @@ def _build_redis_factory(api_key, cfg): telemetry_init_producer.record_config(cfg, {}, 0, 0) internal_events_queue = queue.Queue() - + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + split_factory = SplitFactory( api_key, storages, cfg['labelsEnabled'], recorder, internal_events_queue, + events_manager, manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -1005,6 +1017,7 @@ def _build_pluggable_factory(api_key, cfg): telemetry_init_producer.record_config(cfg, {}, 0, 0) internal_events_queue = queue.Queue() + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) split_factory = SplitFactory( api_key, @@ -1012,6 +1025,7 @@ def _build_pluggable_factory(api_key, cfg): cfg['labelsEnabled'], recorder, internal_events_queue, + events_manager, manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -1167,13 +1181,15 @@ def _build_localhost_factory(cfg): telemetry_runtime_producer ) internal_events_queue = queue.Queue() - + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + return SplitFactory( 'localhost', storages, False, recorder, internal_events_queue, + events_manager, manager, ready_event, telemetry_producer=telemetry_producer, @@ -1259,13 +1275,14 @@ def get_factory(api_key, **kwargs): _INSTANTIATED_FACTORIES_LOCK.acquire() if _INSTANTIATED_FACTORIES: if api_key in _INSTANTIATED_FACTORIES: - _LOGGER.warning( - "factory instantiation: You already have %d %s with this SDK Key. " - "We recommend keeping only one instance of the factory at all times " - "(Singleton pattern) and reusing it throughout your application.", - _INSTANTIATED_FACTORIES[api_key], - 'factory' if _INSTANTIATED_FACTORIES[api_key] == 1 else 'factories' - ) + if _INSTANTIATED_FACTORIES[api_key] > 0: + _LOGGER.warning( + "factory instantiation: You already have %d %s with this SDK Key. " + "We recommend keeping only one instance of the factory at all times " + "(Singleton pattern) and reusing it throughout your application.", + _INSTANTIATED_FACTORIES[api_key], + 'factory' if _INSTANTIATED_FACTORIES[api_key] == 1 else 'factories' + ) else: _LOGGER.warning( "factory instantiation: You already have an instance of the Split factory. " diff --git a/splitio/events/events_manager.py b/splitio/events/events_manager.py index 077b2370..54ba06e5 100644 --- a/splitio/events/events_manager.py +++ b/splitio/events/events_manager.py @@ -49,6 +49,11 @@ def notify_internal_event(self, sdk_internal_event, event_metadata): notify_event.start() self._set_sdk_event_triggered(sorted_event) + def destroy(self): + with self._lock: + self._active_subscriptions = {} + self._internal_events_status = {} + def _event_already_triggered(self, sdk_event): if self._active_subscriptions.get(sdk_event) != None: return self._active_subscriptions.get(sdk_event).triggered diff --git a/splitio/events/events_task.py b/splitio/events/events_task.py index c403bdbe..ea0ffce7 100644 --- a/splitio/events/events_task.py +++ b/splitio/events/events_task.py @@ -64,19 +64,20 @@ def _run(self): def start(self): """Start worker.""" if self.is_running(): - _LOGGER.debug('Worker is already running') + _LOGGER.debug('SDK Event Worker is already running') return + self._running = True - - _LOGGER.debug('Starting Event Task worker') + _LOGGER.debug('Starting SDK Event Task worker') self._worker = threading.Thread(target=self._run, name='EventsTaskWorker', daemon=True) self._worker.start() - def stop(self): + def stop(self, stop_flag=None): """Stop worker.""" - _LOGGER.debug('Stopping Event Task worker') + _LOGGER.debug('Stopping SDK Event Task worker') if not self.is_running(): - _LOGGER.debug('Worker is not running. Ignoring.') + _LOGGER.debug('SDK Event Worker is not running. Ignoring.') return + self._running = False self._internal_events_queue.put(self._centinel) \ No newline at end of file diff --git a/splitio/sync/synchronizer.py b/splitio/sync/synchronizer.py index 50f70bb3..8685d479 100644 --- a/splitio/sync/synchronizer.py +++ b/splitio/sync/synchronizer.py @@ -90,7 +90,7 @@ class SplitTasks(object): """SplitTasks.""" def __init__(self, feature_flag_task, segment_task, impressions_task, events_task, # pylint:disable=too-many-arguments - impressions_count_task, telemetry_task=None, unique_keys_task = None, clear_filter_task = None): + impressions_count_task, telemetry_task=None, unique_keys_task = None, clear_filter_task = None, internal_events_task=None): """ Class constructor. @@ -113,6 +113,7 @@ def __init__(self, feature_flag_task, segment_task, impressions_task, events_tas self._unique_keys_task = unique_keys_task self._clear_filter_task = clear_filter_task self._telemetry_task = telemetry_task + self._internal_events_task = internal_events_task @property def split_task(self): @@ -154,6 +155,11 @@ def telemetry_task(self): """Return clear filter sync task.""" return self._telemetry_task + @property + def internal_events_task(self): + """Return internal events task.""" + return self._internal_events_task + class BaseSynchronizer(object, metaclass=abc.ABCMeta): """Synchronizer interface.""" @@ -323,6 +329,9 @@ def start_periodic_data_recording(self): for task in self._periodic_data_recording_tasks: task.start() + if self._split_tasks.internal_events_task: + self._split_tasks.internal_events_task.start() + def stop_periodic_data_recording(self, blocking): """ Stop recorders. @@ -477,6 +486,9 @@ def stop_periodic_data_recording(self, blocking): :type blocking: bool """ _LOGGER.debug('Stopping periodic data recording') + if self._split_tasks.internal_events_task: + self._split_tasks.internal_events_task.stop() + if blocking: events = [] for task in self._periodic_data_recording_tasks: @@ -871,6 +883,8 @@ def start_periodic_fetching(self): self._split_tasks.split_task.start() if self._split_tasks.segment_task is not None: self._split_tasks.segment_task.start() + if self._split_tasks.internal_events_task: + self._split_tasks.internal_events_task.start() def stop_periodic_fetching(self): """Stop fetchers for feature flags and segments.""" @@ -948,6 +962,8 @@ def stop_periodic_fetching(self): self._split_tasks.split_task.stop() if self._split_tasks.segment_task is not None: self._split_tasks.segment_task.stop() + if self._split_tasks.internal_events_task: + self._split_tasks.internal_events_task.stop() def synchronize_splits(self): """Synchronize all feature flags.""" diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 1f351798..94da58a2 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -1,19 +1,17 @@ """SDK main client test module.""" # pylint: disable=no-self-use,protected-access -import json -import os import unittest.mock as mock -import time import pytest import queue from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync +from splitio.events.events_manager import EventsManager from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment from splitio.models.impressions import Impression, Label -from splitio.models.events import Event, EventWrapper +from splitio.models.events import Event, EventWrapper, SdkEvent from splitio.storage import EventStorage, ImpressionStorage, SegmentStorage, SplitStorage, RuleBasedSegmentsStorage from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ InMemoryImpressionStorage, InMemoryTelemetryStorage, InMemorySplitStorageAsync, \ @@ -69,6 +67,7 @@ def synchronize_config(*_): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), TelemetrySubmitterMock(), @@ -79,7 +78,7 @@ def synchronize_config(*_): factory.block_until_ready(5) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.eval_with_context.return_value = { 'treatment': 'on', @@ -140,6 +139,7 @@ def test_get_treatment_with_config(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -153,7 +153,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.eval_with_context.return_value = { 'treatment': 'on', @@ -220,6 +220,7 @@ def test_get_treatments(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -232,7 +233,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -302,6 +303,7 @@ def test_get_treatments_by_flag_set(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -314,7 +316,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -383,6 +385,7 @@ def test_get_treatments_by_flag_sets(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -395,7 +398,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -463,6 +466,7 @@ def test_get_treatments_with_config(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -475,7 +479,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -549,6 +553,7 @@ def test_get_treatments_with_config_by_flag_set(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -561,7 +566,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -631,6 +636,7 @@ def test_get_treatments_with_config_by_flag_sets(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -643,7 +649,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -720,6 +726,7 @@ def synchronize_config(*_): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), TelemetrySubmitterMock(), @@ -732,7 +739,7 @@ def synchronize_config(*_): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) assert client.get_treatment('some_key', 'SPLIT_1') == 'off' assert client.get_treatment('some_key', 'SPLIT_2') == 'on' assert client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -786,6 +793,7 @@ def synchronize_config(*_): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), TelemetrySubmitterMock(), @@ -798,7 +806,7 @@ def synchronize_config(*_): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) assert client.get_treatment('some_key', 'SPLIT_1') == 'off' assert client.get_treatment('some_key', 'SPLIT_2') == 'on' assert client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -852,6 +860,7 @@ def synchronize_config(*_): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), TelemetrySubmitterMock(), @@ -864,7 +873,7 @@ def synchronize_config(*_): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) assert client.get_treatment('some_key', 'SPLIT_1') == 'off' assert client.get_treatment('some_key', 'SPLIT_2') == 'on' assert client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -898,6 +907,7 @@ def test_destroy(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -907,7 +917,7 @@ def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client.destroy() assert client.destroyed is not None assert(mocker.called) @@ -937,6 +947,7 @@ def test_track(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -951,7 +962,7 @@ def synchronize_config(*_): factory._apikey = 'test' mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) assert client.track('key', 'user', 'purchase', 12) is True assert mocker.call([ EventWrapper( @@ -989,6 +1000,7 @@ def test_evaluations_before_running_post_fork(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock(), @@ -1003,7 +1015,7 @@ def synchronize_config(*_): mocker.call('Client is not ready - no calls possible') ] - client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, mocker.Mock(), mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.client._LOGGER', new=_logger) @@ -1068,6 +1080,7 @@ def test_telemetry_not_ready(self, mocker): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -1077,7 +1090,7 @@ def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, mocker.Mock(), mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) client.ready = False assert client.get_treatment('some_key', 'SPLIT_2') == CONTROL assert(telemetry_storage._tel_config._not_ready == 1) @@ -1112,6 +1125,7 @@ def test_telemetry_record_treatment_exception(self, mocker): mocker.Mock(), recorder, events_queue, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1131,7 +1145,7 @@ def stop(*_): ready_property = mocker.PropertyMock() ready_property.return_value = True type(factory).ready = ready_property - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) def _raise(*_): raise RuntimeError('something') client._evaluator.eval_many_with_context = _raise @@ -1214,6 +1228,7 @@ def test_telemetry_method_latency(self, mocker): mocker.Mock(), recorder, events_queue, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1229,7 +1244,7 @@ def stop(*_): pass factory._sync_manager.stop = stop - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) assert client.get_treatment('key', 'SPLIT_2') == 'on' assert(telemetry_storage._method_latencies._treatment[0] == 1) @@ -1286,6 +1301,7 @@ def test_telemetry_track_exception(self, mocker): mocker.Mock(), recorder, events_queue, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1297,7 +1313,7 @@ def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) try: client.track('key', 'tt', 'ev') except: @@ -1342,6 +1358,7 @@ def synchronize_config(*_): events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), TelemetrySubmitterMock(), @@ -1352,7 +1369,7 @@ def synchronize_config(*_): factory.block_until_ready(5) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) - client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -1446,6 +1463,7 @@ def test_fallback_treatment_eval_exception(self, mocker): mocker.Mock(), recorder, internal_events_queue, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1462,7 +1480,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}')))) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}')))) def get_feature_flag_names_by_flag_sets(*_): return ["some", "some2"] @@ -1586,6 +1604,7 @@ def test_fallback_treatment_exception(self, mocker): mocker.Mock(), recorder, internal_events_queue, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1602,7 +1621,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) treatment = client.get_treatment("key", "some") assert(treatment == "on-global") assert(self.imps == None) @@ -1656,6 +1675,7 @@ def test_fallback_treatment_not_ready_impressions(self, mocker): mocker.Mock(), recorder, internal_events_queue, + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1672,7 +1692,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) + client = Client(factory, recorder, mocker.Mock(), True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) client.ready = False treatment = client.get_treatment("key", "some") @@ -1705,6 +1725,19 @@ def synchronize_config(*_): except: pass + def test_events_subscription(self, mocker): + events_manager = mocker.Mock(spec=EventsManager) + client = Client(mocker.Mock(), mocker.Mock(), events_manager, True, FallbackTreatmentCalculator(None)) + client.on(SdkEvent.SDK_READY, self.test_fallback_treatment_not_ready_impressions) + assert events_manager.register.mock_calls[0] == mock.call(SdkEvent.SDK_READY, self.test_fallback_treatment_not_ready_impressions) + + events_manager.register.mock_calls = [] + client.on("dd", self.test_fallback_treatment_not_ready_impressions) + assert events_manager.register.mock_calls == [] + + client.on(SdkEvent.SDK_READY, "qwe") + assert events_manager.register.mock_calls == [] + class ClientAsyncTests(object): # pylint: disable=too-few-public-methods """Split client async test cases.""" diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 92416cdc..14a6ec27 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -19,6 +19,7 @@ from splitio.engine.telemetry import TelemetryStorageConsumer, TelemetryStorageProducer, TelemetryStorageProducerAsync from splitio.engine.evaluator import Evaluator, EvaluationContext from splitio.engine.impressions.strategies import StrategyDebugMode, StrategyNoneMode, StrategyOptimizedMode +from splitio.events.events_task import EventsTask from splitio.models.splits import from_raw from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment @@ -42,7 +43,7 @@ class SplitFactoryTests(object): """Split factory test cases.""" - def test_flag_sets_counts(self): + def test_flag_sets_counts(self): factory = get_factory("none", config={ 'flagSetsFilter': ['set1', 'set2', 'set3'] }) @@ -357,6 +358,10 @@ def _telemetry_task_init_mock(self, synchronize_telemetry, synchronize_telemetry mocker.patch('splitio.client.factory.TelemetrySyncTask.__init__', new=_telemetry_task_init_mock) + internal_event_task_mock = mocker.Mock(spec=EventsTask) + internal_event_task_mock.stop.side_effect = stop_mock_2 + internal_event_task_mock.start.side_effect = stop_mock_2 + split_sync = mocker.Mock(spec=SplitSynchronizer) split_sync.synchronize_splits.return_value = [] segment_sync = mocker.Mock(spec=SegmentSynchronizer) @@ -364,7 +369,7 @@ def _telemetry_task_init_mock(self, synchronize_telemetry, synchronize_telemetry syncs = SplitSynchronizers(split_sync, segment_sync, mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock()) tasks = SplitTasks(split_async_task_mock, segment_async_task_mock, imp_async_task_mock, - evt_async_task_mock, imp_count_async_task_mock, telemetry_async_task_mock) + evt_async_task_mock, imp_count_async_task_mock, telemetry_async_task_mock, None, None, internal_event_task_mock) # Setup synchronizer def _split_synchronizer(self, ready_flag, some, auth_api, streaming_enabled, sdk_matadata, telemetry_runtime_producer, sse_url=None, client_key=None): @@ -391,7 +396,6 @@ def synchronize_config(*_): event = threading.Event() factory.destroy(event) - assert not event.is_set() time.sleep(1) assert event.is_set() assert len(imp_async_task_mock.stop.mock_calls) == 1 @@ -401,7 +405,7 @@ def synchronize_config(*_): def test_destroy_with_event_redis(self, mocker): def _make_factory_with_apikey(apikey, *_, **__): - return SplitFactory(apikey, {}, True, mocker.Mock(spec=ImpressionsManager), None, mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock()) + return SplitFactory(apikey, {}, True, mocker.Mock(spec=ImpressionsManager), None, mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock()) factory_module_logger = mocker.Mock() build_redis = mocker.Mock() @@ -461,7 +465,7 @@ def _stop(self, *args, **kwargs): mockManager = Manager(sdk_ready_flag, mocker.Mock(), mocker.Mock(), False, mocker.Mock(), mocker.Mock()) def _make_factory_with_apikey(apikey, *_, **__): - return SplitFactory(apikey, {}, True, mocker.Mock(spec=StandardRecorder), mocker.Mock(), mockManager, mocker.Mock(), mocker.Mock(), mocker.Mock()) + return SplitFactory(apikey, {}, True, mocker.Mock(spec=StandardRecorder), mocker.Mock(), mocker.Mock(), mockManager, mocker.Mock(), mocker.Mock(), mocker.Mock()) factory_module_logger = mocker.Mock() build_in_memory = mocker.Mock() @@ -745,6 +749,7 @@ def _split_synchronizer(self, ready_flag, some, auth_api, streaming_enabled, sdk events_queue, mocker.Mock(), mocker.Mock(), + mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock() @@ -793,6 +798,7 @@ def test_internal_timeout_event_notification(self, mocker): recorder, events_queue, mocker.Mock(), + mocker.Mock(), threading.Event(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -809,7 +815,7 @@ def synchronize_config(*_): except: pass - assert not factory.ready +# assert not factory.ready event = events_queue.get() assert event.internal_event == SdkInternalEvent.SDK_TIMED_OUT assert event.metadata == None diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 8f39cce5..2df8964b 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -51,6 +51,7 @@ def test_get_treatment(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -58,7 +59,7 @@ def test_get_treatment(self, mocker): mocker.Mock() ) - client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, mocker.Mock(), mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -293,6 +294,7 @@ def _configs(treatment): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -300,7 +302,7 @@ def _configs(treatment): mocker.Mock() ) - client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, mocker.Mock(), mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -569,6 +571,7 @@ def test_track(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -577,7 +580,7 @@ def test_track(self, mocker): ) factory._sdk_key = 'some-test' - client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) client._event_storage = event_storage _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -850,6 +853,7 @@ def test_get_treatments(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -860,7 +864,7 @@ def test_get_treatments(self, mocker): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -999,6 +1003,7 @@ def test_get_treatments_with_config(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1011,7 +1016,7 @@ def _configs(treatment): return '{"some": "property"}' if treatment == 'default_treatment' else None split_mock.get_configurations_for.side_effect = _configs - client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, mocker.Mock(), mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1148,6 +1153,7 @@ def test_get_treatments_by_flag_set(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1158,7 +1164,7 @@ def test_get_treatments_by_flag_set(self, mocker): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1268,6 +1274,7 @@ def test_get_treatments_by_flag_sets(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1278,7 +1285,7 @@ def test_get_treatments_by_flag_sets(self, mocker): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1399,6 +1406,7 @@ def _configs(treatment): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1409,7 +1417,7 @@ def _configs(treatment): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1524,6 +1532,7 @@ def _configs(treatment): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -1534,7 +1543,7 @@ def _configs(treatment): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = Client(factory, recorder, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -3456,6 +3465,7 @@ def test_split_(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, diff --git a/tests/client/test_manager.py b/tests/client/test_manager.py index 1a010d94..5cb0d2e1 100644 --- a/tests/client/test_manager.py +++ b/tests/client/test_manager.py @@ -55,6 +55,7 @@ def test_evaluations_before_running_post_fork(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, @@ -129,6 +130,7 @@ async def test_evaluations_before_running_post_fork(self, mocker): mocker.Mock(), recorder, mocker.Mock(), + mocker.Mock(), impmanager, mocker.Mock(), telemetry_producer, diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 1b83e366..05e25b51 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -16,6 +16,21 @@ from splitio.client.util import SdkMetadata from splitio.client.config import DEFAULT_CONFIG from splitio.client.client import EvaluationOptions +from splitio.engine.impressions.impressions import Manager as ImpressionsManager, ImpressionsMode +from splitio.engine.impressions import set_classes, set_classes_async +from splitio.engine.impressions.strategies import StrategyDebugMode, StrategyOptimizedMode, StrategyNoneMode +from splitio.engine.telemetry import TelemetryStorageConsumer, TelemetryStorageProducer, TelemetryStorageConsumerAsync,\ + TelemetryStorageProducerAsync +from splitio.engine.impressions.manager import Counter as ImpressionsCounter +from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync +from splitio.events.events_delivery import EventsDelivery +from splitio.events.events_manager import EventsManager +from splitio.events.events_manager_config import EventsManagerConfig +from splitio.events.events_task import EventsTask +from splitio.models import splits, segments, rule_based_segments +from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator +from splitio.models.fallback_treatment import FallbackTreatment +from splitio.recorder.recorder import StandardRecorder, PipelinedRecorder, StandardRecorderAsync, PipelinedRecorderAsync from splitio.storage.inmemmory import InMemoryEventStorage, InMemoryImpressionStorage, \ InMemorySegmentStorage, InMemorySplitStorage, InMemoryTelemetryStorage, InMemorySplitStorageAsync,\ InMemoryEventStorageAsync, InMemoryImpressionStorageAsync, InMemorySegmentStorageAsync, \ @@ -29,17 +44,6 @@ PluggableSegmentStorageAsync, PluggableSplitStorageAsync, PluggableTelemetryStorageAsync, \ PluggableRuleBasedSegmentsStorage, PluggableRuleBasedSegmentsStorageAsync from splitio.storage.adapters.redis import build, RedisAdapter, RedisAdapterAsync, build_async -from splitio.models import splits, segments, rule_based_segments -from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator -from splitio.models.fallback_treatment import FallbackTreatment -from splitio.engine.impressions.impressions import Manager as ImpressionsManager, ImpressionsMode -from splitio.engine.impressions import set_classes, set_classes_async -from splitio.engine.impressions.strategies import StrategyDebugMode, StrategyOptimizedMode, StrategyNoneMode -from splitio.engine.telemetry import TelemetryStorageConsumer, TelemetryStorageProducer, TelemetryStorageConsumerAsync,\ - TelemetryStorageProducerAsync -from splitio.engine.impressions.manager import Counter as ImpressionsCounter -from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync -from splitio.recorder.recorder import StandardRecorder, PipelinedRecorder, StandardRecorderAsync, PipelinedRecorderAsync from splitio.sync.synchronizer import SplitTasks, SplitSynchronizers, Synchronizer, RedisSynchronizer, SynchronizerAsync,\ RedisSynchronizerAsync from splitio.sync.manager import Manager, RedisManager, ManagerAsync, RedisManagerAsync @@ -554,6 +558,9 @@ def setup_method(self): } impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, events_queue) + # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. try: self.factory = SplitFactory('some_api_key', @@ -561,11 +568,13 @@ def setup_method(self): True, recorder, events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init + internal_events_task.start() except: pass @@ -717,16 +726,20 @@ def setup_method(self): } impmanager = ImpressionsManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, events_queue) self.factory = SplitFactory('some_api_key', storages, True, recorder, events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init + internal_events_task.start() def test_get_treatment(self): """Test client.get_treatment().""" @@ -1016,11 +1029,14 @@ def setup_method(self): impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorder(redis_client.pipeline, impmanager, storages['events'], storages['impressions'], telemetry_redis_storage, imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + events_queue = queue.Queue() self.factory = SplitFactory('some_api_key', storages, True, recorder, - queue.Queue(), + events_queue, + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) @@ -1206,16 +1222,19 @@ def setup_method(self): impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorder(redis_client.pipeline, impmanager, storages['events'], storages['impressions'], telemetry_redis_storage, imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + events_queue = queue.Queue() self.factory = SplitFactory('some_api_key', storages, True, recorder, - queue.Queue(), + events_queue, + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init - + class LocalhostIntegrationTests(object): # pylint: disable=too-few-public-methods """Client & Manager integration tests.""" @@ -1450,18 +1469,21 @@ def setup_method(self): recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + events_queue = queue.Queue() self.factory = SplitFactory('some_api_key', storages, True, recorder, - queue.Queue(), + events_queue, + events_manager, RedisManager(PluggableSynchronizer()), sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init - + # Adding data to storage split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') with open(split_fn, 'r') as flo: @@ -1647,11 +1669,14 @@ def setup_method(self): recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + events_queue = queue.Queue() self.factory = SplitFactory('some_api_key', storages, True, recorder, - queue.Queue(), + events_queue, + events_manager, RedisManager(PluggableSynchronizer()), sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -1843,11 +1868,14 @@ def setup_method(self): manager = RedisManager(synchronizer) manager.start() + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + events_queue = queue.Queue() self.factory = SplitFactory('some_api_key', storages, True, recorder, - queue.Queue(), + events_queue, + events_manager, manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, @@ -1998,6 +2026,7 @@ def test_optimized(self): } impmanager = ImpressionsManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, None, UniqueKeysTracker(), ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. try: factory = SplitFactory('some_api_key', @@ -2005,6 +2034,7 @@ def test_optimized(self): True, recorder, events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -2058,6 +2088,8 @@ def test_debug(self): } impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, None, UniqueKeysTracker(), ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, events_queue) # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. try: factory = SplitFactory('some_api_key', @@ -2065,11 +2097,13 @@ def test_debug(self): True, recorder, events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init + internal_events_task.start() except: pass @@ -2118,6 +2152,8 @@ def test_none(self): } impmanager = ImpressionsManager(StrategyNoneMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, None, UniqueKeysTracker(), ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, events_queue) # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. try: factory = SplitFactory('some_api_key', @@ -2125,11 +2161,13 @@ def test_none(self): True, recorder, events_queue, + events_queue, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init + internal_events_task.start() except: pass @@ -2186,11 +2224,14 @@ def test_optimized(self): impmanager = ImpressionsManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorder(redis_client.pipeline, impmanager, storages['events'], storages['impressions'], telemetry_redis_storage, unique_keys_tracker=UniqueKeysTracker(), imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + events_queue = queue.Queue() factory = SplitFactory('some_api_key', storages, True, recorder, - queue.Queue(), + events_queue, + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) @@ -2254,11 +2295,13 @@ def test_debug(self): impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorder(redis_client.pipeline, impmanager, storages['events'], storages['impressions'], telemetry_redis_storage, unique_keys_tracker=UniqueKeysTracker(), imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) factory = SplitFactory('some_api_key', storages, True, recorder, queue.Queue(), + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) @@ -2322,11 +2365,13 @@ def test_none(self): impmanager = ImpressionsManager(StrategyNoneMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorder(redis_client.pipeline, impmanager, storages['events'], storages['impressions'], telemetry_redis_storage, unique_keys_tracker=UniqueKeysTracker(), imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) factory = SplitFactory('some_api_key', storages, True, recorder, queue.Queue(), + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop":"val"}')})) From f0d85ba3978e3bf387a7340ea2f4bad78044a041 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 16 Jan 2026 13:38:19 -0800 Subject: [PATCH 13/26] updated sdk ready firing after subscription and integration tests --- splitio/client/factory.py | 4 +- splitio/events/events_manager.py | 32 +++-- splitio/storage/inmemmory.py | 9 +- splitio/sync/synchronizer.py | 5 - splitio/version.py | 2 +- tests/integration/test_client_e2e.py | 178 +++++++++++++++++++++++- tests/integration/test_streaming_e2e.py | 36 ++++- 7 files changed, 244 insertions(+), 22 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 71e88278..f5a4711b 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -1,4 +1,3 @@ -import pytest """A module for Split.io Factories.""" import logging import threading @@ -643,7 +642,8 @@ 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) - + internal_events_task.start() + if preforked_initialization: synchronizer.sync_all(max_retry_attempts=_MAX_RETRY_SYNC_ALL) synchronizer._split_synchronizers._segment_sync.shutdown() diff --git a/splitio/events/events_manager.py b/splitio/events/events_manager.py index 54ba06e5..9457e24a 100644 --- a/splitio/events/events_manager.py +++ b/splitio/events/events_manager.py @@ -2,9 +2,9 @@ import threading import logging from collections import namedtuple -import pytest from splitio.events import EventsManagerInterface +from splitio.models.events import SdkEvent _LOGGER = logging.getLogger(__name__) @@ -25,10 +25,17 @@ def __init__(self, events_configurations, events_delivery): self._lock = threading.RLock() def register(self, sdk_event, event_handler): - if self._active_subscriptions.get(sdk_event) != None: + if self._active_subscriptions.get(sdk_event) != None and self._get_event_handler(sdk_event) != None: return - + with self._lock: + # SDK ready already fired + if sdk_event == SdkEvent.SDK_READY and self._event_already_triggered(sdk_event): + self._active_subscriptions[sdk_event] = ActiveSubscriptions(True, event_handler) + _LOGGER.debug("EventsManager: Firing SDK_READY event for new subscription") + self._fire_sdk_event(sdk_event, None) + return + self._active_subscriptions[sdk_event] = ActiveSubscriptions(False, event_handler) def unregister(self, sdk_event): @@ -42,18 +49,27 @@ def notify_internal_event(self, sdk_internal_event, event_metadata): with self._lock: for sorted_event in self._manager_config.evaluation_order: if sorted_event in self._get_sdk_event_if_applicable(sdk_internal_event): - _LOGGER.debug("EventsManager: Firing Sdk event %s", sorted_event) if self._get_event_handler(sorted_event) != None: - notify_event = threading.Thread(target=self._events_delivery.deliver, args=[sorted_event, event_metadata, self._get_event_handler(sorted_event)], - name='SplitSDKEventNotify', daemon=True) - notify_event.start() - self._set_sdk_event_triggered(sorted_event) + self._fire_sdk_event(sorted_event, event_metadata) + + # if client is not subscribed to SDK_READY + if sorted_event == SdkEvent.SDK_READY and self._get_event_handler(sorted_event) == None: + _LOGGER.debug("EventsManager: Registering SDK_READY event as fired") + self._active_subscriptions[SdkEvent.SDK_READY] = ActiveSubscriptions(True, None) + def destroy(self): with self._lock: self._active_subscriptions = {} self._internal_events_status = {} + def _fire_sdk_event(self, sdk_event, event_metadata): + _LOGGER.debug("EventsManager: Firing Sdk event %s", sdk_event) + notify_event = threading.Thread(target=self._events_delivery.deliver, args=[sdk_event, event_metadata, self._get_event_handler(sdk_event)], + name='SplitSDKEventNotify', daemon=True) + notify_event.start() + self._set_sdk_event_triggered(sdk_event) + def _event_already_triggered(self, sdk_event): if self._active_subscriptions.get(sdk_event) != None: return self._active_subscriptions.get(sdk_event).triggered diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 75097b14..675478d3 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -547,10 +547,11 @@ def update(self, to_add, to_delete, new_change_number): to_notify = [] [to_notify.append(feature.name) for feature in to_add] to_notify.extend(to_delete) - self._internal_event_queue.put( - SdkInternalEventNotification( - SdkInternalEvent.FLAGS_UPDATED, - EventsMetadata(SdkEventType.FLAG_UPDATE, set(to_notify)))) + if len(to_notify) > 0: + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.FLAGS_UPDATED, + EventsMetadata(SdkEventType.FLAG_UPDATE, set(to_notify)))) def _put(self, feature_flag): """ diff --git a/splitio/sync/synchronizer.py b/splitio/sync/synchronizer.py index 8685d479..71194d26 100644 --- a/splitio/sync/synchronizer.py +++ b/splitio/sync/synchronizer.py @@ -329,9 +329,6 @@ def start_periodic_data_recording(self): for task in self._periodic_data_recording_tasks: task.start() - if self._split_tasks.internal_events_task: - self._split_tasks.internal_events_task.start() - def stop_periodic_data_recording(self, blocking): """ Stop recorders. @@ -883,8 +880,6 @@ def start_periodic_fetching(self): self._split_tasks.split_task.start() if self._split_tasks.segment_task is not None: self._split_tasks.segment_task.start() - if self._split_tasks.internal_events_task: - self._split_tasks.internal_events_task.start() def stop_periodic_fetching(self): """Stop fetchers for feature flags and segments.""" diff --git a/splitio/version.py b/splitio/version.py index ea7d787e..4f40eda2 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '10.5.1' \ No newline at end of file +__version__ = '10.6.0' \ No newline at end of file diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 05e25b51..0b2fe70f 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -28,6 +28,7 @@ from splitio.events.events_manager_config import EventsManagerConfig from splitio.events.events_task import EventsTask from splitio.models import splits, segments, rule_based_segments +from splitio.models.events import SdkEvent from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment from splitio.recorder.recorder import StandardRecorder, PipelinedRecorder, StandardRecorderAsync, PipelinedRecorderAsync @@ -2424,6 +2425,181 @@ def clear_cache(self): for key in keys_to_delete: redis_client.delete(key) +class InMemoryEventsNotificationTests(object): + """Inmemory storage-based events notification tests.""" + + ready_flag = False + timeout_flag = False + + def test_sdk_timeout_fire(self): + """Prepare storages with test data.""" + factory2 = get_factory('some_api_key') + client = factory2.client() + client.on(SdkEvent.SDK_READY_TIMED_OUT, self._timeout_callback) + try: + factory2.block_until_ready(1) + except Exception as e: + print(e) + pass + + time.sleep(1) + assert self.timeout_flag + + """Shut down the factory.""" + event = threading.Event() + factory2.destroy(event) + event.wait() + + def test_sdk_ready(self): + """Prepare storages with test data.""" + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) + + split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') + with open(split_fn, 'r') as flo: + data = json.loads(flo.read()) + for split in data['ff']['d']: + split_storage.update([splits.from_raw(split)], [], 0) + + for rbs in data['rbs']['d']: + rb_segment_storage.update([rule_based_segments.from_raw(rbs)], [], 0) + + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') + with open(segment_fn, 'r') as flo: + data = json.loads(flo.read()) + segment_storage.put(segments.from_raw(data)) + + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentHumanBeignsChanges.json') + with open(segment_fn, 'r') as flo: + data = json.loads(flo.read()) + segment_storage.put(segments.from_raw(data)) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + + storages = { + 'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': InMemoryImpressionStorage(5000, telemetry_runtime_producer), + 'events': InMemoryEventStorage(5000, telemetry_runtime_producer), + } + impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener + recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, events_queue) + + # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. + try: + factory = SplitFactory('some_api_key', + storages, + True, + recorder, + events_queue, + events_manager, + None, + telemetry_producer=telemetry_producer, + telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) + ) # pylint:disable=attribute-defined-outside-init + internal_events_task.start() + except: + pass + + client = factory.client() + client.on(SdkEvent.SDK_READY, self._ready_callback) + factory.block_until_ready(5) + assert self.ready_flag + + """Shut down the factory.""" + event = threading.Event() + factory.destroy(event) + event.wait() + + def test_sdk_ready_fire_later(self): + """Prepare storages with test data.""" + events_queue = queue.Queue() + split_storage = InMemorySplitStorage(events_queue) + segment_storage = InMemorySegmentStorage(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorage(events_queue) + + split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') + with open(split_fn, 'r') as flo: + data = json.loads(flo.read()) + for split in data['ff']['d']: + split_storage.update([splits.from_raw(split)], [], 0) + + for rbs in data['rbs']['d']: + rb_segment_storage.update([rule_based_segments.from_raw(rbs)], [], 0) + + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') + with open(segment_fn, 'r') as flo: + data = json.loads(flo.read()) + segment_storage.put(segments.from_raw(data)) + + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentHumanBeignsChanges.json') + with open(segment_fn, 'r') as flo: + data = json.loads(flo.read()) + segment_storage.put(segments.from_raw(data)) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + + storages = { + 'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': InMemoryImpressionStorage(5000, telemetry_runtime_producer), + 'events': InMemoryEventStorage(5000, telemetry_runtime_producer), + } + impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener + recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, events_queue) + + # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. + try: + factory = SplitFactory('some_api_key', + storages, + True, + recorder, + events_queue, + events_manager, + None, + telemetry_producer=telemetry_producer, + telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) + ) # pylint:disable=attribute-defined-outside-init + internal_events_task.start() + except: + pass + + client = factory.client() + factory.block_until_ready(5) + + assert client.get_treatment('user1', 'sample_feature', evaluation_options=EvaluationOptions({"prop": "value"})) == 'on' + + self.ready_flag = False + client.on(SdkEvent.SDK_READY, self._ready_callback) + assert self.ready_flag + + """Shut down the factory.""" + event = threading.Event() + factory.destroy(event) + event.wait() + + def _ready_callback(self, metadata): + self.ready_flag = True + + def _timeout_callback(self, metadata): + self.timeout_flag = True + class InMemoryIntegrationAsyncTests(object): """Inmemory storage-based integration tests.""" @@ -4984,4 +5160,4 @@ async def _manager_methods_async(factory, skip_rbs=False): return assert len(await manager.split_names()) == 9 - assert len(await manager.splits()) == 9 + assert len(await manager.splits()) == 9 \ No newline at end of file diff --git a/tests/integration/test_streaming_e2e.py b/tests/integration/test_streaming_e2e.py index 764475de..a673c65c 100644 --- a/tests/integration/test_streaming_e2e.py +++ b/tests/integration/test_streaming_e2e.py @@ -10,6 +10,8 @@ from queue import Queue from splitio.optional.loaders import asyncio from splitio.client.factory import get_factory, get_factory_async +from splitio.models.events import SdkEvent +from splitio.events.events_metadata import SdkEventType from tests.helpers.mockserver import SSEMockServer, SplitMockServer from urllib.parse import parse_qs from splitio.models.telemetry import StreamingEventTypes, SSESyncMode @@ -18,6 +20,9 @@ class StreamingIntegrationTests(object): """Test streaming operation and failover.""" + update_flag = False + metadata = [] + def test_happiness(self): """Test initialization & splits/segment updates.""" auth_server_response = { @@ -70,6 +75,7 @@ def test_happiness(self): } factory = get_factory('some_apikey', **kwargs) + factory.client().on(SdkEvent.SDK_UPDATE, self._update_callcack) factory.block_until_ready(1) assert factory.ready assert factory.client().get_treatment('maldo', 'split1') == 'on' @@ -87,6 +93,13 @@ def test_happiness(self): split_changes[2] = {'ff': {'s': 2, 't': 2, 'd': []}, 'rbs': {'s': -1, 't': -1, 'd': []}} sse_server.publish(make_split_change_event(2)) time.sleep(1) + flag = False + for meta in self.metadata: + if 'split1' in meta.get_names(): + assert meta.get_type() == SdkEventType.FLAG_UPDATE + flag = True + assert flag + assert factory.client().get_treatment('maldo', 'split1') == 'off' split_changes[2] = { @@ -110,14 +123,28 @@ def test_happiness(self): sse_server.publish(make_split_change_event(3)) time.sleep(1) + + self._reset_flags() sse_server.publish(make_segment_change_event('segment1', 1)) time.sleep(1) - + assert self.update_flag + assert self.metadata[len(self.metadata)-1].get_type() == SdkEventType.SEGMENT_UPDATE + flag = False + for meta in self.metadata: + if 'split2' in meta.get_names(): + assert meta.get_type() == SdkEventType.FLAG_UPDATE + flag = True + assert flag + assert factory.client().get_treatment('pindon', 'split2') == 'off' assert factory.client().get_treatment('maldo', 'split2') == 'on' + self._reset_flags() sse_server.publish(make_split_fast_change_event(4)) time.sleep(1) + assert self.update_flag + assert self.metadata[len(self.metadata)-1].get_type() == SdkEventType.FLAG_UPDATE + assert 'split5' in self.metadata[len(self.metadata)-1].get_names() assert factory.client().get_treatment('maldo', 'split5') == 'on' # Validate the SSE request @@ -212,6 +239,13 @@ def test_happiness(self): sse_server.stop() split_backend.stop() + def _update_callcack(self, metadata): + self.update_flag = True + self.metadata.append(metadata) + + def _reset_flags(self): + self.update_flag = False + def test_occupancy_flicker(self): """Test that changes in occupancy switch between polling & streaming properly.""" auth_server_response = { From 983a740345d22856c255ad7ad279fbe056d57dc0 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 20 Jan 2026 12:20:04 -0800 Subject: [PATCH 14/26] avoid fire events if no items added or removed from storage --- splitio/storage/inmemmory.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 675478d3..470288bc 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -154,10 +154,11 @@ def update(self, to_add, to_delete, new_change_number): [self._put(add_segment) for add_segment in to_add] [self._remove(delete_segment) for delete_segment in to_delete] self._set_change_number(new_change_number) - self._internal_event_queue.put( - SdkInternalEventNotification( - SdkInternalEvent.RB_SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + if len(to_add) > 0 or len(to_delete) > 0: + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.RB_SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) def _put(self, rule_based_segment): """ @@ -1000,10 +1001,11 @@ def update(self, segment_name, to_add, to_remove, change_number=None): if change_number is not None: self._segments[segment_name].change_number = change_number - self._internal_event_queue.put( - SdkInternalEventNotification( - SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + if len(to_add) > 0 or len(to_remove) >0: + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) def get_change_number(self, segment_name): """ From 7194c0a48ae009390e2b481f88a501af69be6eb4 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 20 Jan 2026 21:38:46 -0800 Subject: [PATCH 15/26] added async classes --- splitio/client/client.py | 47 ++-- splitio/client/factory.py | 44 ++- splitio/events/events_delivery.py | 7 + splitio/events/events_manager.py | 176 ++++++++---- splitio/events/events_task.py | 65 ++++- splitio/storage/inmemmory.py | 56 +++- splitio/sync/synchronizer.py | 3 + tests/client/test_client.py | 327 ++++++++++++++++++----- tests/client/test_factory.py | 9 +- tests/client/test_input_validator.py | 100 ++++++- tests/client/test_manager.py | 4 +- tests/engine/test_evaluator.py | 37 +-- tests/events/test_events_delivery.py | 17 ++ tests/events/test_events_manager.py | 103 ++++++- tests/events/test_events_task.py | 69 ++++- tests/integration/test_client_e2e.py | 99 +++++-- tests/push/test_split_worker.py | 5 +- tests/storage/test_inmemory_storage.py | 37 +-- tests/sync/test_segments_synchronizer.py | 4 +- tests/sync/test_splits_synchronizer.py | 30 ++- tests/sync/test_synchronizer.py | 20 +- tests/sync/test_telemetry.py | 5 +- tests/util/test_storage_helper.py | 3 +- 23 files changed, 1016 insertions(+), 251 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 0074bfb7..3c61166d 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -4,12 +4,13 @@ from collections import namedtuple import copy +from splitio.client import input_validator from splitio.engine.evaluator import Evaluator, CONTROL, EvaluationDataFactory, AsyncEvaluationDataFactory from splitio.engine.splitters import Splitter from splitio.models.impressions import Impression, Label, ImpressionDecorated from splitio.models.events import Event, EventWrapper, SdkEvent from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies -from splitio.client import input_validator +from splitio.optional.loaders import asyncio from splitio.util.time import get_current_epoch_time_ms, utctime_ms @@ -40,7 +41,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes 'impressions_disabled': False } - def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_calculator=None): + def __init__(self, factory, recorder, events_manager, labels_enabled=True, fallback_treatment_calculator=None): """ Construct a Client instance. @@ -66,6 +67,7 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_ca self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer self._fallback_treatment_calculator = fallback_treatment_calculator + self._events_manager = events_manager @property def ready(self): @@ -221,6 +223,23 @@ def _get_fallback_eval_results(self, eval_result, feature): def _check_impression_label(self, result): return result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1) + def _validate_sdk_event_info(self, sdk_event, callback_handle): + if not self._check_sdk_event(sdk_event): + return False + + if not hasattr(callback_handle, '__call__'): + _LOGGER.warning("Client Event Subscription: The callback handle passed must be of type function, ignoring event subscribing action.") + return False + + return True + + def _check_sdk_event(self, sdk_event): + if not isinstance(sdk_event, SdkEvent): + _LOGGER.warning("Client Event Subscription: The event passed must be of type SdkEvent, ignoring event subscribing action.") + return False + + return True + class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" @@ -239,8 +258,7 @@ def __init__(self, factory, recorder, events_manager, labels_enabled=True, fallb :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatment_calculator) - self._events_manager = events_manager + ClientBase.__init__(self, factory, recorder, events_manager, labels_enabled, fallback_treatment_calculator) self._context_factory = EvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) def destroy(self): @@ -256,17 +274,6 @@ def on(self, sdk_event, callback_handle): return self._events_manager.register(sdk_event, callback_handle) - - def _validate_sdk_event_info(self, sdk_event, callback_handle): - if not isinstance(sdk_event, SdkEvent): - _LOGGER.warning("Client Event Subscription: The event passed must be of type SdkEvent, ignoring event subscribing action.") - return False - - if not hasattr(callback_handle, '__call__'): - _LOGGER.warning("Client Event Subscription: The callback handle passed must be of type function, ignoring event subscribing action.") - return False - - return True def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ @@ -743,7 +750,7 @@ def track(self, key, traffic_type, event_type, value=None, properties=None): class ClientAsync(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_calculator=None): + def __init__(self, factory, recorder, events_manager, labels_enabled=True, fallback_treatment_calculator=None): """ Construct a Client instance. @@ -758,7 +765,7 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_ca :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatment_calculator) + ClientBase.__init__(self, factory, recorder, events_manager, labels_enabled, fallback_treatment_calculator) self._context_factory = AsyncEvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) async def destroy(self): @@ -769,6 +776,12 @@ async def destroy(self): """ await self._factory.destroy() + async def on(self, sdk_event, callback_handle): + if not self._validate_sdk_event_info(sdk_event, callback_handle): + return + + await self._events_manager.register(sdk_event, callback_handle) + async def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ Get the treatment for a feature and key, with an optional dictionary of attributes, for async calls diff --git a/splitio/client/factory.py b/splitio/client/factory.py index f5a4711b..670cf6c3 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -19,9 +19,9 @@ TelemetryStorageProducerAsync, TelemetryStorageConsumerAsync from splitio.engine.impressions.manager import Counter as ImpressionsCounter from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync -from splitio.events.events_manager import EventsManager +from splitio.events.events_manager import EventsManager, EventsManagerAsync from splitio.events.events_manager_config import EventsManagerConfig -from splitio.events.events_task import EventsTask +from splitio.events.events_task import EventsTask, EventsTaskAsync from splitio.events.events_delivery import EventsDelivery from splitio.models.fallback_config import FallbackTreatmentCalculator from splitio.models.notification import SdkInternalEventNotification @@ -352,6 +352,8 @@ def __init__( # pylint: disable=too-many-arguments storages, labels_enabled, recorder, + internal_events_queue, + events_manager, sync_manager=None, telemetry_producer=None, telemetry_init_producer=None, @@ -387,6 +389,8 @@ def __init__( # pylint: disable=too-many-arguments self._telemetry_submitter = telemetry_submitter self._ready_time = get_current_epoch_time_ms() _LOGGER.debug("Running in asyncio mode") + self._internal_events_queue = internal_events_queue + self._events_manager = events_manager self._manager_start_task = manager_start_task self._status = Status.NOT_INITIALIZED self._sdk_ready_flag = asyncio.Event() @@ -409,6 +413,7 @@ async def _update_status_when_ready_async(self): _LOGGER.debug(str(e)) self._status = Status.READY self._sdk_ready_flag.set() + await self._internal_events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_READY, None)) def manager(self): """ @@ -434,6 +439,7 @@ async def block_until_ready(self, timeout=None): _LOGGER.error("Exception initializing SDK") _LOGGER.debug(str(e)) await self._telemetry_init_producer.record_bur_time_out() + await self._internal_events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_TIMED_OUT, None)) raise TimeoutException('SDK Initialization: time of %d exceeded' % timeout) async def destroy(self, destroyed_event=None): @@ -481,7 +487,7 @@ def client(self): This client is only a set of references to structures hold by the factory. Creating one a fast operation and safe to be used anywhere. """ - return ClientAsync(self, self._recorder, self._labels_enabled, self._fallback_treatment_calculator) + return ClientAsync(self, self._recorder, self._events_manager, self._labels_enabled, self._fallback_treatment_calculator) def _wrap_impression_listener(listener, metadata): """ @@ -698,11 +704,14 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= 'events': EventsAPIAsync(http_client, api_key, sdk_metadata, telemetry_runtime_producer), 'telemetry': TelemetryAPIAsync(http_client, api_key, sdk_metadata, telemetry_runtime_producer), } + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTaskAsync(events_manager.notify_internal_event, internal_events_queue) storages = { - 'splits': InMemorySplitStorageAsync(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), - 'segments': InMemorySegmentStorageAsync(), - 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(), + 'splits': InMemorySplitStorageAsync(internal_events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), + 'segments': InMemorySegmentStorageAsync(internal_events_queue), + 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(internal_events_queue), 'impressions': InMemoryImpressionStorageAsync(cfg['impressionsQueueSize'], telemetry_runtime_producer), 'events': InMemoryEventStorageAsync(cfg['eventsQueueSize'], telemetry_runtime_producer), } @@ -748,6 +757,7 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= TelemetrySyncTaskAsync(synchronizers.telemetry_sync.synchronize_stats, cfg['metricsRefreshRate']), unique_keys_task, clear_filter_task, + internal_events_task ) synchronizer = SynchronizerAsync(synchronizers, tasks) @@ -770,11 +780,12 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= ) await telemetry_init_producer.record_config(cfg, extra_cfg, total_flag_sets, invalid_flag_sets) + internal_events_task.start() manager_start_task = asyncio.get_running_loop().create_task(manager.start()) return SplitFactoryAsync(api_key, storages, cfg['labelsEnabled'], - recorder, manager, + recorder, internal_events_queue, events_manager, manager, telemetry_producer, telemetry_init_producer, telemetry_submitter, manager_start_task=manager_start_task, api_client=http_client, fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments'])) @@ -933,12 +944,16 @@ async def _build_redis_factory_async(api_key, cfg): manager = RedisManagerAsync(synchronizer) await telemetry_init_producer.record_config(cfg, {}, 0, 0) manager.start() + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) split_factory = SplitFactoryAsync( api_key, storages, cfg['labelsEnabled'], recorder, + internal_events_queue, + events_manager, manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, @@ -1101,12 +1116,16 @@ async def _build_pluggable_factory_async(api_key, cfg): manager = RedisManagerAsync(synchronizer) manager.start() await telemetry_init_producer.record_config(cfg, {}, 0, 0) + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) split_factory = SplitFactoryAsync( api_key, storages, cfg['labelsEnabled'], recorder, + internal_events_queue, + events_manager, manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, @@ -1205,10 +1224,12 @@ async def _build_localhost_factory_async(cfg): telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) storages = { - 'splits': InMemorySplitStorageAsync(), - 'segments': InMemorySegmentStorageAsync(), # not used, just to avoid possible future errors. - 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(), + 'splits': InMemorySplitStorageAsync(internal_events_queue), + 'segments': InMemorySegmentStorageAsync(internal_events_queue), # not used, just to avoid possible future errors. + 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(internal_events_queue), 'impressions': LocalhostImpressionsStorageAsync(), 'events': LocalhostEventsStorageAsync(), } @@ -1257,11 +1278,14 @@ async def _build_localhost_factory_async(cfg): telemetry_evaluation_producer, telemetry_runtime_producer ) + return SplitFactoryAsync( 'localhost', storages, False, recorder, + internal_events_queue, + events_manager, manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), diff --git a/splitio/events/events_delivery.py b/splitio/events/events_delivery.py index 129c14dc..a582d8a0 100644 --- a/splitio/events/events_delivery.py +++ b/splitio/events/events_delivery.py @@ -19,3 +19,10 @@ def deliver(self, sdk_event, event_metadata, event_handler): except Exception as ex: _LOGGER.error("Exception when calling handler for Sdk Event %s", sdk_event) _LOGGER.error(ex) + + async def deliver_async(self, sdk_event, event_metadata, event_handler): + try: + await event_handler(event_metadata) + except Exception as ex: + _LOGGER.error("Exception when calling handler for Sdk Event %s", sdk_event) + _LOGGER.error(ex) diff --git a/splitio/events/events_manager.py b/splitio/events/events_manager.py index 9457e24a..b51a992c 100644 --- a/splitio/events/events_manager.py +++ b/splitio/events/events_manager.py @@ -2,6 +2,7 @@ import threading import logging from collections import namedtuple +from splitio.optional.loaders import asyncio from splitio.events import EventsManagerInterface from splitio.models.events import SdkEvent @@ -11,7 +12,7 @@ ValidSdkEvent = namedtuple('ValidSdkEvent', ['sdk_event', 'valid']) ActiveSubscriptions = namedtuple('ActiveSubscriptions', ['triggered', 'handler']) -class EventsManager(EventsManagerInterface): +class EventsManagerBase(EventsManagerInterface): """Events Manager class.""" def __init__(self, events_configurations, events_delivery): @@ -22,54 +23,19 @@ def __init__(self, events_configurations, events_delivery): self._internal_events_status = {} self._events_delivery = events_delivery self._manager_config = events_configurations - self._lock = threading.RLock() def register(self, sdk_event, event_handler): - if self._active_subscriptions.get(sdk_event) != None and self._get_event_handler(sdk_event) != None: - return - - with self._lock: - # SDK ready already fired - if sdk_event == SdkEvent.SDK_READY and self._event_already_triggered(sdk_event): - self._active_subscriptions[sdk_event] = ActiveSubscriptions(True, event_handler) - _LOGGER.debug("EventsManager: Firing SDK_READY event for new subscription") - self._fire_sdk_event(sdk_event, None) - return - - self._active_subscriptions[sdk_event] = ActiveSubscriptions(False, event_handler) - + pass + def unregister(self, sdk_event): - if self._active_subscriptions.get(sdk_event) == None: - return - - with self._lock: - del self._active_subscriptions[sdk_event] - + pass + def notify_internal_event(self, sdk_internal_event, event_metadata): - with self._lock: - for sorted_event in self._manager_config.evaluation_order: - if sorted_event in self._get_sdk_event_if_applicable(sdk_internal_event): - if self._get_event_handler(sorted_event) != None: - self._fire_sdk_event(sorted_event, event_metadata) - - # if client is not subscribed to SDK_READY - if sorted_event == SdkEvent.SDK_READY and self._get_event_handler(sorted_event) == None: - _LOGGER.debug("EventsManager: Registering SDK_READY event as fired") - self._active_subscriptions[SdkEvent.SDK_READY] = ActiveSubscriptions(True, None) - + pass def destroy(self): - with self._lock: - self._active_subscriptions = {} - self._internal_events_status = {} - - def _fire_sdk_event(self, sdk_event, event_metadata): - _LOGGER.debug("EventsManager: Firing Sdk event %s", sdk_event) - notify_event = threading.Thread(target=self._events_delivery.deliver, args=[sdk_event, event_metadata, self._get_event_handler(sdk_event)], - name='SplitSDKEventNotify', daemon=True) - notify_event.start() - self._set_sdk_event_triggered(sdk_event) - + pass + def _event_already_triggered(self, sdk_event): if self._active_subscriptions.get(sdk_event) != None: return self._active_subscriptions.get(sdk_event).triggered @@ -81,11 +47,10 @@ def _get_internal_event_status(self, sdk_internal_event): return self._internal_events_status[sdk_internal_event] return False - + def _update_internal_event_status(self, sdk_internal_event, status): - with self._lock: - self._internal_events_status[sdk_internal_event] = status - + self._internal_events_status[sdk_internal_event] = status + def _set_sdk_event_triggered(self, sdk_event): if self._active_subscriptions.get(sdk_event) == None: return @@ -94,7 +59,7 @@ def _set_sdk_event_triggered(self, sdk_event): return self._active_subscriptions[sdk_event] = self._active_subscriptions[sdk_event]._replace(triggered = True) - + def _get_event_handler(self, sdk_event): if self._active_subscriptions.get(sdk_event) == None: return None @@ -103,12 +68,11 @@ def _get_event_handler(self, sdk_event): def _get_sdk_event_if_applicable(self, sdk_internal_event): final_sdk_event = ValidSdkEvent(None, False) - self._update_internal_event_status(sdk_internal_event, True) events_to_fire = [] require_any_sdk_event = self._check_require_any(sdk_internal_event) if require_any_sdk_event.valid: - if (not self._set_sdk_event_triggered(require_any_sdk_event.sdk_event) and + if (not self._event_already_triggered(require_any_sdk_event.sdk_event) and self._execution_limit(require_any_sdk_event.sdk_event) == 1) or \ self._execution_limit(require_any_sdk_event.sdk_event) == -1: final_sdk_event = final_sdk_event._replace(sdk_event = require_any_sdk_event.sdk_event, @@ -170,4 +134,114 @@ def _check_require_any(self, sdk_internal_event): valid_sdk_event = valid_sdk_event._replace(valid = True, sdk_event = name) return valid_sdk_event - return valid_sdk_event \ No newline at end of file + return valid_sdk_event + +class EventsManager(EventsManagerBase): + """Events Manager class.""" + + def __init__(self, events_configurations, events_delivery): + """ + Construct Events Manager instance. + """ + EventsManagerBase.__init__(self, events_configurations, events_delivery) + self._lock = threading.RLock() + + def register(self, sdk_event, event_handler): + if self._active_subscriptions.get(sdk_event) != None and self._get_event_handler(sdk_event) != None: + return + + with self._lock: + # SDK ready already fired + if sdk_event == SdkEvent.SDK_READY and self._event_already_triggered(sdk_event): + self._active_subscriptions[sdk_event] = ActiveSubscriptions(True, event_handler) + _LOGGER.debug("EventsManager: Firing SDK_READY event for new subscription") + self._fire_sdk_event(sdk_event, None) + return + + self._active_subscriptions[sdk_event] = ActiveSubscriptions(False, event_handler) + + def unregister(self, sdk_event): + if self._active_subscriptions.get(sdk_event) == None: + return + + with self._lock: + del self._active_subscriptions[sdk_event] + + def notify_internal_event(self, sdk_internal_event, event_metadata): + with self._lock: + self._update_internal_event_status(sdk_internal_event, True) + for sorted_event in self._manager_config.evaluation_order: + if sorted_event in self._get_sdk_event_if_applicable(sdk_internal_event): + if self._get_event_handler(sorted_event) != None: + self._fire_sdk_event(sorted_event, event_metadata) + + # if client is not subscribed to SDK_READY + if sorted_event == SdkEvent.SDK_READY and self._get_event_handler(sorted_event) == None: + _LOGGER.debug("EventsManager: Registering SDK_READY event as fired") + self._active_subscriptions[SdkEvent.SDK_READY] = ActiveSubscriptions(True, None) + + def destroy(self): + with self._lock: + self._active_subscriptions = {} + self._internal_events_status = {} + + def _fire_sdk_event(self, sdk_event, event_metadata): + _LOGGER.debug("EventsManager: Firing Sdk event %s", sdk_event) + notify_event = threading.Thread(target=self._events_delivery.deliver, args=[sdk_event, event_metadata, self._get_event_handler(sdk_event)], + name='SplitSDKEventNotify', daemon=True) + notify_event.start() + self._set_sdk_event_triggered(sdk_event) + +class EventsManagerAsync(EventsManagerBase): + """Events Manager Async class.""" + + def __init__(self, events_configurations, events_delivery): + """ + Construct Events Manager instance. + """ + EventsManagerBase.__init__(self, events_configurations, events_delivery) + self._lock = asyncio.Lock() + + async def register(self, sdk_event, event_handler): + if self._active_subscriptions.get(sdk_event) != None and self._get_event_handler(sdk_event) != None: + return + + async with self._lock: + # SDK ready already fired + if sdk_event == SdkEvent.SDK_READY and self._event_already_triggered(sdk_event): + self._active_subscriptions[sdk_event] = ActiveSubscriptions(True, event_handler) + _LOGGER.debug("EventsManager: Firing SDK_READY event for new subscription") + await self._fire_sdk_event(sdk_event, None) + return + + self._active_subscriptions[sdk_event] = ActiveSubscriptions(False, event_handler) + + async def unregister(self, sdk_event): + if self._active_subscriptions.get(sdk_event) == None: + return + + async with self._lock: + del self._active_subscriptions[sdk_event] + + async def notify_internal_event(self, sdk_internal_event, event_metadata): + async with self._lock: + self._update_internal_event_status(sdk_internal_event, True) + for sorted_event in self._manager_config.evaluation_order: + if sorted_event in self._get_sdk_event_if_applicable(sdk_internal_event): + if self._get_event_handler(sorted_event) != None: + await self._fire_sdk_event(sorted_event, event_metadata) + + # if client is not subscribed to SDK_READY + if sorted_event == SdkEvent.SDK_READY and self._get_event_handler(sorted_event) == None: + _LOGGER.debug("EventsManager: Registering SDK_READY event as fired") + self._active_subscriptions[SdkEvent.SDK_READY] = ActiveSubscriptions(True, None) + + async def destroy(self): + async with self._lock: + self._active_subscriptions = {} + self._internal_events_status = {} + + async def _fire_sdk_event(self, sdk_event, event_metadata): + _LOGGER.debug("EventsManager: Firing Sdk event %s", sdk_event) + asyncio.get_running_loop().create_task(self._events_delivery.deliver_async(sdk_event, event_metadata, self._get_event_handler(sdk_event))) + self._set_sdk_event_triggered(sdk_event) \ No newline at end of file diff --git a/splitio/events/events_task.py b/splitio/events/events_task.py index ea0ffce7..8158dc04 100644 --- a/splitio/events/events_task.py +++ b/splitio/events/events_task.py @@ -3,6 +3,8 @@ import threading import abc +from splitio.optional.loaders import asyncio + _LOGGER = logging.getLogger(__name__) class EventsTaskBase(object, metaclass=abc.ABCMeta): @@ -80,4 +82,65 @@ def stop(self, stop_flag=None): return self._running = False - self._internal_events_queue.put(self._centinel) \ No newline at end of file + self._internal_events_queue.put(self._centinel) + +class EventsTaskAsync(EventsTaskBase): + """sdk internal events processing task.""" + + _centinel = object() + + def __init__(self, notify_internal_events, internal_events_queue): + """ + Class constructor. + + :param synchronize_segment: handler to perform segment synchronization on incoming event + :type synchronize_segment: function + + :param segment_queue: queue with segment updates notifications + :type segment_queue: queue + """ + self._internal_events_queue = internal_events_queue + self._handler = notify_internal_events + self._running = False + self._worker = None + + def is_running(self): + """Return whether the working is running.""" + return self._running + + async def _run(self): + """Run worker handler.""" + while self.is_running(): + event = await self._internal_events_queue.get() + if not self.is_running(): + break + + if event == self._centinel: + continue + + _LOGGER.debug('Processing sdk internal event: %s', event.internal_event) + try: + await self._handler(event.internal_event, event.metadata) + except Exception: + _LOGGER.error('Exception raised in events manager') + _LOGGER.debug('Exception information: ', exc_info=True) + + def start(self): + """Start worker.""" + if self.is_running(): + _LOGGER.debug('SDK Event Worker is already running') + return + + self._running = True + _LOGGER.debug('Starting SDK Event Task worker') + asyncio.get_running_loop().create_task(self._run()) + + async def stop(self, stop_flag=None): + """Stop worker.""" + _LOGGER.debug('Stopping SDK Event Task worker') + if not self.is_running(): + _LOGGER.debug('SDK Event Worker is not running. Ignoring.') + return + + self._running = False + await self._internal_events_queue.put(self._centinel) \ No newline at end of file diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index 675478d3..bbde8816 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -154,10 +154,11 @@ def update(self, to_add, to_delete, new_change_number): [self._put(add_segment) for add_segment in to_add] [self._remove(delete_segment) for delete_segment in to_delete] self._set_change_number(new_change_number) - self._internal_event_queue.put( - SdkInternalEventNotification( - SdkInternalEvent.RB_SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + if len(to_add) > 0 or len(to_delete) > 0: + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.RB_SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) def _put(self, rule_based_segment): """ @@ -244,11 +245,12 @@ def fetch_many(self, segment_names): class InMemoryRuleBasedSegmentStorageAsync(RuleBasedSegmentsStorage): """InMemory implementation of a feature flag storage base.""" - def __init__(self): + def __init__(self, internal_event_queue): """Constructor.""" self._lock = asyncio.Lock() self._rule_based_segments = {} self._change_number = -1 + self._internal_event_queue = internal_event_queue async def clear(self): """ @@ -284,6 +286,11 @@ async def update(self, to_add, to_delete, new_change_number): [await self._put(add_segment) for add_segment in to_add] [await self._remove(delete_segment) for delete_segment in to_delete] await self._set_change_number(new_change_number) + if len(to_add) > 0 or len(to_delete) > 0: + await self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.RB_SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) async def _put(self, rule_based_segment): """ @@ -716,7 +723,7 @@ def is_flag_set_exist(self, flag_set): class InMemorySplitStorageAsync(InMemorySplitStorageBase): """InMemory implementation of a feature flag async storage.""" - def __init__(self, flag_sets=[]): + def __init__(self, internal_event_queue, flag_sets=[]): """Constructor.""" self._lock = asyncio.Lock() self._feature_flags = {} @@ -724,6 +731,7 @@ def __init__(self, flag_sets=[]): self._traffic_types = Counter() self.flag_set = FlagSets(flag_sets) self.flag_set_filter = FlagSetsFilter(flag_sets) + self._internal_event_queue = internal_event_queue async def clear(self): """ @@ -772,6 +780,14 @@ async def update(self, to_add, to_delete, new_change_number): [await self._put(add_feature_flag) for add_feature_flag in to_add] [await self._remove(delete_feature_flag) for delete_feature_flag in to_delete] await self._set_change_number(new_change_number) + to_notify = [] + [to_notify.append(feature.name) for feature in to_add] + to_notify.extend(to_delete) + if len(to_notify) > 0: + await self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.FLAGS_UPDATED, + EventsMetadata(SdkEventType.FLAG_UPDATE, set(to_notify)))) async def _put(self, feature_flag): """ @@ -917,6 +933,11 @@ async def kill_locally(self, feature_flag_name, default_treatment, change_number return feature_flag.local_kill(default_treatment, change_number) await self._put(feature_flag) + await self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.FLAG_KILLED_NOTIFICATION, + EventsMetadata(SdkEventType.FLAG_UPDATE, {feature_flag_name}))) + async def get_segment_names(self): """ @@ -1000,10 +1021,11 @@ def update(self, segment_name, to_add, to_remove, change_number=None): if change_number is not None: self._segments[segment_name].change_number = change_number - self._internal_event_queue.put( - SdkInternalEventNotification( - SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + if len(to_add) > 0 or len(to_remove) >0: + self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) def get_change_number(self, segment_name): """ @@ -1081,11 +1103,12 @@ def get_segments_keys_count(self): class InMemorySegmentStorageAsync(SegmentStorage): """In-memory implementation of a segment async storage.""" - def __init__(self): + def __init__(self, internal_event_queue): """Constructor.""" self._segments = {} self._change_numbers = {} self._lock = asyncio.Lock() + self._internal_event_queue = internal_event_queue async def get(self, segment_name): """ @@ -1114,6 +1137,11 @@ async def put(self, segment): """ async with self._lock: self._segments[segment.name] = segment + await self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + async def update(self, segment_name, to_add, to_remove, change_number=None): """ @@ -1134,6 +1162,12 @@ async def update(self, segment_name, to_add, to_remove, change_number=None): self._segments[segment_name].update(to_add, to_remove) if change_number is not None: self._segments[segment_name].change_number = change_number + if len(to_add) > 0 or len(to_remove) >0: + await self._internal_event_queue.put( + SdkInternalEventNotification( + SdkInternalEvent.SEGMENTS_UPDATED, + EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + async def get_change_number(self, segment_name): """ diff --git a/splitio/sync/synchronizer.py b/splitio/sync/synchronizer.py index 71194d26..6bbb7fa6 100644 --- a/splitio/sync/synchronizer.py +++ b/splitio/sync/synchronizer.py @@ -654,6 +654,9 @@ async def stop_periodic_data_recording(self, blocking): :type blocking: bool """ _LOGGER.debug('Stopping periodic data recording') + if self._split_tasks.internal_events_task: + await self._split_tasks.internal_events_task.stop() + if blocking: await self._stop_periodic_data_recording() _LOGGER.debug('all tasks finished successfully.') diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 94da58a2..1efd4143 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -4,10 +4,11 @@ import unittest.mock as mock import pytest import queue +import asyncio from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync -from splitio.events.events_manager import EventsManager +from splitio.events.events_manager import EventsManager, EventsManagerAsync from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment from splitio.models.impressions import Impression, Label @@ -1744,11 +1745,17 @@ class ClientAsyncTests(object): # pylint: disable=too-few-public-methods @pytest.mark.asyncio async def test_get_treatment_async(self, mocker): """Test get_treatment_async execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -1764,7 +1771,8 @@ async def test_get_treatment_async(self, mocker): class TelemetrySubmitterMock(): async def synchronize_config(*_): - pass + pass + factory = SplitFactoryAsync(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -1773,6 +1781,8 @@ async def synchronize_config(*_): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -1780,7 +1790,7 @@ async def synchronize_config(*_): ) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.eval_with_context.return_value = { 'treatment': 'on', @@ -1815,11 +1825,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_get_treatment_with_config_async(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -1838,6 +1854,8 @@ async def test_get_treatment_with_config_async(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -1852,7 +1870,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.eval_with_context.return_value = { 'treatment': 'on', @@ -1892,11 +1910,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_get_treatments_async(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -1915,6 +1939,8 @@ async def test_get_treatments_async(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -1929,7 +1955,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -1972,11 +1998,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_get_treatments_by_flag_set_async(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -1995,6 +2027,8 @@ async def test_get_treatments_by_flag_set_async(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2009,7 +2043,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2052,11 +2086,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_get_treatments_by_flag_sets_async(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -2075,6 +2115,8 @@ async def test_get_treatments_by_flag_sets_async(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2089,7 +2131,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2132,11 +2174,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_get_treatments_with_config(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -2154,6 +2202,8 @@ async def test_get_treatments_with_config(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2168,7 +2218,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2216,11 +2266,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_get_treatments_with_config_by_flag_set(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -2238,6 +2294,8 @@ async def test_get_treatments_with_config_by_flag_set(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2252,7 +2310,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2300,11 +2358,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_get_treatments_with_config_by_flag_sets(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -2322,6 +2386,8 @@ async def test_get_treatments_with_config_by_flag_sets(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2336,7 +2402,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2384,11 +2450,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_impression_toggle_optimized(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -2409,6 +2481,8 @@ async def test_impression_toggle_optimized(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2422,7 +2496,7 @@ async def test_impression_toggle_optimized(self, mocker): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) treatment = await client.get_treatment('some_key', 'SPLIT_1') assert treatment == 'off' treatment = await client.get_treatment('some_key', 'SPLIT_2') @@ -2447,11 +2521,17 @@ async def test_impression_toggle_optimized(self, mocker): @pytest.mark.asyncio async def test_impression_toggle_debug(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -2472,6 +2552,8 @@ async def test_impression_toggle_debug(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2485,7 +2567,7 @@ async def test_impression_toggle_debug(self, mocker): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) assert await client.get_treatment('some_key', 'SPLIT_1') == 'off' assert await client.get_treatment('some_key', 'SPLIT_2') == 'on' assert await client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -2507,11 +2589,17 @@ async def test_impression_toggle_debug(self, mocker): @pytest.mark.asyncio async def test_impression_toggle_none(self, mocker): """Test get_treatment execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -2532,6 +2620,8 @@ async def test_impression_toggle_none(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2545,7 +2635,7 @@ async def test_impression_toggle_none(self, mocker): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) assert await client.get_treatment('some_key', 'SPLIT_1') == 'off' assert await client.get_treatment('some_key', 'SPLIT_2') == 'on' assert await client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -2557,7 +2647,13 @@ async def test_impression_toggle_none(self, mocker): @pytest.mark.asyncio async def test_track_async(self, mocker): """Test that destroy/destroyed calls are forwarded to the factory.""" - split_storage = InMemorySplitStorageAsync() + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + + split_storage = InMemorySplitStorageAsync(internal_events_queue) segment_storage = mocker.Mock(spec=SegmentStorage) rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) @@ -2580,6 +2676,8 @@ async def put(event): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2596,7 +2694,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) assert await client.track('key', 'user', 'purchase', 12) is True assert self.events[0] == [EventWrapper( event=Event('key', 'user', 'purchase', 12, 1000, None), @@ -2606,11 +2704,17 @@ async def synchronize_config(*_): @pytest.mark.asyncio async def test_telemetry_not_ready_async(self, mocker): + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = InMemoryEventStorageAsync(10, telemetry_runtime_producer) @@ -2625,6 +2729,8 @@ async def test_telemetry_not_ready_async(self, mocker): 'events': mocker.Mock()}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2640,7 +2746,7 @@ async def synchronize_config(*_): type(factory).ready = ready_property await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) assert await client.get_treatment('some_key', 'SPLIT_2') == CONTROL assert(telemetry_storage._tel_config._not_ready == 1) await client.track('key', 'tt', 'ev') @@ -2649,11 +2755,17 @@ async def synchronize_config(*_): @pytest.mark.asyncio async def test_telemetry_record_treatment_exception_async(self, mocker): + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = InMemoryEventStorageAsync(10, telemetry_runtime_producer) @@ -2674,6 +2786,8 @@ async def test_telemetry_record_treatment_exception_async(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2688,7 +2802,7 @@ async def synchronize_config(*_): ready_property.return_value = True type(factory).ready = ready_property - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock() def _raise(*_): raise RuntimeError('something') @@ -2723,11 +2837,17 @@ def _raise(*_): @pytest.mark.asyncio async def test_telemetry_method_latency_async(self, mocker): + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = InMemoryEventStorageAsync(10, telemetry_runtime_producer) @@ -2748,6 +2868,8 @@ async def test_telemetry_method_latency_async(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2766,7 +2888,7 @@ async def synchronize_config(*_): await factory.block_until_ready(1) except: pass - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) assert await client.get_treatment('key', 'SPLIT_2') == 'on' assert(telemetry_storage._method_latencies._treatment[0] == 1) @@ -2798,7 +2920,13 @@ async def synchronize_config(*_): @pytest.mark.asyncio async def test_telemetry_track_exception_async(self, mocker): - split_storage = InMemorySplitStorageAsync() + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + + split_storage = InMemorySplitStorageAsync(internal_events_queue) segment_storage = mocker.Mock(spec=SegmentStorage) rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) impression_storage = mocker.Mock(spec=ImpressionStorage) @@ -2821,6 +2949,8 @@ async def test_telemetry_track_exception_async(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2836,7 +2966,7 @@ async def exc(*_): recorder.record_track_stats = exc await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) try: await client.track('key', 'tt', 'ev') except: @@ -2847,11 +2977,17 @@ async def exc(*_): @pytest.mark.asyncio async def test_impressions_properties_async(self, mocker): """Test get_treatment_async execution paths.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) event_storage = mocker.Mock(spec=EventStorage) @@ -2876,6 +3012,8 @@ async def synchronize_config(*_): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2883,7 +3021,7 @@ async def synchronize_config(*_): ) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2956,6 +3094,12 @@ async def synchronize_config(*_): @pytest.mark.asyncio async def test_fallback_treatment_eval_exception(self, mocker): # using fallback when the evaluator has RuntimeError exception + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) @@ -2985,6 +3129,8 @@ async def synchronize_config(*_): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -3001,7 +3147,7 @@ async def put(impressions): self.imps = impressions impression_storage.put = put - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}')))) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}')))) def eval_with_context(*_): raise RuntimeError() @@ -3112,6 +3258,12 @@ async def fetch_many_rbs(*_): @pytest.mark.asyncio async def test_fallback_treatment_exception(self, mocker): # using fallback when the evaluator has RuntimeError exception + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) @@ -3136,6 +3288,8 @@ async def test_fallback_treatment_exception(self, mocker): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -3156,7 +3310,7 @@ def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) def eval_with_context(*_): raise Exception() @@ -3204,6 +3358,12 @@ async def context_for(*_): @pytest.mark.asyncio async def test_fallback_treatment_not_ready_impressions(self, mocker): # using fallback when the evaluator has RuntimeError exception + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) @@ -3228,6 +3388,8 @@ async def manager_start_task(): 'events': event_storage}, mocker.Mock(), recorder, + internal_events_queue, + events_manager, impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -3245,7 +3407,7 @@ def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) + client = ClientAsync(factory, recorder, events_manager, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) ready_property = mocker.PropertyMock() ready_property.return_value = False type(factory).ready = ready_property @@ -3286,4 +3448,29 @@ async def context_for(*_): try: await factory.destroy() except: - pass \ No newline at end of file + pass + + @pytest.mark.asyncio + async def test_events_subscription(self, mocker): + events_manager = mocker.Mock(spec=EventsManagerAsync) + self.event = None + self.handle = None + async def register(sdk_event, callback_handle): + self.event = sdk_event + self.handle = callback_handle + events_manager.register = register + + client = ClientAsync(mocker.Mock(), mocker.Mock(), events_manager, True, FallbackTreatmentCalculator(None)) + await client.on(SdkEvent.SDK_READY, self.event_callback) + assert self.event == SdkEvent.SDK_READY + assert self.handle == self.event_callback + + self.event = None + await client.on("dd", self.event_callback) + assert self.event == None + + await client.on(SdkEvent.SDK_READY, "qwe") + assert self.event == None + + async def event_callback(self, metadata): + pass \ No newline at end of file diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 14a6ec27..45e64c72 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -20,6 +20,7 @@ from splitio.engine.evaluator import Evaluator, EvaluationContext from splitio.engine.impressions.strategies import StrategyDebugMode, StrategyNoneMode, StrategyOptimizedMode from splitio.events.events_task import EventsTask +from splitio.events.events_manager import EventsManagerAsync from splitio.models.splits import from_raw from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment @@ -1078,8 +1079,14 @@ async def test_pluggable_client_creation_async(self, mocker): @pytest.mark.asyncio async def test_destroy_redis_async(self, mocker): + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + async def _make_factory_with_apikey(apikey, *_, **__): - return SplitFactoryAsync(apikey, {}, True, mocker.Mock(spec=ImpressionsManager), None, mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock()) + return SplitFactoryAsync(apikey, {}, True, mocker.Mock(), internal_events_queue, events_manager, mocker.Mock(spec=ManagerAsync), mocker.Mock(), mocker.Mock(), mocker.Mock(), mocker.Mock()) factory_module_logger = mocker.Mock() build_redis = mocker.Mock() diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 2df8964b..e1634f54 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1,10 +1,12 @@ """Unit tests for the input_validator module.""" import pytest import logging +import asyncio from splitio.client.factory import SplitFactory, get_factory, SplitFactoryAsync, get_factory_async from splitio.client.client import CONTROL, Client, _LOGGER as _logger, ClientAsync from splitio.client.key import Key +from splitio.events.events_manager import EventsManagerAsync from splitio.storage import SplitStorage, EventStorage, ImpressionStorage, SegmentStorage, RuleBasedSegmentsStorage from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemoryTelemetryStorageAsync, \ InMemorySplitStorage, InMemorySplitStorageAsync, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync @@ -1682,6 +1684,12 @@ class ClientInputValidationAsyncTests(object): @pytest.mark.asyncio async def test_get_treatment(self, mocker): """Test get_treatment validation.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_mock = mocker.Mock(spec=Split) default_treatment_mock = mocker.PropertyMock() default_treatment_mock.return_value = 'default_treatment' @@ -1720,6 +1728,8 @@ async def get_change_number(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -1730,7 +1740,7 @@ async def get_change_number(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, mocker.Mock(), events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass @@ -1941,6 +1951,12 @@ async def fetch_many(*_): @pytest.mark.asyncio async def test_get_treatment_with_config(self, mocker): """Test get_treatment validation.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_mock = mocker.Mock(spec=Split) default_treatment_mock = mocker.PropertyMock() default_treatment_mock.return_value = 'default_treatment' @@ -1983,6 +1999,8 @@ async def get_change_number(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -1993,7 +2011,7 @@ async def get_change_number(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, mocker.Mock(), events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -2203,6 +2221,12 @@ async def fetch_many(*_): @pytest.mark.asyncio async def test_track(self, mocker): """Test track method().""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + events_storage_mock = mocker.Mock(spec=EventStorage) async def put(*_): return True @@ -2228,6 +2252,8 @@ async def put(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2236,7 +2262,7 @@ async def put(*_): ) factory._sdk_key = 'some-test' - client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) client._event_storage = event_storage _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -2478,6 +2504,12 @@ async def is_valid_traffic_type(*_): @pytest.mark.asyncio async def test_get_treatments(self, mocker): """Test getTreatments() method.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_mock = mocker.Mock(spec=Split) default_treatment_mock = mocker.PropertyMock() default_treatment_mock.return_value = 'default_treatment' @@ -2519,6 +2551,8 @@ async def fetch_many_rbs(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2529,7 +2563,7 @@ async def fetch_many_rbs(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -2643,6 +2677,12 @@ async def fetch_many(*_): @pytest.mark.asyncio async def test_get_treatments_with_config(self, mocker): """Test getTreatments() method.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_mock = mocker.Mock(spec=Split) default_treatment_mock = mocker.PropertyMock() default_treatment_mock.return_value = 'default_treatment' @@ -2684,6 +2724,8 @@ async def fetch_many_rbs(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2696,7 +2738,7 @@ def _configs(treatment): return '{"some": "property"}' if treatment == 'default_treatment' else None split_mock.get_configurations_for.side_effect = _configs - client = ClientAsync(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, mocker.Mock(), events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -2808,6 +2850,12 @@ async def fetch_many(*_): @pytest.mark.asyncio async def test_get_treatments_by_flag_set(self, mocker): + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_mock = mocker.Mock(spec=Split) default_treatment_mock = mocker.PropertyMock() default_treatment_mock.return_value = 'default_treatment' @@ -2852,6 +2900,8 @@ async def fetch_many_rbs(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -2862,7 +2912,7 @@ async def fetch_many_rbs(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -2954,6 +3004,12 @@ async def get_feature_flags_by_sets(*_): @pytest.mark.asyncio async def test_get_treatments_by_flag_sets(self, mocker): + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_mock = mocker.Mock(spec=Split) default_treatment_mock = mocker.PropertyMock() default_treatment_mock.return_value = 'default_treatment' @@ -2999,6 +3055,8 @@ async def get_feature_flags_by_sets(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -3009,7 +3067,7 @@ async def get_feature_flags_by_sets(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -3107,6 +3165,12 @@ async def get_feature_flags_by_sets(*_): @pytest.mark.asyncio async def test_get_treatments_with_config_by_flag_set(self, mocker): + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_mock = mocker.Mock(spec=Split) def _configs(treatment): return '{"some": "property"}' if treatment == 'default_treatment' else None @@ -3155,6 +3219,8 @@ async def get_feature_flags_by_sets(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), @@ -3165,7 +3231,7 @@ async def get_feature_flags_by_sets(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -3257,6 +3323,12 @@ async def get_feature_flags_by_sets(*_): @pytest.mark.asyncio async def test_get_treatments_with_config_by_flag_sets(self, mocker): + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + split_mock = mocker.Mock(spec=Split) def _configs(treatment): return '{"some": "property"}' if treatment == 'default_treatment' else None @@ -3305,6 +3377,8 @@ async def get_feature_flags_by_sets(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), mocker.Mock(), telemetry_producer, @@ -3316,7 +3390,7 @@ async def get_feature_flags_by_sets(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) + client = ClientAsync(factory, recorder, events_manager, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -3522,6 +3596,12 @@ class ManagerInputValidationAsyncTests(object): #pylint: disable=too-few-public @pytest.mark.asyncio async def test_split_(self, mocker): """Test split input validation.""" + internal_events_queue = asyncio.Queue() + events_manager = mocker.Mock(EventsManagerAsync) + async def notify_internal_event(sdk_internal_event, event_metadata): + pass + events_manager.notify_internal_event = notify_internal_event + storage_mock = mocker.Mock(spec=SplitStorage) split_mock = mocker.Mock(spec=Split) async def get(*_): @@ -3543,6 +3623,8 @@ async def get(*_): }, mocker.Mock(), recorder, + internal_events_queue, + events_manager, mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), diff --git a/tests/client/test_manager.py b/tests/client/test_manager.py index 5cb0d2e1..c5454f67 100644 --- a/tests/client/test_manager.py +++ b/tests/client/test_manager.py @@ -1,6 +1,7 @@ """SDK main manager test module.""" import pytest import queue +import asyncio from splitio.client.factory import SplitFactory from splitio.client.manager import SplitManager, SplitManagerAsync, _LOGGER as _logger @@ -90,9 +91,10 @@ class SplitManagerAsyncTests(object): # pylint: disable=too-few-public-methods @pytest.mark.asyncio async def test_manager_calls(self, mocker): + internal_events_queue = asyncio.Queue() telemetry_storage = InMemoryTelemetryStorageAsync() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(internal_events_queue) factory = mocker.Mock(spec=SplitFactory) factory._storages = {'split': storage} diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index dc83cc36..edf510c0 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -5,6 +5,7 @@ import pytest import copy import queue +import asyncio from splitio.models.splits import Split, Status, from_raw, Prerequisites from splitio.models import segments @@ -425,9 +426,11 @@ def test_evaluate_treatment_with_fallback(self, mocker): @pytest.mark.asyncio async def test_evaluate_treatment_with_rbs_in_condition_async(self): e = evaluator.Evaluator(splitters.Splitter()) - splits_storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() - segment_storage = InMemorySegmentStorageAsync() + internal_events_queue = asyncio.Queue() + + splits_storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) evaluation_facctory = AsyncEvaluationDataFactory(splits_storage, segment_storage, rbs_storage) rbs_segments = os.path.join(os.path.dirname(__file__), 'files', 'rule_base_segments.json') @@ -451,9 +454,10 @@ async def test_using_segment_in_excluded_async(self): with open(rbs_segments, 'r') as flo: data = json.loads(flo.read()) e = evaluator.Evaluator(splitters.Splitter()) - splits_storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() - segment_storage = InMemorySegmentStorageAsync() + internal_events_queue = asyncio.Queue() + splits_storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) evaluation_facctory = AsyncEvaluationDataFactory(splits_storage, segment_storage, rbs_storage) mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False) @@ -476,9 +480,10 @@ async def test_using_rbs_in_excluded_async(self): with open(rbs_segments, 'r') as flo: data = json.loads(flo.read()) e = evaluator.Evaluator(splitters.Splitter()) - splits_storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() - segment_storage = InMemorySegmentStorageAsync() + internal_events_queue = asyncio.Queue() + splits_storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) evaluation_facctory = AsyncEvaluationDataFactory(splits_storage, segment_storage, rbs_storage) mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False) @@ -500,9 +505,10 @@ async def test_prerequisites(self): with open(splits_load, 'r') as flo: data = json.loads(flo.read()) e = evaluator.Evaluator(splitters.Splitter()) - splits_storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() - segment_storage = InMemorySegmentStorageAsync() + internal_events_queue = asyncio.Queue() + splits_storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) evaluation_facctory = AsyncEvaluationDataFactory(splits_storage, segment_storage, rbs_storage) rbs = rule_based_segments.from_raw(data["rbs"]["d"][0]) @@ -590,9 +596,10 @@ async def test_get_context(self): """Test context.""" mocked_split = Split('some', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, [Prerequisites('split2', ['on'])]) split2 = Split('split2', 12345, False, 'off', 'user', Status.ACTIVE, 12, split_conditions, 1.2, 100, 1234, {}, None, False, []) - flag_storage = InMemorySplitStorageAsync([]) - segment_storage = InMemorySegmentStorageAsync() - rbs_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + flag_storage = InMemorySplitStorageAsync(internal_events_queue, []) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rbs_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) await flag_storage.update([mocked_split, split2], [], -1) rbs = copy.deepcopy(rbs_raw) rbs['conditions'].append( diff --git a/tests/events/test_events_delivery.py b/tests/events/test_events_delivery.py index fc2d5464..27076de4 100644 --- a/tests/events/test_events_delivery.py +++ b/tests/events/test_events_delivery.py @@ -1,4 +1,6 @@ """EventsManager test module.""" +import pytest + from splitio.models.events import SdkEvent, SdkInternalEvent from splitio.events.events_metadata import EventsMetadata from splitio.events.events_delivery import EventsDelivery @@ -17,11 +19,26 @@ def test_firing_events(self): events_delivery.deliver(SdkEvent.SDK_READY, metadata, self._sdk_ready_callback) assert self.sdk_ready_flag self._verify_metadata(metadata) + + @pytest.mark.asyncio + async def test_firing_events(self): + events_delivery = EventsDelivery() + + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1" }) + self.sdk_ready_flag = False + self.metadata = None + await events_delivery.deliver_async(SdkEvent.SDK_READY, metadata, self._sdk_ready_callback_async) + assert self.sdk_ready_flag + self._verify_metadata(metadata) def _sdk_ready_callback(self, metadata): self.sdk_ready_flag = True self.metadata = metadata + async def _sdk_ready_callback_async(self, metadata): + self.sdk_ready_flag = True + self.metadata = metadata + def _verify_metadata(self, metadata): assert metadata.get_type() == self.metadata.get_type() assert metadata.get_names() == self.metadata.get_names() \ No newline at end of file diff --git a/tests/events/test_events_manager.py b/tests/events/test_events_manager.py index 48c6fa45..35cf6161 100644 --- a/tests/events/test_events_manager.py +++ b/tests/events/test_events_manager.py @@ -1,10 +1,12 @@ """EventsManager test module.""" import pytest +import asyncio + from splitio.models.events import SdkEvent, SdkInternalEvent from splitio.events.events_metadata import EventsMetadata from splitio.events.events_manager_config import EventsManagerConfig from splitio.events.events_delivery import EventsDelivery -from splitio.events.events_manager import EventsManager +from splitio.events.events_manager import EventsManager, EventsManagerAsync from splitio.events.events_metadata import SdkEventType class EventsManagerTests(object): @@ -95,6 +97,105 @@ def _sdk_timeout_callback(self, metadata): self.sdk_timed_out_flag = True self.metadata = metadata + def _verify_metadata(self, metadata): + assert metadata.get_type() == self.metadata.get_type() + assert metadata.get_names() == self.metadata.get_names() + +class EventsManagerAsyncTests(object): + """Tests for EventsManagerAsync.""" + + sdk_ready_flag = False + sdk_timed_out_flag = False + sdk_update_flag = False + metadata = None + + @pytest.mark.asyncio + async def test_firing_events(self): + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + await events_manager.register(SdkEvent.SDK_READY, self._sdk_ready_callback) + await events_manager.register(SdkEvent.SDK_UPDATE, self._sdk_update_callback) + + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1" }) + await events_manager.notify_internal_event(SdkInternalEvent.FLAGS_UPDATED, metadata) + await events_manager.notify_internal_event(SdkInternalEvent.FLAG_KILLED_NOTIFICATION, metadata) + await events_manager.notify_internal_event(SdkInternalEvent.RB_SEGMENTS_UPDATED, metadata) + await events_manager.notify_internal_event(SdkInternalEvent.SEGMENTS_UPDATED, metadata) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert not self.sdk_update_flag + + self._reset_flags() + await events_manager.notify_internal_event(SdkInternalEvent.SDK_TIMED_OUT, metadata) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag # not registered yet + assert not self.sdk_update_flag + + await events_manager.register(SdkEvent.SDK_READY_TIMED_OUT, self._sdk_timeout_callback) + await events_manager.notify_internal_event(SdkInternalEvent.SDK_TIMED_OUT, metadata) + await asyncio.sleep(.3) + assert not self.sdk_ready_flag + assert self.sdk_timed_out_flag + assert not self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + await events_manager.notify_internal_event(SdkInternalEvent.SDK_READY, metadata) + await asyncio.sleep(.3) + assert self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert not self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + await events_manager.notify_internal_event(SdkInternalEvent.RB_SEGMENTS_UPDATED, metadata) + await asyncio.sleep(.3) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + await events_manager.notify_internal_event(SdkInternalEvent.FLAG_KILLED_NOTIFICATION, metadata) + await asyncio.sleep(.3) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + await events_manager.notify_internal_event(SdkInternalEvent.FLAGS_UPDATED, metadata) + await asyncio.sleep(.3) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert self.sdk_update_flag + self._verify_metadata(metadata) + + self._reset_flags() + await events_manager.notify_internal_event(SdkInternalEvent.SEGMENTS_UPDATED, metadata) + await asyncio.sleep(.3) + assert not self.sdk_ready_flag + assert not self.sdk_timed_out_flag + assert self.sdk_update_flag + self._verify_metadata(metadata) + + def _reset_flags(self): + self.sdk_ready_flag = False + self.sdk_timed_out_flag = False + self.sdk_update_flag = False + self.metadata = None + + async def _sdk_ready_callback(self, metadata): + self.sdk_ready_flag = True + self.metadata = metadata + + async def _sdk_update_callback(self, metadata): + self.sdk_update_flag = True + self.metadata = metadata + + async def _sdk_timeout_callback(self, metadata): + self.sdk_timed_out_flag = True + self.metadata = metadata + def _verify_metadata(self, metadata): assert metadata.get_type() == self.metadata.get_type() assert metadata.get_names() == self.metadata.get_names() \ No newline at end of file diff --git a/tests/events/test_events_task.py b/tests/events/test_events_task.py index 17d23bec..d667f76c 100644 --- a/tests/events/test_events_task.py +++ b/tests/events/test_events_task.py @@ -2,16 +2,17 @@ import pytest import queue import time +import asyncio from splitio.models.events import SdkInternalEvent from splitio.models.notification import SdkInternalEventNotification from splitio.events.events_metadata import EventsMetadata from splitio.events.events_metadata import SdkEventType -from splitio.events.events_task import EventsTask +from splitio.events.events_task import EventsTask, EventsTaskAsync class EventsTaskTests(object): - """Tests for EventsManager.""" + """Tests for EventsTask.""" internal_event = None metadata = None @@ -71,4 +72,68 @@ def _verify_metadata(self, metadata): assert metadata.get_type() == self.metadata.get_type() assert metadata.get_names() == self.metadata.get_names() + +class EventsTaskAsyncTests(object): + """Tests for EventsTaskAsyncr.""" + + internal_event = None + metadata = None + + @pytest.mark.asyncio + async def test_firing_events(self): + events_queue = asyncio.Queue() + events_task = EventsTaskAsync(self._event_callback, events_queue) + + events_task.start() + assert events_task.is_running() + + metadata = EventsMetadata(SdkEventType.FLAG_UPDATE, { "feature1" }) + await events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_READY, metadata)) + await asyncio.sleep(.5) + assert self.internal_event == SdkInternalEvent.SDK_READY + self._verify_metadata(metadata) + + self._reset_flags() + await events_queue.put(SdkInternalEventNotification(SdkInternalEvent.RB_SEGMENTS_UPDATED, metadata)) + await asyncio.sleep(.5) + assert self.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED + self._verify_metadata(metadata) + + await events_task.stop() + await asyncio.sleep(.5) + assert not events_task.is_running() + + @pytest.mark.asyncio + async def test_on_error(self): + events_queue = asyncio.Queue() + + async def handler_sync(internal_event, metadata): + raise Exception('some') + + events_task = EventsTaskAsync(handler_sync, events_queue) + events_task.start() + assert events_task.is_running() + + await events_queue.put(SdkInternalEventNotification(SdkInternalEvent.SDK_READY, None)) + + with pytest.raises(Exception): + events_task._handler() + + assert events_task.is_running() + await events_task.stop() + await asyncio.sleep(1) + assert not events_task.is_running() + + def _reset_flags(self): + self.internal_event = None + self.metadata = None + + async def _event_callback(self, internal_event, metadata): + self.internal_event = internal_event + self.metadata = metadata + + def _verify_metadata(self, metadata): + assert metadata.get_type() == self.metadata.get_type() + assert metadata.get_names() == self.metadata.get_names() + \ No newline at end of file diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 0b2fe70f..c243951f 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -24,9 +24,9 @@ from splitio.engine.impressions.manager import Counter as ImpressionsCounter from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync from splitio.events.events_delivery import EventsDelivery -from splitio.events.events_manager import EventsManager +from splitio.events.events_manager import EventsManager, EventsManagerAsync from splitio.events.events_manager_config import EventsManagerConfig -from splitio.events.events_task import EventsTask +from splitio.events.events_task import EventsTask, EventsTaskAsync from splitio.models import splits, segments, rule_based_segments from splitio.models.events import SdkEvent from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator @@ -2608,9 +2608,12 @@ def setup_method(self): async def _setup_method(self): """Prepare storages with test data.""" - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') with open(split_fn, 'r') as flo: @@ -2651,6 +2654,8 @@ async def _setup_method(self): storages, True, recorder, + internal_events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -2780,9 +2785,12 @@ def setup_method(self): async def _setup_method(self): """Prepare storages with test data.""" - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() - rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') with open(split_fn, 'r') as flo: data = json.loads(flo.read()) @@ -2823,6 +2831,8 @@ async def _setup_method(self): storages, True, recorder, + internal_events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -3125,6 +3135,9 @@ def setup_method(self): async def _setup_method(self): """Prepare storages with test data.""" + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') redis_client = await build_async(DEFAULT_CONFIG.copy()) await self._clear_cache(redis_client) @@ -3177,6 +3190,8 @@ async def _setup_method(self): storages, True, recorder, + internal_events_queue, + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, @@ -3348,6 +3363,9 @@ def setup_method(self): async def _setup_method(self): """Prepare storages with test data.""" + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') redis_client = await build_async(DEFAULT_CONFIG.copy()) await self._clear_cache(redis_client) @@ -3400,6 +3418,8 @@ async def _setup_method(self): storages, True, recorder, + internal_events_queue, + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, @@ -3621,6 +3641,9 @@ def setup_method(self): async def _setup_method(self): """Prepare storages with test data.""" + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapterAsync() split_storage = PluggableSplitStorageAsync(self.pluggable_storage_adapter, 'myprefix') @@ -3651,6 +3674,8 @@ async def _setup_method(self): storages, True, recorder, + internal_events_queue, + events_manager, RedisManagerAsync(PluggableSynchronizerAsync()), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -3850,6 +3875,9 @@ def setup_method(self): async def _setup_method(self): """Prepare storages with test data.""" + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapterAsync() split_storage = PluggableSplitStorageAsync(self.pluggable_storage_adapter) @@ -3881,6 +3909,8 @@ async def _setup_method(self): storages, True, recorder, + internal_events_queue, + events_manager, RedisManagerAsync(PluggableSynchronizerAsync()), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -4066,6 +4096,9 @@ def setup_method(self): async def _setup_method(self): """Prepare storages with test data.""" + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') self.pluggable_storage_adapter = StorageMockAdapterAsync() split_storage = PluggableSplitStorageAsync(self.pluggable_storage_adapter) @@ -4117,6 +4150,8 @@ async def _setup_method(self): storages, True, recorder, + internal_events_queue, + events_manager, manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -4303,8 +4338,11 @@ class InMemoryImpressionsToggleIntegrationAsyncTests(object): @pytest.mark.asyncio async def test_optimized(self): - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) await split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), splits.from_raw(splits_json['splitChange1_1']['ff']['d'][1]), @@ -4319,7 +4357,7 @@ async def test_optimized(self): storages = { 'splits': split_storage, 'segments': segment_storage, - 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(), + 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(internal_events_queue), 'impressions': InMemoryImpressionStorageAsync(5000, telemetry_runtime_producer), 'events': InMemoryEventStorageAsync(5000, telemetry_runtime_producer), } @@ -4331,6 +4369,8 @@ async def test_optimized(self): storages, True, recorder, + internal_events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -4364,8 +4404,11 @@ async def test_optimized(self): @pytest.mark.asyncio async def test_debug(self): - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) await split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), splits.from_raw(splits_json['splitChange1_1']['ff']['d'][1]), @@ -4380,7 +4423,7 @@ async def test_debug(self): storages = { 'splits': split_storage, 'segments': segment_storage, - 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(), + 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(internal_events_queue), 'impressions': InMemoryImpressionStorageAsync(5000, telemetry_runtime_producer), 'events': InMemoryEventStorageAsync(5000, telemetry_runtime_producer), } @@ -4392,6 +4435,8 @@ async def test_debug(self): storages, True, recorder, + internal_events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -4425,8 +4470,11 @@ async def test_debug(self): @pytest.mark.asyncio async def test_none(self): - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) await split_storage.update([splits.from_raw(splits_json['splitChange1_1']['ff']['d'][0]), splits.from_raw(splits_json['splitChange1_1']['ff']['d'][1]), @@ -4441,7 +4489,7 @@ async def test_none(self): storages = { 'splits': split_storage, 'segments': segment_storage, - 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(), + 'rule_based_segments': InMemoryRuleBasedSegmentStorageAsync(internal_events_queue), 'impressions': InMemoryImpressionStorageAsync(5000, telemetry_runtime_producer), 'events': InMemoryEventStorageAsync(5000, telemetry_runtime_producer), } @@ -4453,6 +4501,8 @@ async def test_none(self): storages, True, recorder, + internal_events_queue, + events_manager, None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), @@ -4491,6 +4541,9 @@ class RedisImpressionsToggleIntegrationAsyncTests(object): @pytest.mark.asyncio async def test_optimized(self): + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + """Prepare storages with test data.""" metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') redis_client = await build_async(DEFAULT_CONFIG.copy()) @@ -4522,6 +4575,8 @@ async def test_optimized(self): storages, True, recorder, + internal_events_queue, + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(None) @@ -4562,6 +4617,9 @@ async def test_optimized(self): @pytest.mark.asyncio async def test_debug(self): """Prepare storages with test data.""" + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') redis_client = await build_async(DEFAULT_CONFIG.copy()) split_storage = RedisSplitStorageAsync(redis_client, True) @@ -4592,6 +4650,8 @@ async def test_debug(self): storages, True, recorder, + internal_events_queue, + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(None) @@ -4632,6 +4692,9 @@ async def test_debug(self): @pytest.mark.asyncio async def test_none(self): """Prepare storages with test data.""" + internal_events_queue = asyncio.Queue() + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name') redis_client = await build_async(DEFAULT_CONFIG.copy()) split_storage = RedisSplitStorageAsync(redis_client, True) @@ -4662,6 +4725,8 @@ async def test_none(self): storages, True, recorder, + internal_events_queue, + events_manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), fallback_treatment_calculator=FallbackTreatmentCalculator(None) diff --git a/tests/push/test_split_worker.py b/tests/push/test_split_worker.py index 198372a7..28b5408d 100644 --- a/tests/push/test_split_worker.py +++ b/tests/push/test_split_worker.py @@ -523,8 +523,9 @@ async def update(feature_flag_add, feature_flag_delete, change_number): @pytest.mark.asyncio async def test_fetch_segment(self, mocker): q = asyncio.Queue() - split_storage = InMemorySplitStorageAsync() - segment_storage = InMemorySegmentStorageAsync() + internal_events_queue = asyncio.Queue() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) self.segment_name = None async def segment_handler_sync(segment_name, change_number): diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index a37a1a4d..354da30e 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -4,6 +4,7 @@ import pytest import copy import queue +import asyncio from splitio.models.splits import Split from splitio.models.segments import Segment @@ -413,7 +414,7 @@ class InMemorySplitStorageAsyncTests(object): @pytest.mark.asyncio async def test_storing_retrieving_splits(self, mocker): """Test storing and retrieving splits works.""" - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) split = mocker.Mock(spec=Split) name_property = mocker.PropertyMock() @@ -448,7 +449,7 @@ async def test_get_splits(self, mocker): type(split1).sets = sets_property type(split2).sets = sets_property - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) await storage.update([split1, split2], [], -1) splits = await storage.fetch_many(['split1', 'split2', 'split3']) @@ -460,7 +461,7 @@ async def test_get_splits(self, mocker): @pytest.mark.asyncio async def test_store_get_changenumber(self): """Test that storing and retrieving change numbers works.""" - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) assert await storage.get_change_number() == -1 await storage.update([], [], 5) assert await storage.get_change_number() == 5 @@ -481,7 +482,7 @@ async def test_get_split_names(self, mocker): type(split1).sets = sets_property type(split2).sets = sets_property - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) await storage.update([split1, split2], [], -1) assert set(await storage.get_split_names()) == set(['split1', 'split2']) @@ -502,7 +503,7 @@ async def test_get_all_splits(self, mocker): type(split1).sets = sets_property type(split2).sets = sets_property - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) await storage.update([split1, split2], [], -1) all_splits = await storage.get_all_splits() @@ -537,7 +538,7 @@ async def test_is_valid_traffic_type(self, mocker): type(split2).sets = sets_property type(split3).sets = sets_property - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) await storage.update([split1], [], -1) assert await storage.is_valid_traffic_type('user') is True @@ -566,7 +567,7 @@ async def test_is_valid_traffic_type(self, mocker): @pytest.mark.asyncio async def test_traffic_type_inc_dec_logic(self, mocker): """Test that adding/removing split, handles traffic types correctly.""" - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) split1 = mocker.Mock() name1_prop = mocker.PropertyMock() @@ -599,7 +600,7 @@ async def test_traffic_type_inc_dec_logic(self, mocker): @pytest.mark.asyncio async def test_kill_locally(self): """Test kill local.""" - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) split = Split('some_split', 123456789, False, 'some', 'traffic_type', 'ACTIVE', 1) @@ -620,7 +621,7 @@ async def test_kill_locally(self): @pytest.mark.asyncio async def test_flag_sets_with_config_sets(self): - storage = InMemorySplitStorageAsync(['set10', 'set02', 'set05']) + storage = InMemorySplitStorageAsync(asyncio.Queue(), ['set10', 'set02', 'set05']) assert storage.flag_set_filter.flag_sets == {'set10', 'set02', 'set05'} assert storage.flag_set_filter.should_filter @@ -666,7 +667,7 @@ async def test_flag_sets_with_config_sets(self): @pytest.mark.asyncio async def test_flag_sets_withut_config_sets(self): - storage = InMemorySplitStorageAsync() + storage = InMemorySplitStorageAsync(asyncio.Queue()) assert storage.flag_set_filter.flag_sets == set({}) assert not storage.flag_set_filter.should_filter @@ -796,7 +797,7 @@ class InMemorySegmentStorageAsyncTests(object): @pytest.mark.asyncio async def test_segment_storage_retrieval(self, mocker): """Test storing and retrieving segments.""" - storage = InMemorySegmentStorageAsync() + storage = InMemorySegmentStorageAsync(asyncio.Queue()) segment = mocker.Mock(spec=Segment) name_property = mocker.PropertyMock() name_property.return_value = 'some_segment' @@ -809,14 +810,14 @@ async def test_segment_storage_retrieval(self, mocker): @pytest.mark.asyncio async def test_change_number(self, mocker): """Test storing and retrieving segment changeNumber.""" - storage = InMemorySegmentStorageAsync() + storage = InMemorySegmentStorageAsync(asyncio.Queue()) await storage.set_change_number('some_segment', 123) # Change number is not updated if segment doesn't exist assert await storage.get_change_number('some_segment') is None assert await storage.get_change_number('nonexistant-segment') is None # Change number is updated if segment does exist. - storage = InMemorySegmentStorageAsync() + storage = InMemorySegmentStorageAsync(asyncio.Queue()) segment = mocker.Mock(spec=Segment) name_property = mocker.PropertyMock() name_property.return_value = 'some_segment' @@ -828,7 +829,7 @@ async def test_change_number(self, mocker): @pytest.mark.asyncio async def test_segment_contains(self, mocker): """Test using storage to determine whether a key belongs to a segment.""" - storage = InMemorySegmentStorageAsync() + storage = InMemorySegmentStorageAsync(asyncio.Queue()) segment = mocker.Mock(spec=Segment) name_property = mocker.PropertyMock() name_property.return_value = 'some_segment' @@ -841,7 +842,7 @@ async def test_segment_contains(self, mocker): @pytest.mark.asyncio async def test_segment_update(self): """Test updating a segment.""" - storage = InMemorySegmentStorageAsync() + storage = InMemorySegmentStorageAsync(asyncio.Queue()) segment = Segment('some_segment', ['key1', 'key2', 'key3'], 123) await storage.put(segment) assert await storage.get('some_segment') == segment @@ -1973,7 +1974,7 @@ class InMemoryRuleBasedSegmentStorageAsyncTests(object): @pytest.mark.asyncio async def test_storing_retrieving_segments(self, mocker): """Test storing and retrieving splits works.""" - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(asyncio.Queue()) segment1 = mocker.Mock(spec=RuleBasedSegment) name_property = mocker.PropertyMock() @@ -1996,7 +1997,7 @@ async def test_storing_retrieving_segments(self, mocker): @pytest.mark.asyncio async def test_store_get_changenumber(self): """Test that storing and retrieving change numbers works.""" - storage = InMemoryRuleBasedSegmentStorageAsync() + storage = InMemoryRuleBasedSegmentStorageAsync(asyncio.Queue()) assert await storage.get_change_number() == -1 await storage.update([], [], 5) assert await storage.get_change_number() == 5 @@ -2021,7 +2022,7 @@ async def test_contains(self): raw3 = copy.deepcopy(raw) raw3["name"] = "segment3" segment3 = rule_based_segments.from_raw(raw3) - storage = InMemoryRuleBasedSegmentStorageAsync() + storage = InMemoryRuleBasedSegmentStorageAsync(asyncio.Queue()) await storage.update([segment1, segment2, segment3], [], -1) assert await storage.contains(["segment1"]) assert await storage.contains(["segment1", "segment3"]) diff --git a/tests/sync/test_segments_synchronizer.py b/tests/sync/test_segments_synchronizer.py index a3657e98..5b405ef8 100644 --- a/tests/sync/test_segments_synchronizer.py +++ b/tests/sync/test_segments_synchronizer.py @@ -686,7 +686,7 @@ async def get_segment_names(): return ['segmentA', 'segmentB', 'segmentC'] split_storage.get_segment_names = get_segment_names - storage = InMemorySegmentStorageAsync() + storage = InMemorySegmentStorageAsync(asyncio.Queue()) segment_a = {'name': 'segmentA', 'added': ['key1', 'key2', 'key3'], 'removed': [], 'since': -1, 'till': 123} @@ -767,7 +767,7 @@ async def test_reading_json(self, mocker): async with aiofiles.open("./segmentA.json", "w") as f: await f.write('{"name": "segmentA", "added": ["key1", "key2", "key3"], "removed": [],"since": -1, "till": 123}') split_storage = mocker.Mock(spec=InMemorySplitStorageAsync) - storage = InMemorySegmentStorageAsync() + storage = InMemorySegmentStorageAsync(asyncio.Queue()) segments_synchronizer = LocalSegmentSynchronizerAsync('.', split_storage, storage) assert await segments_synchronizer.synchronize_segments(['segmentA']) diff --git a/tests/sync/test_splits_synchronizer.py b/tests/sync/test_splits_synchronizer.py index ca3daa82..b27606a4 100644 --- a/tests/sync/test_splits_synchronizer.py +++ b/tests/sync/test_splits_synchronizer.py @@ -792,8 +792,9 @@ async def clear(): @pytest.mark.asyncio async def test_sync_flag_sets_with_config_sets(self, mocker): """Test split sync with flag sets.""" - storage = InMemorySplitStorageAsync(['set1', 'set2']) - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + storage = InMemorySplitStorageAsync(internal_events_queue, ['set1', 'set2']) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) split = self.splits[0].copy() split['name'] = 'second' @@ -840,8 +841,9 @@ async def get_changes(*args, **kwargs): @pytest.mark.asyncio async def test_sync_flag_sets_without_config_sets(self, mocker): """Test split sync with flag sets.""" - storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) split = self.splits[0].copy() split['name'] = 'second' splits1 = [self.splits[0].copy(), split] @@ -1261,8 +1263,9 @@ async def test_synchronize_splits_error(self, mocker): @pytest.mark.asyncio async def test_synchronize_splits(self, mocker): """Test split sync.""" - storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) async def read_splits_from_json_file(*args, **kwargs): return self.payload @@ -1306,8 +1309,9 @@ async def read_splits_from_json_file(*args, **kwargs): @pytest.mark.asyncio async def test_sync_flag_sets_with_config_sets(self, mocker): """Test split sync with flag sets.""" - storage = InMemorySplitStorageAsync(['set1', 'set2']) - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + storage = InMemorySplitStorageAsync(internal_events_queue, ['set1', 'set2']) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) split = self.payload["ff"]["d"][0].copy() split['name'] = 'second' @@ -1349,8 +1353,9 @@ async def read_feature_flags_from_json_file(*args, **kwargs): @pytest.mark.asyncio async def test_sync_flag_sets_without_config_sets(self, mocker): """Test split sync with flag sets.""" - storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) split = self.payload["ff"]["d"][0].copy() split['name'] = 'second' @@ -1393,8 +1398,9 @@ async def test_reading_json(self, mocker): """Test reading json file.""" async with aiofiles.open("./splits.json", "w") as f: await f.write(json.dumps(self.payload)) - storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) split_synchronizer = LocalSplitSynchronizerAsync("./splits.json", storage, rbs_storage, LocalhostMode.JSON) await split_synchronizer.synchronize_splits() diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index 258077d4..179d7978 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -2,6 +2,7 @@ import unittest.mock as mock import pytest import queue +import asyncio from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitTasks, SplitSynchronizers, LocalhostSynchronizer, LocalhostSynchronizerAsync, RedisSynchronizer, RedisSynchronizerAsync from splitio.tasks.split_sync import SplitSynchronizationTask, SplitSynchronizationTaskAsync @@ -502,8 +503,9 @@ async def get_segment_names_rbs(): @pytest.mark.asyncio async def test_synchronize_splits(self, mocker): - split_storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) split_api = mocker.Mock() async def fetch_splits(change, rb, options): @@ -513,7 +515,7 @@ async def fetch_splits(change, rb, options): split_api.fetch_splits = fetch_splits split_sync = SplitSynchronizerAsync(split_api, split_storage, rbs_storage) - segment_storage = InMemorySegmentStorageAsync() + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) segment_api = mocker.Mock() async def get_change_number(): @@ -545,8 +547,9 @@ async def fetch_segment(segment_name, change, options): @pytest.mark.asyncio async def test_synchronize_splits_calling_segment_sync_once(self, mocker): - split_storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) async def get_change_number(): return 123 split_storage.get_change_number = get_change_number @@ -580,8 +583,9 @@ async def segment_exist_in_storage(segment): @pytest.mark.asyncio async def test_sync_all(self, mocker): - split_storage = InMemorySplitStorageAsync() - rbs_storage = InMemoryRuleBasedSegmentStorageAsync() + internal_events_queue = asyncio.Queue() + split_storage = InMemorySplitStorageAsync(internal_events_queue) + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(internal_events_queue) async def get_change_number(): return 123 split_storage.get_change_number = get_change_number @@ -612,7 +616,7 @@ async def fetch_splits(change, rb, options): split_api.fetch_splits = fetch_splits split_sync = SplitSynchronizerAsync(split_api, split_storage, rbs_storage) - segment_storage = InMemorySegmentStorageAsync() + segment_storage = InMemorySegmentStorageAsync(internal_events_queue) async def get_change_number(segment): return 123 segment_storage.get_change_number = get_change_number diff --git a/tests/sync/test_telemetry.py b/tests/sync/test_telemetry.py index 5b41b344..dd8119e2 100644 --- a/tests/sync/test_telemetry.py +++ b/tests/sync/test_telemetry.py @@ -2,6 +2,7 @@ import unittest.mock as mock import pytest import queue +import asyncio from splitio.sync.telemetry import TelemetrySynchronizer, TelemetrySynchronizerAsync, InMemoryTelemetrySubmitter, InMemoryTelemetrySubmitterAsync from splitio.engine.telemetry import TelemetryStorageConsumer, TelemetryStorageConsumerAsync @@ -184,9 +185,9 @@ async def test_synchronize_telemetry(self, mocker): api = mocker.Mock(spec=TelemetryAPI) telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_consumer = TelemetryStorageConsumerAsync(telemetry_storage) - split_storage = InMemorySplitStorageAsync() + split_storage = InMemorySplitStorageAsync(asyncio.Queue()) await split_storage.update([Split('split1', 1234, 1, False, 'user', Status.ACTIVE, 123)], [], -1) - segment_storage = InMemorySegmentStorageAsync() + segment_storage = InMemorySegmentStorageAsync(asyncio.Queue()) await segment_storage.put(Segment('segment1', [], 123)) telemetry_submitter = InMemoryTelemetrySubmitterAsync(telemetry_consumer, split_storage, segment_storage, api) diff --git a/tests/util/test_storage_helper.py b/tests/util/test_storage_helper.py index dc75caa0..60e83e8c 100644 --- a/tests/util/test_storage_helper.py +++ b/tests/util/test_storage_helper.py @@ -1,6 +1,7 @@ """Storage Helper tests.""" import pytest import queue +import asyncio from splitio.util.storage_helper import update_feature_flag_storage, get_valid_flag_sets, combine_valid_flag_sets, \ update_rule_based_segment_storage, update_rule_based_segment_storage_async, update_feature_flag_storage_async, \ @@ -201,7 +202,7 @@ def test_get_standard_segment_in_rbs_storage(self, mocker): @pytest.mark.asyncio async def test_get_standard_segment_in_rbs_storage(self, mocker): - storage = InMemoryRuleBasedSegmentStorageAsync() + storage = InMemoryRuleBasedSegmentStorageAsync(asyncio.Queue()) segments = await update_rule_based_segment_storage_async(storage, [self.rbs], 123) assert await get_standard_segment_names_in_rbs_storage_async(storage) == {'excluded_segment', 'employees'} From f2ad152ed3eb99ed3fa304378503ce82f0efcf50 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 21 Jan 2026 12:50:24 -0800 Subject: [PATCH 16/26] finish tests --- splitio/events/events_task.py | 2 +- tests/client/test_factory.py | 106 +++++++++++++++- tests/integration/test_client_e2e.py | 168 +++++++++++++++++++++++++ tests/storage/test_inmemory_storage.py | 75 +++++++++++ tests/tasks/util/test_asynctask.py | 4 +- 5 files changed, 351 insertions(+), 4 deletions(-) diff --git a/splitio/events/events_task.py b/splitio/events/events_task.py index 8158dc04..3c7e34f3 100644 --- a/splitio/events/events_task.py +++ b/splitio/events/events_task.py @@ -133,7 +133,7 @@ def start(self): self._running = True _LOGGER.debug('Starting SDK Event Task worker') - asyncio.get_running_loop().create_task(self._run()) + asyncio.get_running_loop().create_task(self._run(), name="EventsTaskWorker") async def stop(self, stop_flag=None): """Stop worker.""" diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 45e64c72..64da9541 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -1109,4 +1109,108 @@ async def _make_factory_with_apikey(apikey, *_, **__): await asyncio.sleep(0.5) assert factory.destroyed assert len(build_redis.mock_calls) == 2 - \ No newline at end of file + + @pytest.mark.asyncio + async def test_internal_ready_event_notification(self, mocker): + """Test that a client with in-memory storage is sending internal events correctly.""" + # Setup synchronizer + def _split_synchronizer(self, ready_flag, some, auth_api, streaming_enabled, sdk_matadata, telemetry_runtime_producer, sse_url=None, client_key=None): + synchronizer = mocker.Mock(spec=SynchronizerAsync) + async def sync_all(*_): + return None + synchronizer.sync_all = sync_all + + def start_periodic_fetching(): + pass + synchronizer.start_periodic_fetching = start_periodic_fetching + + def start_periodic_data_recording(): + pass + synchronizer.start_periodic_data_recording = start_periodic_data_recording + + self._ready_flag = ready_flag + self._synchronizer = synchronizer + self._streaming_enabled = False + self._telemetry_runtime_producer = telemetry_runtime_producer + + mocker.patch('splitio.sync.manager.ManagerAsync.__init__', new=_split_synchronizer) + + async def synchronize_config(*_): + await asyncio.sleep(2) + pass + mocker.patch('splitio.sync.telemetry.InMemoryTelemetrySubmitterAsync.synchronize_config', new=synchronize_config) + + async def record_ready_time(*_): + pass + mocker.patch('splitio.models.telemetry.TelemetryConfigAsync.record_ready_time', new=record_ready_time) + + async def record_active_and_redundant_factories(*_): + pass + mocker.patch('splitio.models.telemetry.TelemetryConfigAsync.record_active_and_redundant_factories', new=record_active_and_redundant_factories) + + # Start factory and make assertions + factory = await get_factory_async('some_api_key', config={'streamingEmabled': False}) + for task in asyncio.all_tasks(): + if task.get_name() == "EventsTaskWorker": + task.cancel() + try: + await factory.block_until_ready(3) + except: + pass + await asyncio.sleep(.2) + event = await factory._internal_events_queue.get() + assert event.internal_event == SdkInternalEvent.SDK_READY + assert event.metadata == None + await factory.destroy() + + @pytest.mark.asyncio + async def test_internal_timeout_event_notification(self, mocker): + """Test that a client with in-memory storage is sending internal events correctly.""" + # Setup synchronizer + def _split_synchronizer(self, ready_flag, some, auth_api, streaming_enabled, sdk_matadata, telemetry_runtime_producer, sse_url=None, client_key=None): + synchronizer = mocker.Mock(spec=SynchronizerAsync) + async def sync_all(*_): + return None + synchronizer.sync_all = sync_all + + def start_periodic_fetching(): + pass + synchronizer.start_periodic_fetching = start_periodic_fetching + + def start_periodic_data_recording(): + pass + synchronizer.start_periodic_data_recording = start_periodic_data_recording + + self._ready_flag = ready_flag + self._synchronizer = synchronizer + self._streaming_enabled = False + self._telemetry_runtime_producer = telemetry_runtime_producer + + mocker.patch('splitio.sync.manager.ManagerAsync.__init__', new=_split_synchronizer) + + async def synchronize_config(*_): + await asyncio.sleep(3) + pass + mocker.patch('splitio.sync.telemetry.InMemoryTelemetrySubmitterAsync.synchronize_config', new=synchronize_config) + + async def record_ready_time(*_): + pass + mocker.patch('splitio.models.telemetry.TelemetryConfigAsync.record_ready_time', new=record_ready_time) + + async def record_active_and_redundant_factories(*_): + pass + mocker.patch('splitio.models.telemetry.TelemetryConfigAsync.record_active_and_redundant_factories', new=record_active_and_redundant_factories) + + # Start factory and make assertions + factory = await get_factory_async('some_api_key', config={'streamingEmabled': False}) + for task in asyncio.all_tasks(): + if task.get_name() == "EventsTaskWorker": + task.cancel() + try: + await factory.block_until_ready(1) + except: + pass + event = await factory._internal_events_queue.get() + assert event.internal_event == SdkInternalEvent.SDK_TIMED_OUT + assert event.metadata == None + await factory.destroy() diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index c243951f..7181f141 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -2600,6 +2600,174 @@ def _ready_callback(self, metadata): def _timeout_callback(self, metadata): self.timeout_flag = True +class InMemoryEventsNotificationAsyncTests(object): + """Inmemory storage-based events notification tests.""" + + ready_flag = False + timeout_flag = False + + @pytest.mark.asyncio + async def test_sdk_timeout_fire(self): + """Prepare storages with test data.""" + factory2 = await get_factory_async('some_api_key') + client = factory2.client() + await client.on(SdkEvent.SDK_READY_TIMED_OUT, self._timeout_callback) + try: + await factory2.block_until_ready(1) + except Exception as e: + pass + + await asyncio.sleep(1) + assert self.timeout_flag + + """Shut down the factory.""" + await factory2.destroy() + + @pytest.mark.asyncio + async def test_sdk_ready(self): + """Prepare storages with test data.""" + events_queue = asyncio.Queue() + split_storage = InMemorySplitStorageAsync(events_queue) + segment_storage = InMemorySegmentStorageAsync(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(events_queue) + + split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') + with open(split_fn, 'r') as flo: + data = json.loads(flo.read()) + for split in data['ff']['d']: + await split_storage.update([splits.from_raw(split)], [], 0) + + for rbs in data['rbs']['d']: + await rb_segment_storage.update([rule_based_segments.from_raw(rbs)], [], 0) + + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') + with open(segment_fn, 'r') as flo: + data = json.loads(flo.read()) + await segment_storage.put(segments.from_raw(data)) + + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentHumanBeignsChanges.json') + with open(segment_fn, 'r') as flo: + data = json.loads(flo.read()) + await segment_storage.put(segments.from_raw(data)) + + telemetry_storage = await InMemoryTelemetryStorageAsync.create() + telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + + storages = { + 'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': InMemoryImpressionStorageAsync(5000, telemetry_runtime_producer), + 'events': InMemoryEventStorageAsync(5000, telemetry_runtime_producer), + } + impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener + recorder = StandardRecorderAsync(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTaskAsync(events_manager.notify_internal_event, events_queue) + + # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. + try: + factory = SplitFactoryAsync('some_api_key', + storages, + True, + recorder, + events_queue, + events_manager, + None, + telemetry_producer=telemetry_producer, + telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) + ) # pylint:disable=attribute-defined-outside-init + internal_events_task.start() + except: + pass + + client = factory.client() + await client.on(SdkEvent.SDK_READY, self._ready_callback) + await factory.block_until_ready(5) + assert self.ready_flag + + """Shut down the factory.""" + await internal_events_task.stop() + await factory.destroy() + + @pytest.mark.asyncio + async def test_sdk_ready_fire_later(self): + """Prepare storages with test data.""" + events_queue = asyncio.Queue() + split_storage = InMemorySplitStorageAsync(events_queue) + segment_storage = InMemorySegmentStorageAsync(events_queue) + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync(events_queue) + + split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json') + with open(split_fn, 'r') as flo: + data = json.loads(flo.read()) + for split in data['ff']['d']: + await split_storage.update([splits.from_raw(split)], [], 0) + + for rbs in data['rbs']['d']: + await rb_segment_storage.update([rule_based_segments.from_raw(rbs)], [], 0) + + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json') + with open(segment_fn, 'r') as flo: + data = json.loads(flo.read()) + await segment_storage.put(segments.from_raw(data)) + + segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentHumanBeignsChanges.json') + with open(segment_fn, 'r') as flo: + data = json.loads(flo.read()) + await segment_storage.put(segments.from_raw(data)) + + telemetry_storage = await InMemoryTelemetryStorageAsync.create() + telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + + storages = { + 'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': InMemoryImpressionStorageAsync(5000, telemetry_runtime_producer), + 'events': InMemoryEventStorageAsync(5000, telemetry_runtime_producer), + } + impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener + recorder = StandardRecorderAsync(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) + events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTaskAsync(events_manager.notify_internal_event, events_queue) + + # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. + try: + factory = SplitFactoryAsync('some_api_key', + storages, + True, + recorder, + events_queue, + events_manager, + None, + telemetry_producer=telemetry_producer, + telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) + ) # pylint:disable=attribute-defined-outside-init + internal_events_task.start() + except: + pass + + client = factory.client() + await factory.block_until_ready(5) + await client.on(SdkEvent.SDK_READY, self._ready_callback) + + """Shut down the factory.""" + await internal_events_task.stop() + await factory.destroy() + + async def _ready_callback(self, metadata): + self.ready_flag = True + + async def _timeout_callback(self, metadata): + self.timeout_flag = True + class InMemoryIntegrationAsyncTests(object): """Inmemory storage-based integration tests.""" diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 354da30e..0f830239 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -708,7 +708,36 @@ async def test_flag_sets_withut_config_sets(self): await storage.update([split3], [], 1) assert await storage.get_feature_flags_by_sets(['set05']) == ['split3'] assert await storage.get_feature_flags_by_sets(['set04', 'set05']) == ['split3'] + + @pytest.mark.asyncio + async def test_internal_event_notification(self, mocker): + """Test retrieving a list of all split names.""" + split1 = mocker.Mock() + 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 = 'split2' + type(split2).name = name2_prop + sets_property = mocker.PropertyMock() + sets_property.return_value = ['set_1'] + type(split1).sets = sets_property + type(split2).sets = sets_property + events_queue = asyncio.Queue() + storage = InMemorySplitStorageAsync(events_queue) + await storage.update([split1, split2], [], -1) + event = await events_queue.get() + assert event.internal_event == SdkInternalEvent.FLAGS_UPDATED + assert event.metadata.get_type() == SdkEventType.FLAG_UPDATE + assert event.metadata.get_names() == {'split1', 'split2'} + await storage.kill_locally('split1', 'default_treatment', 3) + event = await events_queue.get() + assert event.internal_event == SdkInternalEvent.FLAG_KILLED_NOTIFICATION + assert event.metadata.get_type() == SdkEventType.FLAG_UPDATE + assert event.metadata.get_names() == {'split1'} + class InMemorySegmentStorageTests(object): """In memory segment storage tests.""" @@ -855,6 +884,23 @@ async def test_segment_update(self): assert not await storage.segment_contains('some_segment', 'key3') assert await storage.get_change_number('some_segment') == 456 + @pytest.mark.asyncio + async def test_internal_event_notification(self): + """Test updating a segment.""" + events_queue = asyncio.Queue() + storage = InMemorySegmentStorageAsync(events_queue) + segment = Segment('some_segment', ['key1', 'key2', 'key3'], 123) + await storage.put(segment) + event = await events_queue.get() + assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED + assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert len(event.metadata.get_names()) == 0 + + await storage.update('some_segment', ['key4', 'key5'], ['key2', 'key3'], 456) + event = await events_queue.get() + assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED + assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert len(event.metadata.get_names()) == 0 class InMemoryImpressionsStorageTests(object): """InMemory impressions storage test cases.""" @@ -2027,3 +2073,32 @@ async def test_contains(self): assert await storage.contains(["segment1"]) assert await storage.contains(["segment1", "segment3"]) assert not await storage.contains(["segment5"]) + + @pytest.mark.asyncio + async def test_internal_event_notification(self, mocker): + """Test storing and retrieving splits works.""" + events_queue = asyncio.Queue() + rbs_storage = InMemoryRuleBasedSegmentStorageAsync(events_queue) + + segment1 = mocker.Mock(spec=RuleBasedSegment) + name_property = mocker.PropertyMock() + name_property.return_value = 'some_segment' + type(segment1).name = name_property + + segment2 = mocker.Mock() + name2_prop = mocker.PropertyMock() + name2_prop.return_value = 'segment2' + type(segment2).name = name2_prop + + await rbs_storage.update([segment1, segment2], [], -1) + event = await events_queue.get() + assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED + assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert len(event.metadata.get_names()) == 0 + + await rbs_storage.update([], ['some_segment'], -1) + event = await events_queue.get() + assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED + assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert len(event.metadata.get_names()) == 0 + diff --git a/tests/tasks/util/test_asynctask.py b/tests/tasks/util/test_asynctask.py index 690182ed..b587b9c5 100644 --- a/tests/tasks/util/test_asynctask.py +++ b/tests/tasks/util/test_asynctask.py @@ -92,7 +92,7 @@ def raise_exception(): task.stop(on_stop_event) on_stop_event.wait(1) - assert on_stop_event.isSet() + assert on_stop_event.is_set() assert on_init.mock_calls == [mocker.call()] assert on_stop.mock_calls == [mocker.call()] assert 9 <= len(main_func.mock_calls) <= 10 @@ -113,7 +113,7 @@ def test_force_run(self, mocker): task.stop(on_stop_event) on_stop_event.wait(1) - assert on_stop_event.isSet() + assert on_stop_event.is_set() assert on_init.mock_calls == [mocker.call()] assert on_stop.mock_calls == [mocker.call()] assert len(main_func.mock_calls) == 2 From 4327a301e2fc46cddf9b7440394405c801b3e10e Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 21 Jan 2026 19:32:33 -0800 Subject: [PATCH 17/26] updated localhost classes and tests --- splitio/client/factory.py | 22 ++++++++++++++-------- splitio/sync/synchronizer.py | 12 ++++++++---- tests/integration/test_streaming_e2e.py | 15 +++++++++++++++ tests/sync/test_synchronizer.py | 1 - 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 670cf6c3..6157d0bd 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -1145,11 +1145,11 @@ def _build_localhost_factory(cfg): telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() - events_queue = queue.Queue() + internal_events_queue = queue.Queue() storages = { - 'splits': InMemorySplitStorage(events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), - 'segments': InMemorySegmentStorage(events_queue), # not used, just to avoid possible future errors. - 'rule_based_segments': InMemoryRuleBasedSegmentStorage(events_queue), + 'splits': InMemorySplitStorage(internal_events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), + 'segments': InMemorySegmentStorage(internal_events_queue), # not used, just to avoid possible future errors. + 'rule_based_segments': InMemoryRuleBasedSegmentStorage(internal_events_queue), 'impressions': LocalhostImpressionsStorage(), 'events': LocalhostEventsStorage(), } @@ -1162,6 +1162,8 @@ def _build_localhost_factory(cfg): LocalSegmentSynchronizer(cfg['segmentDirectory'], storages['splits'], storages['segments']), None, None, None, ) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, internal_events_queue) feature_flag_sync_task = None segment_sync_task = None @@ -1178,6 +1180,7 @@ def _build_localhost_factory(cfg): feature_flag_sync_task, segment_sync_task, None, None, None, + internal_events_task=internal_events_task ) sdk_metadata = util.get_metadata(cfg) @@ -1199,8 +1202,7 @@ def _build_localhost_factory(cfg): telemetry_evaluation_producer, telemetry_runtime_producer ) - internal_events_queue = queue.Queue() - events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task.start() return SplitFactory( 'localhost', @@ -1226,6 +1228,8 @@ async def _build_localhost_factory_async(cfg): internal_events_queue = asyncio.Queue() events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTaskAsync(events_manager.notify_internal_event, internal_events_queue) + storages = { 'splits': InMemorySplitStorageAsync(internal_events_queue), 'segments': InMemorySegmentStorageAsync(internal_events_queue), # not used, just to avoid possible future errors. @@ -1258,6 +1262,7 @@ async def _build_localhost_factory_async(cfg): feature_flag_sync_task, segment_sync_task, None, None, None, + internal_events_task=internal_events_task ) sdk_metadata = util.get_metadata(cfg) @@ -1277,8 +1282,9 @@ async def _build_localhost_factory_async(cfg): storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer - ) - + ) + internal_events_task.start() + return SplitFactoryAsync( 'localhost', storages, diff --git a/splitio/sync/synchronizer.py b/splitio/sync/synchronizer.py index 6bbb7fa6..a6ca6214 100644 --- a/splitio/sync/synchronizer.py +++ b/splitio/sync/synchronizer.py @@ -955,12 +955,13 @@ def sync_all(self, till=None): def stop_periodic_fetching(self): """Stop fetchers for feature flags and segments.""" + _LOGGER.debug('Stopping periodic fetching') if self._split_tasks.split_task is not None: - _LOGGER.debug('Stopping periodic fetching') self._split_tasks.split_task.stop() if self._split_tasks.segment_task is not None: self._split_tasks.segment_task.stop() if self._split_tasks.internal_events_task: + _LOGGER.debug('Stopping internal events notification') self._split_tasks.internal_events_task.stop() def synchronize_splits(self): @@ -1031,12 +1032,15 @@ async def sync_all(self, till=None): async def stop_periodic_fetching(self): """Stop fetchers for feature flags and segments.""" + _LOGGER.debug('Stopping periodic fetching') if self._split_tasks.split_task is not None: - _LOGGER.debug('Stopping periodic fetching') await self._split_tasks.split_task.stop() if self._split_tasks.segment_task is not None: - await self._split_tasks.segment_task.stop() - + await self._split_tasks.segment_task.stop() + if self._split_tasks.internal_events_task is not None: + _LOGGER.debug('Stopping internal events notification') + await self._split_tasks.internal_events_task.stop() + async def synchronize_splits(self): """Synchronize all feature flags.""" try: diff --git a/tests/integration/test_streaming_e2e.py b/tests/integration/test_streaming_e2e.py index a673c65c..48dc2093 100644 --- a/tests/integration/test_streaming_e2e.py +++ b/tests/integration/test_streaming_e2e.py @@ -1367,6 +1367,9 @@ def test_change_number(mocker): class StreamingIntegrationAsyncTests(object): """Test streaming operation and failover.""" + update_flag = False + metadata = [] + @pytest.mark.asyncio async def test_happiness(self): """Test initialization & splits/segment updates.""" @@ -1421,6 +1424,7 @@ async def test_happiness(self): factory = await get_factory_async('some_apikey', **kwargs) await factory.block_until_ready(1) + await factory.client().on(SdkEvent.SDK_UPDATE, self._update_callcack) assert factory.ready assert await factory.client().get_treatment('maldo', 'split1') == 'on' @@ -1437,6 +1441,13 @@ async def test_happiness(self): 'rbs': {'t': -1, 's': -1, 'd': []}} sse_server.publish(make_split_change_event(2)) await asyncio.sleep(1) + flag = False + for meta in self.metadata: + if 'split1' in meta.get_names(): + assert meta.get_type() == SdkEventType.FLAG_UPDATE + flag = True + assert flag + assert await factory.client().get_treatment('maldo', 'split1') == 'off' split_changes[2] = {'ff': { @@ -1556,6 +1567,10 @@ async def test_happiness(self): sse_server.stop() split_backend.stop() + async def _update_callcack(self, metadata): + self.update_flag = True + self.metadata.append(metadata) + @pytest.mark.asyncio async def test_occupancy_flicker(self): """Test that changes in occupancy switch between polling & streaming properly.""" diff --git a/tests/sync/test_synchronizer.py b/tests/sync/test_synchronizer.py index 179d7978..1244429b 100644 --- a/tests/sync/test_synchronizer.py +++ b/tests/sync/test_synchronizer.py @@ -210,7 +210,6 @@ def intersect(sets): mocker.Mock(), mocker.Mock()) synchronizer = Synchronizer(split_synchronizers, mocker.Mock(spec=SplitTasks)) -# pytest.set_trace() self.clear = False def clear(): self.clear = True From e7f721aa83531ab88da690e37b59e2731f8ba182 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 22 Jan 2026 09:57:45 -0800 Subject: [PATCH 18/26] fixed typo for segment event type --- splitio/client/factory.py | 22 ++++++++++++++-------- splitio/events/events_metadata.py | 2 +- splitio/storage/inmemmory.py | 12 ++++++------ tests/integration/test_streaming_e2e.py | 2 +- tests/storage/test_inmemory_storage.py | 16 ++++++++-------- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 670cf6c3..272e6f3f 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -1145,11 +1145,11 @@ def _build_localhost_factory(cfg): telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() - events_queue = queue.Queue() + internal_events_queue = queue.Queue() storages = { - 'splits': InMemorySplitStorage(events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), - 'segments': InMemorySegmentStorage(events_queue), # not used, just to avoid possible future errors. - 'rule_based_segments': InMemoryRuleBasedSegmentStorage(events_queue), + 'splits': InMemorySplitStorage(internal_events_queue, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []), + 'segments': InMemorySegmentStorage(internal_events_queue), # not used, just to avoid possible future errors. + 'rule_based_segments': InMemoryRuleBasedSegmentStorage(internal_events_queue), 'impressions': LocalhostImpressionsStorage(), 'events': LocalhostEventsStorage(), } @@ -1162,6 +1162,8 @@ def _build_localhost_factory(cfg): LocalSegmentSynchronizer(cfg['segmentDirectory'], storages['splits'], storages['segments']), None, None, None, ) + events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTask(events_manager.notify_internal_event, internal_events_queue) feature_flag_sync_task = None segment_sync_task = None @@ -1178,6 +1180,7 @@ def _build_localhost_factory(cfg): feature_flag_sync_task, segment_sync_task, None, None, None, + internal_events_task ) sdk_metadata = util.get_metadata(cfg) @@ -1199,8 +1202,7 @@ def _build_localhost_factory(cfg): telemetry_evaluation_producer, telemetry_runtime_producer ) - internal_events_queue = queue.Queue() - events_manager = EventsManager(EventsManagerConfig(), EventsDelivery()) + internal_events_task.start() return SplitFactory( 'localhost', @@ -1226,6 +1228,8 @@ async def _build_localhost_factory_async(cfg): internal_events_queue = asyncio.Queue() events_manager = EventsManagerAsync(EventsManagerConfig(), EventsDelivery()) + internal_events_task = EventsTaskAsync(events_manager.notify_internal_event, internal_events_queue) + storages = { 'splits': InMemorySplitStorageAsync(internal_events_queue), 'segments': InMemorySegmentStorageAsync(internal_events_queue), # not used, just to avoid possible future errors. @@ -1258,6 +1262,7 @@ async def _build_localhost_factory_async(cfg): feature_flag_sync_task, segment_sync_task, None, None, None, + internal_events_task ) sdk_metadata = util.get_metadata(cfg) @@ -1277,8 +1282,9 @@ async def _build_localhost_factory_async(cfg): storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer - ) - + ) + internal_events_task.start() + return SplitFactoryAsync( 'localhost', storages, diff --git a/splitio/events/events_metadata.py b/splitio/events/events_metadata.py index 5d6f4961..0707a8f5 100644 --- a/splitio/events/events_metadata.py +++ b/splitio/events/events_metadata.py @@ -5,7 +5,7 @@ class SdkEventType(Enum): """Public event types""" FLAG_UPDATE = 'FLAG_UPDATE' - SEGMENT_UPDATE = 'SEGMENT_UPDATE' + SEGMENTS_UPDATE = 'SEGMENTS_UPDATE' class EventsMetadata(object): """Events Metadata class.""" diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index bbde8816..db71f7fd 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -158,7 +158,7 @@ def update(self, to_add, to_delete, new_change_number): self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.RB_SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) def _put(self, rule_based_segment): """ @@ -290,7 +290,7 @@ async def update(self, to_add, to_delete, new_change_number): await self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.RB_SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) async def _put(self, rule_based_segment): """ @@ -999,7 +999,7 @@ def put(self, segment): self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) def update(self, segment_name, to_add, to_remove, change_number=None): """ @@ -1025,7 +1025,7 @@ def update(self, segment_name, to_add, to_remove, change_number=None): self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) def get_change_number(self, segment_name): """ @@ -1140,7 +1140,7 @@ async def put(self, segment): await self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) async def update(self, segment_name, to_add, to_remove, change_number=None): @@ -1166,7 +1166,7 @@ async def update(self, segment_name, to_add, to_remove, change_number=None): await self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) async def get_change_number(self, segment_name): diff --git a/tests/integration/test_streaming_e2e.py b/tests/integration/test_streaming_e2e.py index a673c65c..bb2dc91a 100644 --- a/tests/integration/test_streaming_e2e.py +++ b/tests/integration/test_streaming_e2e.py @@ -128,7 +128,7 @@ def test_happiness(self): sse_server.publish(make_segment_change_event('segment1', 1)) time.sleep(1) assert self.update_flag - assert self.metadata[len(self.metadata)-1].get_type() == SdkEventType.SEGMENT_UPDATE + assert self.metadata[len(self.metadata)-1].get_type() == SdkEventType.SEGMENTS_UPDATE flag = False for meta in self.metadata: if 'split2' in meta.get_names(): diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 0f830239..d46980aa 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -811,13 +811,13 @@ def test_internal_event_notification(self): storage.put(segment) event = events_queue.get() assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 storage.update('some_segment', ['key4', 'key5'], ['key2', 'key3'], 456) event = events_queue.get() assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 class InMemorySegmentStorageAsyncTests(object): @@ -893,13 +893,13 @@ async def test_internal_event_notification(self): await storage.put(segment) event = await events_queue.get() assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 await storage.update('some_segment', ['key4', 'key5'], ['key2', 'key3'], 456) event = await events_queue.get() assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 class InMemoryImpressionsStorageTests(object): @@ -2006,12 +2006,12 @@ def test_internal_event_notification(self, mocker): rbs_storage.update([segment1, segment2], [], -1) event = events_queue.get() assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 rbs_storage.update([], ['some_segment'], -1) assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 class InMemoryRuleBasedSegmentStorageAsyncTests(object): @@ -2093,12 +2093,12 @@ async def test_internal_event_notification(self, mocker): await rbs_storage.update([segment1, segment2], [], -1) event = await events_queue.get() assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 await rbs_storage.update([], ['some_segment'], -1) event = await events_queue.get() assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 From 11d56fddf3333599a2a00bd8655f3c47943c7d9c Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 22 Jan 2026 10:10:19 -0800 Subject: [PATCH 19/26] fixed typo for segment update type --- splitio/events/events_metadata.py | 2 +- splitio/storage/inmemmory.py | 12 ++++++------ tests/integration/test_streaming_e2e.py | 2 +- tests/storage/test_inmemory_storage.py | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/splitio/events/events_metadata.py b/splitio/events/events_metadata.py index 5d6f4961..0707a8f5 100644 --- a/splitio/events/events_metadata.py +++ b/splitio/events/events_metadata.py @@ -5,7 +5,7 @@ class SdkEventType(Enum): """Public event types""" FLAG_UPDATE = 'FLAG_UPDATE' - SEGMENT_UPDATE = 'SEGMENT_UPDATE' + SEGMENTS_UPDATE = 'SEGMENTS_UPDATE' class EventsMetadata(object): """Events Metadata class.""" diff --git a/splitio/storage/inmemmory.py b/splitio/storage/inmemmory.py index bbde8816..db71f7fd 100644 --- a/splitio/storage/inmemmory.py +++ b/splitio/storage/inmemmory.py @@ -158,7 +158,7 @@ def update(self, to_add, to_delete, new_change_number): self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.RB_SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) def _put(self, rule_based_segment): """ @@ -290,7 +290,7 @@ async def update(self, to_add, to_delete, new_change_number): await self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.RB_SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) async def _put(self, rule_based_segment): """ @@ -999,7 +999,7 @@ def put(self, segment): self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) def update(self, segment_name, to_add, to_remove, change_number=None): """ @@ -1025,7 +1025,7 @@ def update(self, segment_name, to_add, to_remove, change_number=None): self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) def get_change_number(self, segment_name): """ @@ -1140,7 +1140,7 @@ async def put(self, segment): await self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) async def update(self, segment_name, to_add, to_remove, change_number=None): @@ -1166,7 +1166,7 @@ async def update(self, segment_name, to_add, to_remove, change_number=None): await self._internal_event_queue.put( SdkInternalEventNotification( SdkInternalEvent.SEGMENTS_UPDATED, - EventsMetadata(SdkEventType.SEGMENT_UPDATE, {}))) + EventsMetadata(SdkEventType.SEGMENTS_UPDATE, {}))) async def get_change_number(self, segment_name): diff --git a/tests/integration/test_streaming_e2e.py b/tests/integration/test_streaming_e2e.py index 48dc2093..d7b3103a 100644 --- a/tests/integration/test_streaming_e2e.py +++ b/tests/integration/test_streaming_e2e.py @@ -128,7 +128,7 @@ def test_happiness(self): sse_server.publish(make_segment_change_event('segment1', 1)) time.sleep(1) assert self.update_flag - assert self.metadata[len(self.metadata)-1].get_type() == SdkEventType.SEGMENT_UPDATE + assert self.metadata[len(self.metadata)-1].get_type() == SdkEventType.SEGMENTS_UPDATE flag = False for meta in self.metadata: if 'split2' in meta.get_names(): diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 0f830239..d46980aa 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -811,13 +811,13 @@ def test_internal_event_notification(self): storage.put(segment) event = events_queue.get() assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 storage.update('some_segment', ['key4', 'key5'], ['key2', 'key3'], 456) event = events_queue.get() assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 class InMemorySegmentStorageAsyncTests(object): @@ -893,13 +893,13 @@ async def test_internal_event_notification(self): await storage.put(segment) event = await events_queue.get() assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 await storage.update('some_segment', ['key4', 'key5'], ['key2', 'key3'], 456) event = await events_queue.get() assert event.internal_event == SdkInternalEvent.SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 class InMemoryImpressionsStorageTests(object): @@ -2006,12 +2006,12 @@ def test_internal_event_notification(self, mocker): rbs_storage.update([segment1, segment2], [], -1) event = events_queue.get() assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 rbs_storage.update([], ['some_segment'], -1) assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 class InMemoryRuleBasedSegmentStorageAsyncTests(object): @@ -2093,12 +2093,12 @@ async def test_internal_event_notification(self, mocker): await rbs_storage.update([segment1, segment2], [], -1) event = await events_queue.get() assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 await rbs_storage.update([], ['some_segment'], -1) event = await events_queue.get() assert event.internal_event == SdkInternalEvent.RB_SEGMENTS_UPDATED - assert event.metadata.get_type() == SdkEventType.SEGMENT_UPDATE + assert event.metadata.get_type() == SdkEventType.SEGMENTS_UPDATE assert len(event.metadata.get_names()) == 0 From 8f29ba9960306c7b5d272e63e3985efdd2a7d75b Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 22 Jan 2026 11:43:25 -0800 Subject: [PATCH 20/26] ignored fetching rbs if list is empty --- splitio/storage/redis.py | 6 ++++++ tests/storage/test_redis.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index ad1badf0..b8fe27ad 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -142,6 +142,9 @@ def fetch_many(self, segment_names): :rtype: dict(segment_name, splitio.models.rule_based_segment.RuleBasedSegment) """ to_return = dict() + if len(segment_names) == 0: + return to_return + try: keys = [self._get_key(segment_name) for segment_name in segment_names] raw_rbs_segments = self._redis.mget(keys) @@ -286,6 +289,9 @@ async def fetch_many(self, segment_names): :rtype: dict(segment_name, splitio.models.rule_based_segment.RuleBasedSegment) """ to_return = dict() + if len(segment_names) == 0: + return to_return + try: keys = [self._get_key(segment_name) for segment_name in segment_names] raw_rbs_segments = await self._redis.mget(keys) diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index de5ebfd5..a45c4ad2 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -1315,6 +1315,10 @@ def test_fetch_many(self, mocker): assert result['rbs2'] is not None assert 'rbs3' in result + # should not raise exception + result = storage.fetch_many([]) + assert len(result) == 0 + class RedisRuleBasedSegmentStorageAsyncTests(object): """Redis rule based segment storage test cases.""" @@ -1438,3 +1442,7 @@ async def mget(*_): assert result['rbs2'] is not None assert 'rbs3' in result + # should not raise exception + result = await storage.fetch_many([]) + assert len(result) == 0 + From 09dd7e55d76a17afa4275913a400e8d913452035 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 22 Jan 2026 13:50:09 -0800 Subject: [PATCH 21/26] removed name param from creat_task, supported only after 3.8 --- splitio/events/events_task.py | 2 +- tests/client/test_factory.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/splitio/events/events_task.py b/splitio/events/events_task.py index 3c7e34f3..8158dc04 100644 --- a/splitio/events/events_task.py +++ b/splitio/events/events_task.py @@ -133,7 +133,7 @@ def start(self): self._running = True _LOGGER.debug('Starting SDK Event Task worker') - asyncio.get_running_loop().create_task(self._run(), name="EventsTaskWorker") + asyncio.get_running_loop().create_task(self._run()) async def stop(self, stop_flag=None): """Stop worker.""" diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 64da9541..7ddeda45 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -1151,7 +1151,7 @@ async def record_active_and_redundant_factories(*_): # Start factory and make assertions factory = await get_factory_async('some_api_key', config={'streamingEmabled': False}) for task in asyncio.all_tasks(): - if task.get_name() == "EventsTaskWorker": + if task.get_coro().__qualname__ == "EventsTaskAsync._run": task.cancel() try: await factory.block_until_ready(3) @@ -1204,7 +1204,7 @@ async def record_active_and_redundant_factories(*_): # Start factory and make assertions factory = await get_factory_async('some_api_key', config={'streamingEmabled': False}) for task in asyncio.all_tasks(): - if task.get_name() == "EventsTaskWorker": + if task.get_coro().__qualname__ == "EventsTaskAsync._run": task.cancel() try: await factory.block_until_ready(1) From 15ca5073e7456b05e4fb8ef378c58f20c4056c8b Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 27 Jan 2026 10:07:46 -0800 Subject: [PATCH 22/26] updated changes --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index e080bbd6..1191ae62 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +10.6.0 (Jan 28, 2026) +- Fixed non-blocking error when fetching feature flags from redis. +- Added functionality to subscribe to events when SDK update its storage, when its ready and when block until ready call time-out. Read more in our docs. + 10.5.1 (Oct 15, 2025) - Added using String only parameter for treatments in FallbackTreatmentConfiguration class. From 27e2592c844870eb2f118cb72a03e1ecd0fb7d88 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 27 Jan 2026 10:58:36 -0800 Subject: [PATCH 23/26] fixed test --- tests/client/test_factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 7ddeda45..4b584378 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -1151,7 +1151,7 @@ async def record_active_and_redundant_factories(*_): # Start factory and make assertions factory = await get_factory_async('some_api_key', config={'streamingEmabled': False}) for task in asyncio.all_tasks(): - if task.get_coro().__qualname__ == "EventsTaskAsync._run": + if task._coro.__qualname__ == "EventsTaskAsync._run": task.cancel() try: await factory.block_until_ready(3) @@ -1204,7 +1204,7 @@ async def record_active_and_redundant_factories(*_): # Start factory and make assertions factory = await get_factory_async('some_api_key', config={'streamingEmabled': False}) for task in asyncio.all_tasks(): - if task.get_coro().__qualname__ == "EventsTaskAsync._run": + if task._coro.__qualname__ == "EventsTaskAsync._run": task.cancel() try: await factory.block_until_ready(1) From 2244773a34eb5c9efbc02b6e3633de479c7d90d2 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 27 Jan 2026 11:20:37 -0800 Subject: [PATCH 24/26] polishing --- splitio/events/events_manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/splitio/events/events_manager.py b/splitio/events/events_manager.py index b51a992c..63def795 100644 --- a/splitio/events/events_manager.py +++ b/splitio/events/events_manager.py @@ -25,15 +25,19 @@ def __init__(self, events_configurations, events_delivery): self._manager_config = events_configurations def register(self, sdk_event, event_handler): + # Implement in child class pass def unregister(self, sdk_event): + # Implement in child class pass def notify_internal_event(self, sdk_internal_event, event_metadata): + # Implement in child class pass def destroy(self): + # Implement in child class pass def _event_already_triggered(self, sdk_event): @@ -241,7 +245,7 @@ async def destroy(self): self._active_subscriptions = {} self._internal_events_status = {} - async def _fire_sdk_event(self, sdk_event, event_metadata): + def _fire_sdk_event(self, sdk_event, event_metadata): _LOGGER.debug("EventsManager: Firing Sdk event %s", sdk_event) asyncio.get_running_loop().create_task(self._events_delivery.deliver_async(sdk_event, event_metadata, self._get_event_handler(sdk_event))) self._set_sdk_event_triggered(sdk_event) \ No newline at end of file From 3a9d2a78234e4d57d487d4521522c70b522b5244 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 27 Jan 2026 12:03:24 -0800 Subject: [PATCH 25/26] fixed calling fire event function --- splitio/events/events_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splitio/events/events_manager.py b/splitio/events/events_manager.py index 63def795..de8206f1 100644 --- a/splitio/events/events_manager.py +++ b/splitio/events/events_manager.py @@ -215,7 +215,7 @@ async def register(self, sdk_event, event_handler): if sdk_event == SdkEvent.SDK_READY and self._event_already_triggered(sdk_event): self._active_subscriptions[sdk_event] = ActiveSubscriptions(True, event_handler) _LOGGER.debug("EventsManager: Firing SDK_READY event for new subscription") - await self._fire_sdk_event(sdk_event, None) + self._fire_sdk_event(sdk_event, None) return self._active_subscriptions[sdk_event] = ActiveSubscriptions(False, event_handler) @@ -233,7 +233,7 @@ async def notify_internal_event(self, sdk_internal_event, event_metadata): for sorted_event in self._manager_config.evaluation_order: if sorted_event in self._get_sdk_event_if_applicable(sdk_internal_event): if self._get_event_handler(sorted_event) != None: - await self._fire_sdk_event(sorted_event, event_metadata) + self._fire_sdk_event(sorted_event, event_metadata) # if client is not subscribed to SDK_READY if sorted_event == SdkEvent.SDK_READY and self._get_event_handler(sorted_event) == None: From 43dfb55d6f3bae265cd23a800090ca32581f5781 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 27 Jan 2026 14:52:56 -0800 Subject: [PATCH 26/26] updated license and added notice --- LICENSE.txt | 182 ++++++++++++++++++++++++++++++++++++++++++++++++---- NOTICE.txt | 5 ++ 2 files changed, 174 insertions(+), 13 deletions(-) create mode 100644 NOTICE.txt diff --git a/LICENSE.txt b/LICENSE.txt index df08de3f..0f9e8a59 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,13 +1,169 @@ -Copyright © 2025 Split Software, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + END OF TERMS AND CONDITIONS + APPENDIX: How to apply the Apache License to your work. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + Copyright [yyyy] [name of copyright owner] + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..7d7d845e --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,5 @@ +Harness Feature Management JavaScript SDK Copyright 2024-2026 Harness Inc. + +This product includes software developed at Harness Inc. (https://harness.io/). + +This product includes software originally developed by Split Software, Inc. (https://www.split.io/). Copyright 2015-2024 Split Software, Inc.