Skip to content

Commit c6c97c0

Browse files
committed
Factory and test integration for pluggable optimized mode
1 parent 01362fa commit c6c97c0

File tree

5 files changed

+292
-11
lines changed

5 files changed

+292
-11
lines changed

splitio/client/config.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,6 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None):
111111
'one of the following values: `debug`, `none` or `optimized`. '
112112
' Defaulting to `optimized` mode.')
113113

114-
if storage_type == 'pluggable' and mode != ImpressionsMode.DEBUG:
115-
mode = ImpressionsMode.DEBUG
116-
_LOGGER.warning('`pluggable` storageMode only support `debug` impressionMode, adjusting impressionsMode to `debug`. ')
117-
118114
if mode == ImpressionsMode.DEBUG:
119115
refresh_rate = max(1, refresh_rate) if refresh_rate is not None else 60
120116
else:

splitio/client/factory.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,15 +537,30 @@ def _build_pluggable_factory(api_key, cfg):
537537

538538
unique_keys_synchronizer, clear_filter_sync, unique_keys_task, \
539539
clear_filter_task, impressions_count_sync, impressions_count_task, \
540-
imp_strategy = set_classes('PLUGGABLE', cfg['impressionsMode'], pluggable_adapter)
540+
imp_strategy = set_classes('PLUGGABLE', cfg['impressionsMode'], pluggable_adapter, storage_prefix)
541541

542542
imp_manager = ImpressionsManager(
543543
imp_strategy,
544544
telemetry_runtime_producer,
545545
_wrap_impression_listener(cfg['impressionListener'], sdk_metadata),
546546
)
547547

