Skip to content

Commit 4e2b33a

Browse files
authored
Merge pull request #339 from splitio/development
Release 9.4.1
2 parents 749923a + 8ffb5ae commit 4e2b33a

25 files changed

+3177
-116
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
9.4.1 (Apr 18, 2023)
2+
- Fixed storing incorrect Telemetry method latency data
3+
14
9.4.0 (Mar 1, 2023)
25
- Added support to use JSON files in localhost mode.
36
- Updated default periodic telemetry post time to one hour.

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
'pytest-cov',
1313
'importlib-metadata==4.2',
1414
'tomli==1.2.3',
15-
'iniconfig==1.1.1'
15+
'iniconfig==1.1.1',
16+
'attrs==22.1.0'
1617
]
1718

1819
INSTALL_REQUIRES = [

splitio/client/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def track(self, key, traffic_type, event_type, value=None, properties=None):
404404
return_flag = self._recorder.record_track_stats([EventWrapper(
405405
event=event,
406406
size=size,
407-
)], get_current_epoch_time_ms() - start)
407+
)], get_latency_bucket_index(get_current_epoch_time_ms() - start))
408408
return return_flag
409409
except Exception: # pylint: disable=broad-except
410410
self._telemetry_evaluation_producer.record_exception(MethodExceptionsAndLatencies.TRACK)

splitio/client/config.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
DEFAULT_CONFIG = {
13-
'operationMode': 'in-memory',
13+
'operationMode': 'standalone',
1414
'connectionTimeout': 1500,
1515
'streamingEnabled': True,
1616
'featuresRefreshRate': 30,
@@ -56,29 +56,43 @@
5656
'localhostRefreshEnabled': False,
5757
'preforkedInitialization': False,
5858
'dataSampling': DEFAULT_DATA_SAMPLING,
59+
'storageWrapper': None,
60+
'storagePrefix': None,
61+
'storageType': None
5962
}
6063

6164

6265
def _parse_operation_mode(apikey, config):
6366
"""
64-
Process incoming config to determine operation mode.
67+
Process incoming config to determine operation mode and storage type
6568
6669
:param config: user supplied config
6770
:type config: dict
6871
69-
:returns: operation mode
70-
:rtype: str
72+
:returns: operation mode and storage type
73+
:rtype: Tuple (str, str)
7174
"""
7275
if apikey == 'localhost':
73-
return 'localhost-standalone'
76+
_LOGGER.debug('Using Localhost operation mode')
77+
return 'localhost', 'localhost'
7478

7579
if 'redisHost' in config or 'redisSentinels' in config:
76-
return 'redis-consumer'
80+
_LOGGER.debug('Using Redis storage operation mode')
81+
return 'consumer', 'redis'
7782

78-
return 'inmemory-standalone'
83+
if config.get('storageType') is not None:
84+
if config.get('storageType').lower() == 'pluggable':
85+
_LOGGER.debug('Using Pluggable storage operation mode')
86+
return 'consumer', 'pluggable'
7987

88+
_LOGGER.warning('You passed an invalid storageType, acceptable value is '
89+
'`pluggable`. Defaulting storage to In-Memory mode.')
8090

