Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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. Read more in our docs.
- 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