548-
synchronizer = PluggableSynchronizer()
548+
synchronizers = SplitSynchronizers(None, None, None, None,
549+
impressions_count_sync,
550+
None,
551+
unique_keys_synchronizer,
552+
clear_filter_sync
553+
)
554+
555+
tasks = SplitTasks(None, None, None, None,
556+
impressions_count_task,
557+
None,
558+
unique_keys_task,
559+
clear_filter_task
560+
)
561+
562+
# Using same class as redis for consumer mode only
563+
synchronizer = RedisSynchronizer(synchronizers, tasks)
549564
recorder = StandardRecorder(
550565
imp_manager,
551566
storages['events'],

splitio/engine/impressions/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from splitio.engine.impressions.impressions import ImpressionsMode
22
from splitio.engine.impressions.manager import Counter as ImpressionsCounter
33
from splitio.engine.impressions.strategies import StrategyNoneMode, StrategyDebugMode, StrategyOptimizedMode
4-
from splitio.engine.impressions.adapters import InMemorySenderAdapter, RedisSenderAdapter
4+
from splitio.engine.impressions.adapters import InMemorySenderAdapter, RedisSenderAdapter, PluggableSenderAdapter
55
from splitio.tasks.unique_keys_sync import UniqueKeysSyncTask, ClearFilterSyncTask
66
from splitio.sync.unique_keys import UniqueKeysSynchronizer, ClearFilterSynchronizer
77
from splitio.sync.impression import ImpressionsCountSynchronizer
88
from splitio.tasks.impressions_sync import ImpressionsCountSyncTask
99

10-
def set_classes(storage_mode, impressions_mode, api_adapter):
10+
def set_classes(storage_mode, impressions_mode, api_adapter, prefix=None):
1111
unique_keys_synchronizer = None
1212
clear_filter_sync = None
1313
unique_keys_task = None
@@ -16,6 +16,7 @@ def set_classes(storage_mode, impressions_mode, api_adapter):
1616
impressions_count_task = None
1717
sender_adapter = None
1818
if storage_mode == 'PLUGGABLE':
19+
sender_adapter = PluggableSenderAdapter(api_adapter, prefix)
1920
api_telemetry_adapter = sender_adapter
2021
api_impressions_adapter = sender_adapter
2122
elif storage_mode == 'REDIS':

tests/client/test_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ def test_sanitize_imp_mode(self):
3939
assert rate == 200
4040

4141
mode, rate = config._sanitize_impressions_mode('pluggable', 'ANYTHING', 200)
42-
assert mode == ImpressionsMode.DEBUG
42+
assert mode == ImpressionsMode.OPTIMIZED
4343
assert rate == 200
4444

4545
mode, rate = config._sanitize_impressions_mode('pluggable', 'NONE', 200)
46-
assert mode == ImpressionsMode.DEBUG
46+
assert mode == ImpressionsMode.NONE
4747
assert rate == 200
4848

4949
mode, rate = config._sanitize_impressions_mode('pluggable', 'OPTIMIZED', 200)
50-
assert mode == ImpressionsMode.DEBUG
50+
assert mode == ImpressionsMode.OPTIMIZED
5151
assert rate == 200
5252

5353
mode, rate = config._sanitize_impressions_mode('memory', 43, -1)

tests/integration/test_client_e2e.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,3 +1452,272 @@ def teardown_method(self):
14521452

14531453
for key in keys_to_delete:
14541454
self.pluggable_storage_adapter.delete(key)
1455+
1456+
class PluggableOptimizedIntegrationTests(object):
1457+
"""Pluggable storage-based integration tests."""
1458+
1459+
def setup_method(self):
1460+
"""Prepare storages with test data."""
1461+
metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name')
1462+
self.pluggable_storage_adapter = StorageMockAdapter()
1463+
split_storage = PluggableSplitStorage(self.pluggable_storage_adapter, 'myprefix')
1464+
segment_storage = PluggableSegmentStorage(self.pluggable_storage_adapter, 'myprefix')
1465+
1466+
telemetry_pluggable_storage = PluggableTelemetryStorage(self.pluggable_storage_adapter, metadata, 'myprefix')
1467+
telemetry_producer = TelemetryStorageProducer(telemetry_pluggable_storage)
1468+
telemetry_consumer = TelemetryStorageConsumer(telemetry_pluggable_storage)
1469+
telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer()
1470+
1471+
storages = {
1472+
'splits': split_storage,
1473+
'segments': segment_storage,
1474+
'impressions': PluggableImpressionsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'),
1475+
'events': PluggableEventsStorage(self.pluggable_storage_adapter, metadata, 'myprefix'),
1476+
'telemetry': telemetry_pluggable_storage
1477+
}
1478+
1479+
impmanager = ImpressionsManager(StrategyOptimizedMode(ImpressionsCounter()), telemetry_runtime_producer) # no listener
1480+
recorder = StandardRecorder(impmanager, storages['events'],
1481+
storages['impressions'], storages['telemetry'])
1482+
1483+
self.factory = SplitFactory('some_api_key',
1484+
storages,
1485+
True,
1486+
recorder,
1487+
RedisManager(PluggableSynchronizer()),
1488+
sdk_ready_flag=None,
1489+
telemetry_producer=telemetry_producer,
1490+
telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(),
1491+
) # pylint:disable=attribute-defined-outside-init
1492+
1493+
# Adding data to storage
1494+
split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json')
1495+
with open(split_fn, 'r') as flo:
1496+
data = json.loads(flo.read())
1497+
for split in data['splits']:
1498+
self.pluggable_storage_adapter.set(split_storage._prefix.format(split_name=split['name']), split)
1499+
self.pluggable_storage_adapter.set(split_storage._split_till_prefix, data['till'])
1500+
1501+
segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json')
1502+
with open(segment_fn, 'r') as flo:
1503+
data = json.loads(flo.read())
1504+
self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added']))
1505+
self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till'])
1506+
1507+
segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentHumanBeignsChanges.json')
1508+
with open(segment_fn, 'r') as flo:
1509+
data = json.loads(flo.read())
1510+
self.pluggable_storage_adapter.set(segment_storage._prefix.format(segment_name=data['name']), set(data['added']))
1511+
self.pluggable_storage_adapter.set(segment_storage._segment_till_prefix.format(segment_name=data['name']), data['till'])
1512+
1513+
def _validate_last_events(self, client, *to_validate):
1514+
"""Validate the last N impressions are present disregarding the order."""
1515+
event_storage = client._factory._get_storage('events')
1516+
events_raw = []
1517+
stored_events = self.pluggable_storage_adapter.pop_items(event_storage._events_queue_key)
1518+
if stored_events is not None:
1519+
events_raw = [json.loads(im) for im in stored_events]
1520+
1521+
as_tup_set = set(
1522+
(i['e']['key'], i['e']['trafficTypeName'], i['e']['eventTypeId'], i['e']['value'], str(i['e']['properties']))
1523+
for i in events_raw
1524+
)
1525+
assert as_tup_set == set(to_validate)
1526+
1527+
def _validate_last_impressions(self, client, *to_validate):
1528+
"""Validate the last N impressions are present disregarding the order."""
1529+
imp_storage = client._factory._get_storage('impressions')
1530+
impressions_raw = []
1531+
stored_impressions = self.pluggable_storage_adapter.pop_items(imp_storage._impressions_queue_key)
1532+
if stored_impressions is not None:
1533+
impressions_raw = [json.loads(im) for im in stored_impressions]
1534+
as_tup_set = set(
1535+
(i['i']['f'], i['i']['k'], i['i']['t'])
1536+
for i in impressions_raw
1537+
)
1538+
1539+
assert as_tup_set == set(to_validate)
1540+
1541+
def test_get_treatment(self):
1542+
"""Test client.get_treatment()."""
1543+
client = self.factory.client()
1544+
1545+
assert client.get_treatment('user1', 'sample_feature') == 'on'
1546+
self._validate_last_impressions(client, ('sample_feature', 'user1', 'on'))
1547+
client.get_treatment('user1', 'sample_feature')
1548+
client.get_treatment('user1', 'sample_feature')
1549+
client.get_treatment('user1', 'sample_feature')
1550+
1551+
# Only one impression was added, and popped when validating, the rest were ignored
1552+
# pytest.set_trace()
1553+
assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == []
1554+
1555+
assert client.get_treatment('invalidKey', 'sample_feature') == 'off'
1556+
self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off'))
1557+
1558+
assert client.get_treatment('invalidKey', 'invalid_feature') == 'control'
1559+
self._validate_last_impressions(client) # No impressions should be present
1560+
1561+
# testing a killed feature. No matter what the key, must return default treatment
1562+
assert client.get_treatment('invalidKey', 'killed_feature') == 'defTreatment'
1563+
self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment'))
1564+
1565+
# testing ALL matcher
1566+
assert client.get_treatment('invalidKey', 'all_feature') == 'on'
1567+
self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on'))
1568+
1569+
# testing WHITELIST matcher
1570+
assert client.get_treatment('whitelisted_user', 'whitelist_feature') == 'on'
1571+
self._validate_last_impressions(client, ('whitelist_feature', 'whitelisted_user', 'on'))
1572+
assert client.get_treatment('unwhitelisted_user', 'whitelist_feature') == 'off'
1573+
self._validate_last_impressions(client, ('whitelist_feature', 'unwhitelisted_user', 'off'))
1574+
1575+
# testing INVALID matcher
1576+
assert client.get_treatment('some_user_key', 'invalid_matcher_feature') == 'control'
1577+
self._validate_last_impressions(client) # No impressions should be present
1578+
1579+
# testing Dependency matcher
1580+
assert client.get_treatment('somekey', 'dependency_test') == 'off'
1581+
self._validate_last_impressions(client, ('dependency_test', 'somekey', 'off'))
1582+
1583+
# testing boolean matcher
1584+
assert client.get_treatment('True', 'boolean_test') == 'on'
1585+
self._validate_last_impressions(client, ('boolean_test', 'True', 'on'))
1586+
1587+
# testing regex matcher
1588+
assert client.get_treatment('abc4', 'regex_test') == 'on'
1589+
self._validate_last_impressions(client, ('regex_test', 'abc4', 'on'))
1590+
1591+
def test_get_treatments(self):
1592+
"""Test client.get_treatments()."""
1593+
client = self.factory.client()
1594+
1595+
result = client.get_treatments('user1', ['sample_feature'])
1596+
assert len(result) == 1
1597+
assert result['sample_feature'] == 'on'
1598+
self._validate_last_impressions(client, ('sample_feature', 'user1', 'on'))
1599+
1600+
result = client.get_treatments('invalidKey', ['sample_feature'])
1601+
assert len(result) == 1
1602+
assert result['sample_feature'] == 'off'
1603+
self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off'))
1604+
1605+
result = client.get_treatments('invalidKey', ['invalid_feature'])
1606+
assert len(result) == 1
1607+
assert result['invalid_feature'] == 'control'
1608+
self._validate_last_impressions(client)
1609+
1610+
# testing a killed feature. No matter what the key, must return default treatment
1611+
result = client.get_treatments('invalidKey', ['killed_feature'])
1612+
assert len(result) == 1
1613+
assert result['killed_feature'] == 'defTreatment'
1614+
self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment'))
1615+
1616+
# testing ALL matcher
1617+
result = client.get_treatments('invalidKey', ['all_feature'])
1618+
assert len(result) == 1
1619+
assert result['all_feature'] == 'on'
1620+
self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on'))
1621+
1622+
# testing multiple splitNames
1623+
result = client.get_treatments('invalidKey', [
1624+
'all_feature',
1625+
'killed_feature',
1626+
'invalid_feature',
1627+
'sample_feature'
1628+
])
1629+
assert len(result) == 4
1630+
assert result['all_feature'] == 'on'
1631+
assert result['killed_feature'] == 'defTreatment'
1632+
assert result['invalid_feature'] == 'control'
1633+
assert result['sample_feature'] == 'off'
1634+
assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == []
1635+
1636+
def test_get_treatments_with_config(self):
1637+
"""Test client.get_treatments_with_config()."""
1638+
client = self.factory.client()
1639+
1640+
result = client.get_treatments_with_config('user1', ['sample_feature'])
1641+
assert len(result) == 1
1642+
assert result['sample_feature'] == ('on', '{"size":15,"test":20}')
1643+
self._validate_last_impressions(client, ('sample_feature', 'user1', 'on'))
1644+
1645+
result = client.get_treatments_with_config('invalidKey', ['sample_feature'])
1646+
assert len(result) == 1
1647+
assert result['sample_feature'] == ('off', None)
1648+
self._validate_last_impressions(client, ('sample_feature', 'invalidKey', 'off'))
1649+
1650+
result = client.get_treatments_with_config('invalidKey', ['invalid_feature'])
1651+
assert len(result) == 1
1652+
assert result['invalid_feature'] == ('control', None)
1653+
self._validate_last_impressions(client)
1654+
1655+
# testing a killed feature. No matter what the key, must return default treatment
1656+
result = client.get_treatments_with_config('invalidKey', ['killed_feature'])
1657+
assert len(result) == 1
1658+
assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}')
1659+
self._validate_last_impressions(client, ('killed_feature', 'invalidKey', 'defTreatment'))
1660+
1661+
# testing ALL matcher
1662+
result = client.get_treatments_with_config('invalidKey', ['all_feature'])
1663+
assert len(result) == 1
1664+
assert result['all_feature'] == ('on', None)
1665+
self._validate_last_impressions(client, ('all_feature', 'invalidKey', 'on'))
1666+
1667+
# testing multiple splitNames
1668+
result = client.get_treatments_with_config('invalidKey', [
1669+
'all_feature',
1670+
'killed_feature',
1671+
'invalid_feature',
1672+
'sample_feature'
1673+
])
1674+
assert len(result) == 4
1675+
1676+
assert result['all_feature'] == ('on', None)
1677+
assert result['killed_feature'] == ('defTreatment', '{"size":15,"defTreatment":true}')
1678+
assert result['invalid_feature'] == ('control', None)
1679+
assert result['sample_feature'] == ('off', None)
1680+
assert self.pluggable_storage_adapter._keys['myprefix.SPLITIO.impressions'] == []
1681+
1682+
def test_manager_methods(self):
1683+
"""Test manager.split/splits."""
1684+
manager = self.factory.manager()
1685+
result = manager.split('all_feature')
1686+
assert result.name == 'all_feature'
1687+
assert result.traffic_type is None
1688+
assert result.killed is False
1689+
assert len(result.treatments) == 2
1690+
assert result.change_number == 123
1691+
assert result.configs == {}
1692+
1693+
result = manager.split('killed_feature')
1694+
assert result.name == 'killed_feature'
1695+
assert result.traffic_type is None
1696+
assert result.killed is True
1697+
assert len(result.treatments) == 2
1698+
assert result.change_number == 123
1699+
assert result.configs['defTreatment'] == '{"size":15,"defTreatment":true}'
1700+
assert result.configs['off'] == '{"size":15,"test":20}'
1701+
1702+
result = manager.split('sample_feature')
1703+
assert result.name == 'sample_feature'
1704+
assert result.traffic_type is None
1705+
assert result.killed is False
1706+
assert len(result.treatments) == 2
1707+
assert result.change_number == 123
1708+
assert result.configs['on'] == '{"size":15,"test":20}'
1709+
1710+
assert len(manager.split_names()) == 7
1711+
assert len(manager.splits()) == 7
1712+
1713+
def test_track(self):
1714+
"""Test client.track()."""
1715+
client = self.factory.client()
1716+
assert(client.track('user1', 'user', 'conversion', 1, {"prop1": "value1"}))
1717+
assert(not client.track(None, 'user', 'conversion'))
1718+
assert(not client.track('user1', None, 'conversion'))
1719+
assert(not client.track('user1', 'user', None))
1720+
self._validate_last_events(
1721+
client,
1722+
('user1', 'user', 'conversion', 1, "{'prop1': 'value1'}")
1723+
)

0 commit comments

Comments
 (0)