81-
def _sanitize_impressions_mode(mode, refresh_rate=None):
91+
_LOGGER.debug('Using In-Memory operation mode')
92+
return 'standalone', 'memory'
93+
94+
95+
def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None):
8296
"""
8397
Check supplied impressions mode and adjust refresh rate.
8498
@@ -92,10 +106,10 @@ def _sanitize_impressions_mode(mode, refresh_rate=None):
92106
try:
93107
mode = ImpressionsMode(mode.upper())
94108
except (ValueError, AttributeError):
95-
_LOGGER.warning('You passed an invalid impressionsMode, impressionsMode should be '
96-
'one of the following values: `debug`, `none` or `optimized`. '
97-
'Defaulting to `optimized` mode.')
98109
mode = ImpressionsMode.OPTIMIZED
110+
_LOGGER.warning('You passed an invalid impressionsMode, impressionsMode should be ' \
111+
'one of the following values: `debug`, `none` or `optimized`. '
112+
' Defaulting to `optimized` mode.')
99113

100114
if mode == ImpressionsMode.DEBUG:
101115
refresh_rate = max(1, refresh_rate) if refresh_rate is not None else 60
@@ -118,10 +132,10 @@ def sanitize(apikey, config):
118132
:returns: sanitized config
119133
:rtype: dict
120134
"""
121-
config['operationMode'] = _parse_operation_mode(apikey, config)
135+
config['operationMode'], config['storageType'] = _parse_operation_mode(apikey, config)
122136
processed = DEFAULT_CONFIG.copy()
123137
processed.update(config)
124-
imp_mode, imp_rate = _sanitize_impressions_mode(config.get('impressionsMode'),
138+
imp_mode, imp_rate = _sanitize_impressions_mode(config['storageType'], config.get('impressionsMode'),
125139
config.get('impressionsRefreshRate'))
126140
processed['impressionsMode'] = imp_mode
127141
processed['impressionsRefreshRate'] = imp_rate

splitio/client/factory.py

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from splitio.storage.adapters import redis
2525
from splitio.storage.redis import RedisSplitStorage, RedisSegmentStorage, RedisImpressionsStorage, \
2626
RedisEventsStorage, RedisTelemetryStorage
27+
from splitio.storage.pluggable import PluggableEventsStorage, PluggableImpressionsStorage, PluggableSegmentStorage, \
28+
PluggableSplitStorage, PluggableTelemetryStorage
2729

2830
# APIs
2931
from splitio.api.client import HttpClient
@@ -45,7 +47,7 @@
4547

4648
# Synchronizer
4749
from splitio.sync.synchronizer import SplitTasks, SplitSynchronizers, Synchronizer, \
48-
LocalhostSynchronizer, RedisSynchronizer
50+
LocalhostSynchronizer, RedisSynchronizer, PluggableSynchronizer
4951
from splitio.sync.manager import Manager, RedisManager
5052
from splitio.sync.split import SplitSynchronizer, LocalSplitSynchronizer, LocalhostMode
5153
from splitio.sync.segment import SegmentSynchronizer, LocalSegmentSynchronizer
@@ -512,6 +514,84 @@ def _build_redis_factory(api_key, cfg):
512514
return split_factory
513515

514516

517+
def _build_pluggable_factory(api_key, cfg):
518+
"""Build and return a split factory with pluggable storage."""
519+
sdk_metadata = util.get_metadata(cfg)
520+
if not input_validator.validate_pluggable_adapter(cfg):
521+
raise Exception("Pluggable Adapter validation failed, exiting")
522+
523+
pluggable_adapter = cfg.get('storageWrapper')
524+
storage_prefix = cfg.get('storagePrefix')
525+
storages = {
526+
'splits': PluggableSplitStorage(pluggable_adapter, storage_prefix),
527+
'segments': PluggableSegmentStorage(pluggable_adapter, storage_prefix),
528+
'impressions': PluggableImpressionsStorage(pluggable_adapter, sdk_metadata, storage_prefix),
529+
'events': PluggableEventsStorage(pluggable_adapter, sdk_metadata, storage_prefix),
530+
'telemetry': PluggableTelemetryStorage(pluggable_adapter, sdk_metadata, storage_prefix)
531+
}
532+
telemetry_producer = TelemetryStorageProducer(storages['telemetry'])
533+
telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer()
534+
telemetry_init_producer = telemetry_producer.get_telemetry_init_producer()
535+
# Using same class as redis
536+
telemetry_submitter = RedisTelemetrySubmitter(storages['telemetry'])
537+
538+
unique_keys_synchronizer, clear_filter_sync, unique_keys_task, \
539+
clear_filter_task, impressions_count_sync, impressions_count_task, \
540+
imp_strategy = set_classes('PLUGGABLE', cfg['impressionsMode'], pluggable_adapter, storage_prefix)
541+
542+
imp_manager = ImpressionsManager(
543+
imp_strategy,
544+
telemetry_runtime_producer,
545+
_wrap_impression_listener(cfg['impressionListener'], sdk_metadata),
546+
)
547+
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)
564+
recorder = StandardRecorder(
565+
imp_manager,
566+
storages['events'],
567+
storages['impressions'],
568+
storages['telemetry']
569+
)
570+
571+
# Using same class as redis for consumer mode only
572+
manager = RedisManager(synchronizer)
573+
initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True)
574+
initialization_thread.start()
575+
576+
telemetry_init_producer.record_config(cfg, {})
577+
578+
split_factory = SplitFactory(
579+
api_key,
580+
storages,
581+
cfg['labelsEnabled'],
582+
recorder,
583+
manager,
584+
sdk_ready_flag=None,
585+
telemetry_producer=telemetry_producer,
586+
telemetry_init_producer=telemetry_init_producer
587+
)
588+
redundant_factory_count, active_factory_count = _get_active_and_redundant_count()
589+
storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count)
590+
telemetry_submitter.synchronize_config()
591+
592+
return split_factory
593+
594+
515595
def _build_localhost_factory(cfg):
516596
"""Build and return a localhost factory for testing/development purposes."""
517597
telemetry_storage = LocalhostTelemetryStorage()
@@ -606,10 +686,12 @@ def get_factory(api_key, **kwargs):
606686

