Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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.
- Added the ability to listen to different events triggered by the SDK
- SDK_UPDATE notify when a flag or user segment has changed
- SDK_READY notify when the SDK is ready to evaluate

10.5.1 (Oct 15, 2025)
- Added using String only parameter for treatments in FallbackTreatmentConfiguration class.
Expand Down
2 changes: 0 additions & 2 deletions splitio/client/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ 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):
Expand Down Expand Up @@ -439,7 +438,6 @@ 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):
Expand Down
7 changes: 2 additions & 5 deletions splitio/events/events_manager_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,25 @@ 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}
SdkInternalEvent.RB_SEGMENTS_UPDATED, SdkInternalEvent.SEGMENTS_UPDATED}
}

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]:
for sdk_event in [SdkEvent.SDK_READY, SdkEvent.SDK_UPDATE]:
sorted_events = self._dfs_recursive(sdk_event, sorted_events)

return sorted_events
Expand Down
2 changes: 0 additions & 2 deletions splitio/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,12 @@ 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'
Expand Down
104 changes: 1 addition & 103 deletions tests/client/test_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,56 +772,6 @@ def synchronize_config(*_):
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(),
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."""
# Invalid API Key with preforked should exit after 3 attempts.
Expand Down Expand Up @@ -1161,56 +1111,4 @@ async def record_active_and_redundant_factories(*_):
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._coro.__qualname__ == "EventsTaskAsync._run":
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()
await factory.destroy()
51 changes: 0 additions & 51 deletions tests/events/test_events_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class EventsManagerTests(object):
"""Tests for EventsManager."""

sdk_ready_flag = False
sdk_timed_out_flag = False
sdk_update_flag = False
metadata = None

Expand All @@ -28,60 +27,40 @@ def test_firing_events(self):
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

Expand All @@ -93,10 +72,6 @@ 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()
Expand All @@ -105,7 +80,6 @@ class EventsManagerAsyncTests(object):
"""Tests for EventsManagerAsync."""

sdk_ready_flag = False
sdk_timed_out_flag = False
sdk_update_flag = False
metadata = None

Expand All @@ -121,66 +95,45 @@ async def test_firing_events(self):
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

Expand All @@ -192,10 +145,6 @@ 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()
13 changes: 2 additions & 11 deletions tests/events/test_events_manager_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,20 @@ def test_build_instance(self):

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
assert len(config.evaluation_order) == 2
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:
if order == 2:
assert sdk_event == SdkEvent.SDK_UPDATE
Loading