607687
config = sanitize_config(api_key, kwargs.get('config', {}))
608688

609-
if config['operationMode'] == 'localhost-standalone':
689+
if config['operationMode'] == 'localhost':
610690
split_factory = _build_localhost_factory(config)
611-
elif config['operationMode'] == 'redis-consumer':
691+
elif config['storageType'] == 'redis':
612692
split_factory = _build_redis_factory(api_key, config)
693+
elif config['storageType'] == 'pluggable':
694+
split_factory = _build_pluggable_factory(api_key, config)
613695
else:
614696
split_factory = _build_in_memory_factory(
615697
api_key,

splitio/client/input_validator.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import re
55
import math
6+
import inspect
67

78
from splitio.api import APIException
89
from splitio.api.commons import FetchOptions
@@ -517,3 +518,51 @@ def valid_properties(properties):
517518
_LOGGER.warning('Event has more than 300 properties. Some of them will be trimmed' +
518519
' when processed')
519520
return True, valid_properties if len(valid_properties) else None, size
521+
522+
def validate_pluggable_adapter(config):
523+
"""
524+
Check if pluggable adapter contains the expected method signature
525+
526+
:param config: config parameters
527+
:type config: Dict
528+
529+
:return: True if no issue found otherwise False
530+
:rtype: bool
531+
"""
532+
if config.get('storageType') != 'pluggable':
533+
return True
534+
535+
if config.get('storageWrapper') is None:
536+
_LOGGER.error("Expecting pluggable storage `wrapper` in options, but no valid wrapper instance was provided.")
537+
return False
538+
539+
if config.get('storagePrefix') is not None:
540+
if not isinstance(config.get('storagePrefix'), str):
541+
_LOGGER.error("Pluggable storage prefix should be string type only")
542+
return False
543+
544+
pluggable_adapter = config.get('storageWrapper')
545+
if not isinstance(pluggable_adapter, object):
546+
_LOGGER.error("Pluggable storage instance is not inherted from object class")
547+
return False
548+
549+
expected_methods = {'get': 1, 'get_items': 1, 'get_many': 1, 'set': 2, 'push_items': 2,
550+
'delete': 1, 'increment': 2, 'decrement': 2, 'get_keys_by_prefix': 1,
551+
'get_many': 1, 'add_items' : 2, 'remove_items': 2, 'item_contains': 2,
552+
'get_items_count': 1, 'expire': 2}
553+
methods = inspect.getmembers(pluggable_adapter, predicate=inspect.ismethod)
554+
for exp_method in expected_methods:
555+
method_found = False
556+
get_method_args = set()
557+
for method in methods:
558+
if exp_method == method[0]:
559+
method_found = True
560+
get_method_args = inspect.signature(method[1]).parameters
561+
break
562+
if not method_found:
563+
_LOGGER.error("Pluggable adapter does not have required method: %s" % exp_method)
564+
return False
565+
if len(get_method_args) < expected_methods[exp_method]:
566+
_LOGGER.error("Pluggable adapter method %s has less than required arguments count: %s : " % (exp_method, len(get_method_args)))
567+
return False
568+
return True

splitio/engine/impressions/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
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
1414
clear_filter_task = None
1515
impressions_count_sync = None
1616
impressions_count_task = None
1717
sender_adapter = None
18-
if storage_mode == 'REDIS':
18+
if storage_mode == 'PLUGGABLE':
19+
sender_adapter = PluggableSenderAdapter(api_adapter, prefix)
20+
api_telemetry_adapter = sender_adapter
21+
api_impressions_adapter = sender_adapter
22+
elif storage_mode == 'REDIS':
1923
sender_adapter = RedisSenderAdapter(api_adapter)
2024
api_telemetry_adapter = sender_adapter
2125
api_impressions_adapter = sender_adapter

0 commit comments

Comments
 (0)