diff --git a/splitio/client/client.py b/splitio/client/client.py index 257c9b97..9a33e67c 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -2,6 +2,7 @@ import logging import json from collections import namedtuple +import copy from splitio.engine.evaluator import Evaluator, CONTROL, EvaluationDataFactory, AsyncEvaluationDataFactory from splitio.engine.splitters import Splitter @@ -9,6 +10,7 @@ from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies from splitio.client import input_validator +from splitio.client.util import get_fallback_treatment_and_label from splitio.util.time import get_current_epoch_time_ms, utctime_ms @@ -39,7 +41,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes 'impressions_disabled': False } - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -61,9 +63,10 @@ def __init__(self, factory, recorder, labels_enabled=True): self._feature_flag_storage = factory._get_storage('splits') # pylint: disable=protected-access self._segment_storage = factory._get_storage('segments') # pylint: disable=protected-access self._events_storage = factory._get_storage('events') # pylint: disable=protected-access - self._evaluator = Evaluator(self._splitter) + self._evaluator = Evaluator(self._splitter, fallback_treatments_configuration) self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer + self._fallback_treatments_configuration = fallback_treatments_configuration @property def ready(self): @@ -203,11 +206,26 @@ def _validate_track(self, key, traffic_type, event_type, value=None, properties= def _get_properties(self, evaluation_options): return evaluation_options.properties if evaluation_options != None else None - + def _get_fallback_treatment_with_config(self, treatment, feature): + label = "" + + label, treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature, treatment, label, _LOGGER) + return treatment, config + + def _get_fallback_eval_results(self, eval_result, feature): + result = copy.deepcopy(eval_result) + result["impression"]["label"], result["treatment"], result["configurations"] = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature, result["treatment"], result["impression"]["label"], _LOGGER) + return result + + 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) + class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -222,7 +240,7 @@ def __init__(self, factory, recorder, labels_enabled=True): :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled) + ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration) self._context_factory = EvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) def destroy(self): @@ -254,10 +272,11 @@ def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_opti try: treatment, _ = self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, evaluation_options) return treatment - + except: _LOGGER.error('get_treatment failed') - return CONTROL + treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + return treatment def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ @@ -282,8 +301,8 @@ def get_treatment_with_config(self, key, feature_flag_name, attributes=None, eva except Exception: _LOGGER.error('get_treatment_with_config failed') - return CONTROL, None - + return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None): """ Validate key, feature flag name and object, and get the treatment and config with an optional dictionary of attributes. @@ -302,7 +321,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio :rtype: dict """ if not self._client_is_usable(): # not destroyed & not waiting for a fork - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) start = get_current_epoch_time_ms() if not self.ready: @@ -312,9 +331,10 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio try: key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) - result = self._NON_READY_EVAL_RESULT + result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature) + if self.ready: try: ctx = self._context_factory.context_for(key, [feature]) @@ -324,15 +344,15 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) self._telemetry_evaluation_producer.record_exception(method) - result = self._FAILED_EVAL_RESULT + result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] != Label.SPLIT_NOT_FOUND: + if self._check_impression_label(result): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] - + def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ Evaluate multiple feature flags and return a dictionary with all the feature flag/treatments. @@ -356,7 +376,7 @@ def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_op return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: - return {feature: CONTROL for feature in feature_flag_names} + return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names} def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ @@ -380,7 +400,7 @@ def get_treatments_with_config(self, key, feature_flag_names, attributes=None, e return self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options) except Exception: - return {feature: (CONTROL, None) for feature in feature_flag_names} + return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names} def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ @@ -604,7 +624,7 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt """ start = get_current_epoch_time_ms() if not self._client_is_usable(): - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) if not self.ready: _LOGGER.error("Client is not ready - no calls possible") @@ -613,9 +633,9 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt try: key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options) except _InvalidInputError: - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) - results = {n: self._NON_READY_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features} if self.ready: try: ctx = self._context_factory.context_for(key, features) @@ -625,12 +645,12 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) self._telemetry_evaluation_producer.record_exception(method) - results = {n: self._FAILED_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, n) for n in features} properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) - if i.Impression.label != Label.SPLIT_NOT_FOUND + if i.Impression.label == None or (i.Impression.label != None and i.Impression.label.find(Label.SPLIT_NOT_FOUND)) == -1 ] self._record_stats(imp_decorated_attrs, start, method) @@ -706,7 +726,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): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -721,7 +741,7 @@ def __init__(self, factory, recorder, labels_enabled=True): :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled) + ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration) self._context_factory = AsyncEvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) async def destroy(self): @@ -756,7 +776,8 @@ async def get_treatment(self, key, feature_flag_name, attributes=None, evaluatio except: _LOGGER.error('get_treatment failed') - return CONTROL + treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + return treatment async def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ @@ -781,7 +802,7 @@ async def get_treatment_with_config(self, key, feature_flag_name, attributes=Non except Exception: _LOGGER.error('get_treatment_with_config failed') - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) async def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None): """ @@ -801,7 +822,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation :rtype: dict """ if not self._client_is_usable(): # not destroyed & not waiting for a fork - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) start = get_current_epoch_time_ms() if not self.ready: @@ -811,9 +832,9 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation try: key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) - result = self._NON_READY_EVAL_RESULT + result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature) if self.ready: try: ctx = await self._context_factory.context_for(key, [feature]) @@ -823,10 +844,10 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) await self._telemetry_evaluation_producer.record_exception(method) - result = self._FAILED_EVAL_RESULT + result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] != Label.SPLIT_NOT_FOUND: + if self._check_impression_label(result): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] @@ -854,7 +875,7 @@ async def get_treatments(self, key, feature_flag_names, attributes=None, evaluat return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: - return {feature: CONTROL for feature in feature_flag_names} + return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names} async def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ @@ -878,8 +899,7 @@ async def get_treatments_with_config(self, key, feature_flag_names, attributes=N return await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options) except Exception: - _LOGGER.error("AA", exc_info=True) - return {feature: (CONTROL, None) for feature in feature_flag_names} + return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names} async def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ @@ -1017,7 +1037,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati """ start = get_current_epoch_time_ms() if not self._client_is_usable(): - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) if not self.ready: _LOGGER.error("Client is not ready - no calls possible") @@ -1026,9 +1046,9 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati try: key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options) except _InvalidInputError: - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) - results = {n: self._NON_READY_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features} if self.ready: try: ctx = await self._context_factory.context_for(key, features) @@ -1038,12 +1058,12 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) await self._telemetry_evaluation_producer.record_exception(method) - results = {n: self._FAILED_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, n) for n in features} properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) - if i.Impression.label != Label.SPLIT_NOT_FOUND + if i.Impression.label == None or (i.Impression.label != None and i.Impression.label.find(Label.SPLIT_NOT_FOUND)) == -1 ] await self._record_stats(imp_decorated_attrs, start, method) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index f6070243..e06d6cf9 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -170,7 +170,8 @@ def __init__( # pylint: disable=too-many-arguments telemetry_producer=None, telemetry_init_producer=None, telemetry_submitter=None, - preforked_initialization=False + preforked_initialization=False, + fallback_treatments_configuration=None ): """ Class constructor. @@ -201,6 +202,7 @@ def __init__( # pylint: disable=too-many-arguments self._ready_time = get_current_epoch_time_ms() _LOGGER.debug("Running in threading mode") self._sdk_internal_ready_flag = sdk_ready_flag + self._fallback_treatments_configuration = fallback_treatments_configuration self._start_status_updater() def _start_status_updater(self): @@ -242,7 +244,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) + return Client(self, self._recorder, self._labels_enabled, self._fallback_treatments_configuration) def manager(self): """ @@ -338,7 +340,8 @@ def __init__( # pylint: disable=too-many-arguments telemetry_init_producer=None, telemetry_submitter=None, manager_start_task=None, - api_client=None + api_client=None, + fallback_treatments_configuration=None ): """ Class constructor. @@ -372,6 +375,7 @@ def __init__( # pylint: disable=too-many-arguments self._sdk_ready_flag = asyncio.Event() self._ready_task = asyncio.get_running_loop().create_task(self._update_status_when_ready_async()) self._api_client = api_client + self._fallback_treatments_configuration = fallback_treatments_configuration async def _update_status_when_ready_async(self): """Wait until the sdk is ready and update the status for async mode.""" @@ -460,7 +464,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) + return ClientAsync(self, self._recorder, self._labels_enabled, self._fallback_treatments_configuration) def _wrap_impression_listener(listener, metadata): """ @@ -623,7 +627,8 @@ 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, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, + fallback_treatments_configuration=cfg['fallbackTreatments']) initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() @@ -631,7 +636,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, sdk_ready_flag, telemetry_producer, telemetry_init_producer, - telemetry_submitter) + telemetry_submitter, fallback_treatments_configuration=cfg['fallbackTreatments']) async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-localsa auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None, @@ -750,7 +755,7 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= recorder, manager, telemetry_producer, telemetry_init_producer, telemetry_submitter, manager_start_task=manager_start_task, - api_client=http_client) + api_client=http_client, fallback_treatments_configuration=cfg['fallbackTreatments']) def _build_redis_factory(api_key, cfg): """Build and return a split factory with redis-based storage.""" @@ -828,7 +833,8 @@ def _build_redis_factory(api_key, cfg): manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, - telemetry_init_producer=telemetry_init_producer + telemetry_init_producer=telemetry_init_producer, + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -910,7 +916,8 @@ async def _build_redis_factory_async(api_key, cfg): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -992,7 +999,8 @@ def _build_pluggable_factory(api_key, cfg): manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, - telemetry_init_producer=telemetry_init_producer + telemetry_init_producer=telemetry_init_producer, + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1072,7 +1080,8 @@ async def _build_pluggable_factory_async(api_key, cfg): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1150,6 +1159,7 @@ def _build_localhost_factory(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitter(), + fallback_treatments_configuration=cfg['fallbackTreatments'] ) async def _build_localhost_factory_async(cfg): @@ -1220,7 +1230,8 @@ async def _build_localhost_factory_async(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitterAsync(), - manager_start_task=manager_start_task + manager_start_task=manager_start_task, + fallback_treatments_configuration=cfg['fallbackTreatments'] ) def get_factory(api_key, **kwargs): diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 90d1028f..aaaf8026 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -7,6 +7,7 @@ from splitio.client.key import Key from splitio.client import client +from splitio.client.util import get_fallback_treatment_and_label from splitio.engine.evaluator import CONTROL from splitio.models.fallback_treatment import FallbackTreatment @@ -16,7 +17,7 @@ EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$' MAX_PROPERTIES_LENGTH_BYTES = 32768 _FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$' -_FALLBACK_TREATMENT_REGEX = '^[a-zA-Z][a-zA-Z0-9-_;]+$' +_FALLBACK_TREATMENT_REGEX = '^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$' _FALLBACK_TREATMENT_SIZE = 100 def _check_not_null(value, name, operation): @@ -502,7 +503,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name valid_feature_flags.append(ff) return valid_feature_flags -def generate_control_treatments(feature_flags): +def generate_control_treatments(feature_flags, fallback_treatments_configuration): """ Generate valid feature flags to control. @@ -517,7 +518,13 @@ def generate_control_treatments(feature_flags): to_return = {} for feature_flag in feature_flags: if isinstance(feature_flag, str) and len(feature_flag.strip())> 0: - to_return[feature_flag] = (CONTROL, None) + treatment = CONTROL + config = None + label = "" + label, treatment, config = get_fallback_treatment_and_label(fallback_treatments_configuration, + feature_flag, treatment, label, _LOGGER) + + to_return[feature_flag] = (treatment, config) return to_return @@ -719,6 +726,10 @@ def validate_fallback_treatment(fallback_treatment): if not isinstance(fallback_treatment, FallbackTreatment): _LOGGER.warning("Config: Fallback treatment instance should be FallbackTreatment, input is discarded") return False + + if not isinstance(fallback_treatment.treatment, str): + _LOGGER.warning("Config: Fallback treatment value should be str type, input is discarded") + return False if not validate_regex_name(fallback_treatment.treatment): _LOGGER.warning("Config: Fallback treatment should match regex %s", _FALLBACK_TREATMENT_REGEX) diff --git a/splitio/client/util.py b/splitio/client/util.py index e4892512..1f01de3f 100644 --- a/splitio/client/util.py +++ b/splitio/client/util.py @@ -51,3 +51,22 @@ def get_metadata(config): version = 'python-%s' % __version__ ip_address, hostname = _get_hostname_and_ip(config) return SdkMetadata(version, hostname, ip_address) + +def get_fallback_treatment_and_label(fallback_treatments_configuration, feature_name, treatment, label, _logger): + if fallback_treatments_configuration == None: + return label, treatment, None + + if fallback_treatments_configuration.by_flag_fallback_treatment != None and \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name) != None: + _logger.debug('Using Fallback Treatment for feature: %s', feature_name) + return fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).treatment, \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).config + + if fallback_treatments_configuration.global_fallback_treatment != None: + _logger.debug('Using Global Fallback Treatment.') + return fallback_treatments_configuration.global_fallback_treatment.label_prefix + label, \ + fallback_treatments_configuration.global_fallback_treatment.treatment, \ + fallback_treatments_configuration.global_fallback_treatment.config + + return label, treatment, None diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index 4b37229c..2a564d3a 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -2,6 +2,7 @@ import logging from collections import namedtuple +from splitio.client.util import get_fallback_treatment_and_label from splitio.models.impressions import Label from splitio.models.grammar.condition import ConditionType from splitio.models.grammar.matchers.misc import DependencyMatcher @@ -52,7 +53,8 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): if not feature: _LOGGER.warning('Unknown or invalid feature: %s', feature) label = Label.SPLIT_NOT_FOUND - label, _treatment, config = self._get_fallback_treatment_and_label(feature_name, _treatment, label) + label, _treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature_name, _treatment, label, _LOGGER) else: _change_number = feature.change_number if feature.killed: @@ -72,25 +74,6 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): }, 'impressions_disabled': feature.impressions_disabled if feature else None } - - def _get_fallback_treatment_and_label(self, feature_name, treatment, label): - if self._fallback_treatments_configuration == None: - return label, treatment, None - - if self._fallback_treatments_configuration.by_flag_fallback_treatment != None and \ - self._fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name) != None: - _LOGGER.debug('Using Fallback Treatment for feature: %s', feature_name) - return self._fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ - self._fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).treatment, \ - self._fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).config - - if self._fallback_treatments_configuration.global_fallback_treatment != None: - _LOGGER.debug('Using Global Fallback Treatment.') - return self._fallback_treatments_configuration.global_fallback_treatment.label_prefix + label, \ - self._fallback_treatments_configuration.global_fallback_treatment.treatment, \ - self._fallback_treatments_configuration.global_fallback_treatment.config - - return label, treatment, None def _get_treatment(self, feature, bucketing, key, attrs, ctx, label, _treatment): if _treatment == CONTROL: diff --git a/splitio/storage/adapters/redis.py b/splitio/storage/adapters/redis.py index 78d88487..4cf87b5e 100644 --- a/splitio/storage/adapters/redis.py +++ b/splitio/storage/adapters/redis.py @@ -715,7 +715,7 @@ def _build_default_client(config): # pylint: disable=too-many-locals unix_socket_path = config.get('redisUnixSocketPath', None) encoding = config.get('redisEncoding', 'utf-8') encoding_errors = config.get('redisEncodingErrors', 'strict') - errors = config.get('redisErrors', None) +# errors = config.get('redisErrors', None) decode_responses = config.get('redisDecodeResponses', True) retry_on_timeout = config.get('redisRetryOnTimeout', False) ssl = config.get('redisSsl', False) @@ -740,7 +740,7 @@ def _build_default_client(config): # pylint: disable=too-many-locals unix_socket_path=unix_socket_path, encoding=encoding, encoding_errors=encoding_errors, - errors=errors, +# errors=errors, Starting from redis 6.0.0 errors argument is removed decode_responses=decode_responses, retry_on_timeout=retry_on_timeout, ssl=ssl, diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 9a6848eb..75f46464 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -9,6 +9,8 @@ from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync +from splitio.models.fallback_config import FallbackTreatmentsConfiguration +from splitio.models.fallback_treatment import FallbackTreatment from splitio.models.impressions import Impression, Label from splitio.models.events import Event, EventWrapper from splitio.storage import EventStorage, ImpressionStorage, SegmentStorage, SplitStorage, RuleBasedSegmentsStorage @@ -1375,6 +1377,287 @@ def synchronize_config(*_): assert client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': ('on', None)} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=RuntimeError()) + def test_fallback_treatment_eval_exception(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + 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()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}))) + + def get_feature_flag_names_by_flag_sets(*_): + return ["some", "some2"] + client._get_feature_flag_names_by_flag_sets = get_feature_flag_names_by_flag_sets + + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key_m", ["some", "some2"]) + assert(treatment == {"some": "on-global", "some2": "on-global"}) + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + assert(self.imps[1].treatment == "on-global") + assert(self.imps[1].label == "fallback - exception") + + assert(client.get_treatment_with_config("key", "some") == ("on-global", '{"prop": "val"}')) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-global", "some2": "on-global"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-global", "some2": "on-global"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key2_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "on-global"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "on-global") + assert(imp.label == "fallback - exception") + assert assert_both == 2 + + assert(client.get_treatment_with_config("key", "some") == ("on-local", None)) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "on-global"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "on-global"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", {"prop":"val"})}) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key3_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "control"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "control") + assert(imp.label == "exception") + assert assert_both == 2 + + assert(client.get_treatment_with_config("key", "some") == ("on-local", '{"prop": "val"}')) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "control"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "control"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "exception") + + try: + factory.destroy() + except: + pass + + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) + def test_fallback_treatment_exception_no_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + 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()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps == None) + + try: + factory.destroy() + except: + pass + + @mock.patch('splitio.client.client.Client.ready', side_effect=None) + def test_fallback_treatment_not_ready_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + 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()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) + client.ready = False + + treatment = client.get_treatment("key", "some") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "not ready") + + try: + factory.destroy() + except: + pass + class ClientAsyncTests(object): # pylint: disable=too-few-public-methods """Split client async test cases.""" @@ -2585,3 +2868,297 @@ async def synchronize_config(*_): assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': ('on', None)} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + @pytest.mark.asyncio + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=RuntimeError()) + async def test_fallback_treatment_eval_exception(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = await InMemoryTelemetryStorageAsync.create() + telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) + telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_evaluation_producer, telemetry_producer.get_telemetry_runtime_producer()) + + factory = SplitFactoryAsync(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock(), + mocker.Mock() + ) + + self.imps = None + async def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + async def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = ClientAsync(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}))) + + async def get_feature_flag_names_by_flag_sets(*_): + return ["some", "some2"] + client._get_feature_flag_names_by_flag_sets = get_feature_flag_names_by_flag_sets + + async def fetch_many(*_): + return {"some": from_raw(splits_json['splitChange1_1']['ff']['d'][0])} + split_storage.fetch_many = fetch_many + + async def fetch_many_rbs(*_): + return {} + rb_segment_storage.fetch_many = fetch_many_rbs + + treatment = await client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key_m", ["some", "some2"]) + assert(treatment == {"some": "on-global", "some2": "on-global"}) + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + assert(self.imps[1].treatment == "on-global") + assert(self.imps[1].label == "fallback - exception") + + assert(await client.get_treatment_with_config("key", "some") == ("on-global", '{"prop": "val"}')) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-global", "some2": "on-global"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-global", "some2": "on-global"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")}) + treatment = await client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key2_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "on-global"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "on-global") + assert(imp.label == "fallback - exception") + assert assert_both == 2 + + assert(await client.get_treatment_with_config("key", "some") == ("on-local", None)) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "on-global"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "on-global"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", {"prop":"val"})}) + treatment = await client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key3_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "control"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "control") + assert(imp.label == "exception") + assert assert_both == 2 + + assert(await client.get_treatment_with_config("key", "some") == ("on-local", '{"prop": "val"}')) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "control"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "control"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + treatment = await client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "exception") + + try: + await factory.destroy() + except: + pass + + @pytest.mark.asyncio + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) + def test_fallback_treatment_exception_no_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + 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()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps == None) + + try: + factory.destroy() + except: + pass + + @pytest.mark.asyncio + @mock.patch('splitio.client.client.Client.ready', side_effect=None) + def test_fallback_treatment_not_ready_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + 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()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) + client.ready = False + + treatment = client.get_treatment("key", "some") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "not ready") + + try: + factory.destroy() + except: + pass \ No newline at end of file diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 76164016..0017938c 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -102,7 +102,7 @@ def test_sanitize(self, mocker): assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(FallbackTreatment("123"))}) + processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(FallbackTreatment(123))}) assert processed['fallbackTreatments'].global_fallback_treatment == None assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index fbe499d6..86e13088 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -13,6 +13,8 @@ from splitio.storage import redis, inmemmory, pluggable from splitio.tasks.util import asynctask from splitio.engine.impressions.impressions import Manager as ImpressionsManager +from splitio.models.fallback_config import FallbackTreatmentsConfiguration +from splitio.models.fallback_treatment import FallbackTreatment from splitio.sync.manager import Manager, ManagerAsync from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitSynchronizers, SplitTasks from splitio.sync.split import SplitSynchronizer, SplitSynchronizerAsync @@ -94,7 +96,7 @@ def test_redis_client_creation(self, mocker): """Test that a client with redis storage is created correctly.""" strict_redis_mock = mocker.Mock() mocker.patch('splitio.storage.adapters.redis.StrictRedis', new=strict_redis_mock) - + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on")) config = { 'labelsEnabled': False, 'impressionListener': 123, @@ -119,7 +121,8 @@ def test_redis_client_creation(self, mocker): 'redisSslCertReqs': 'some_cert_req', 'redisSslCaCerts': 'some_ca_cert', 'redisMaxConnections': 999, - 'flagSetsFilter': ['set_1'] + 'flagSetsFilter': ['set_1'], + 'fallbackTreatments': fallback_treatments_configuration } factory = get_factory('some_api_key', config=config) class TelemetrySubmitterMock(): @@ -133,6 +136,7 @@ def synchronize_config(*_): assert isinstance(factory._get_storage('events'), redis.RedisEventsStorage) assert factory._get_storage('splits').flag_set_filter.flag_sets == set([]) + assert factory._fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment adapter = factory._get_storage('splits')._redis assert adapter == factory._get_storage('segments')._redis @@ -153,7 +157,7 @@ def synchronize_config(*_): unix_socket_path='/some_path', encoding='utf-8', encoding_errors='non-strict', - errors=True, +# errors=True, decode_responses=True, retry_on_timeout=True, ssl=True, @@ -705,10 +709,13 @@ class SplitFactoryAsyncTests(object): @pytest.mark.asyncio async def test_flag_sets_counts(self): + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on")) factory = await get_factory_async("none", config={ 'flagSetsFilter': ['set1', 'set2', 'set3'], - 'streamEnabled': False + 'streamEnabled': False, + 'fallbackTreatments': fallback_treatments_configuration }) + assert factory._fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 0 await factory.destroy() diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 476db45e..85afb248 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1,5 +1,6 @@ """Unit tests for the input_validator module.""" import pytest +import logging from splitio.client.factory import SplitFactory, get_factory, SplitFactoryAsync, get_factory_async from splitio.client.client import CONTROL, Client, _LOGGER as _logger, ClientAsync @@ -9,6 +10,7 @@ InMemorySplitStorage, InMemorySplitStorageAsync, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync from splitio.models.splits import Split from splitio.client import input_validator +from splitio.client.manager import SplitManager, SplitManagerAsync from splitio.recorder.recorder import StandardRecorder, StandardRecorderAsync from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageProducerAsync from splitio.engine.impressions.impressions import Manager as ImpressionManager @@ -1643,19 +1645,13 @@ def test_fallback_treatments(self, mocker): _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("on/c")) assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") - ] - - _logger.reset_mock() - assert not input_validator.validate_fallback_treatment(FallbackTreatment("9on")) - assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("on$as")) assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] assert input_validator.validate_fallback_treatment(FallbackTreatment("on_c")) diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index f8625f6a..257d9099 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -29,6 +29,8 @@ 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 +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 @@ -196,6 +198,11 @@ def _get_treatment(factory, skip_rbs=False): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('prereq_feature', 'user1234', 'off_default')) + # test fallback treatment + assert client.get_treatment('user4321', 'fallback_feature') == 'on-local' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatment_with_config(factory): """Test client.get_treatment_with_config().""" try: @@ -229,6 +236,11 @@ def _get_treatment_with_config(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatment_with_config('user4321', 'fallback_feature') == ('on-local', '{"prop": "val"}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments(factory): """Test client.get_treatments().""" try: @@ -267,6 +279,11 @@ def _get_treatments(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatments('user4321', ['fallback_feature']) == {'fallback_feature': 'on-local'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments_with_config(factory): """Test client.get_treatments_with_config().""" try: @@ -306,6 +323,11 @@ def _get_treatments_with_config(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatments_with_config('user4321', ['fallback_feature']) == {'fallback_feature': ('on-local', '{"prop": "val"}')} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments_by_flag_set(factory): """Test client.get_treatments_by_flag_set().""" try: @@ -539,6 +561,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -697,6 +720,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -819,7 +843,11 @@ def setup_method(self): 'sdk_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'events_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'auth_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), - 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug'} + 'config': {'connectTimeout': 10000, + 'streamingEnabled': False, + 'impressionsMode': 'debug', + 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + } } self.factory = get_factory('some_apikey', **kwargs) @@ -989,6 +1017,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -1177,6 +1206,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init class LocalhostIntegrationTests(object): # pylint: disable=too-few-public-methods @@ -1400,6 +1430,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1595,6 +1626,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1789,6 +1821,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1942,6 +1975,7 @@ def test_optimized(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -1964,6 +1998,8 @@ def test_optimized(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' def test_debug(self): split_storage = InMemorySplitStorage() @@ -1997,6 +2033,7 @@ def test_debug(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2019,6 +2056,8 @@ def test_debug(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' def test_none(self): split_storage = InMemorySplitStorage() @@ -2052,6 +2091,7 @@ def test_none(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2076,6 +2116,8 @@ def test_none(self): assert imps_count[1].count == 1 assert imps_count[2].feature == 'SPLIT_3' assert imps_count[2].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' class RedisImpressionsToggleIntegrationTests(object): """Run impression toggle tests for Redis.""" @@ -2113,6 +2155,7 @@ def test_optimized(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2141,6 +2184,8 @@ def test_optimized(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2177,6 +2222,7 @@ def test_debug(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2205,6 +2251,8 @@ def test_debug(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2241,6 +2289,7 @@ def test_none(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2271,6 +2320,8 @@ def test_none(self): assert imps_count[1].count == 1 assert imps_count[2].feature == 'SPLIT_3' assert imps_count[2].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2342,6 +2393,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2513,6 +2565,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2653,7 +2706,11 @@ async def _setup_method(self): 'sdk_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'events_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'auth_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), - 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug'} + 'config': {'connectTimeout': 10000, + 'streamingEnabled': False, + 'impressionsMode': 'debug', + 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + } } self.factory = await get_factory_async('some_apikey', **kwargs) @@ -2861,7 +2918,8 @@ async def _setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3083,7 +3141,8 @@ async def _setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3317,7 +3376,8 @@ async def _setup_method(self): RedisManagerAsync(PluggableSynchronizerAsync()), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3546,7 +3606,8 @@ async def _setup_method(self): RedisManagerAsync(PluggableSynchronizerAsync()), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() @@ -3781,6 +3842,7 @@ async def _setup_method(self): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -4481,6 +4543,11 @@ async def _get_treatment_async(factory, skip_rbs=False): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('regex_test', 'abc4', 'on')) + # test fallback treatment + assert await client.get_treatment('user4321', 'fallback_feature') == 'on-local' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + if skip_rbs: return @@ -4537,6 +4604,11 @@ async def _get_treatment_with_config_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatment_with_config('user4321', 'fallback_feature') == ('on-local', '{"prop": "val"}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_async(factory): """Test client.get_treatments().""" try: @@ -4575,6 +4647,11 @@ async def _get_treatments_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatments('user4321', ['fallback_feature']) == {'fallback_feature': 'on-local'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_with_config_async(factory): """Test client.get_treatments_with_config().""" try: @@ -4614,6 +4691,11 @@ async def _get_treatments_with_config_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatments_with_config('user4321', ['fallback_feature']) == {'fallback_feature': ('on-local', '{"prop": "val"}')} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_by_flag_set_async(factory): """Test client.get_treatments_by_flag_set().""" try: diff --git a/tests/push/test_manager.py b/tests/push/test_manager.py index c85301d8..3525baf3 100644 --- a/tests/push/test_manager.py +++ b/tests/push/test_manager.py @@ -259,7 +259,6 @@ class PushManagerAsyncTests(object): async def test_connection_success(self, mocker): """Test the initial status is ok and reset() works as expected.""" api_mock = mocker.Mock() - async def authenticate(): return Token(True, 'abc', {}, 2000000, 1000000) api_mock.authenticate.side_effect = authenticate @@ -274,8 +273,8 @@ async def coro(): t = 0 try: while t < 3: - yield SSEEvent('1', EventType.MESSAGE, '', '{}') await asyncio.sleep(1) + yield SSEEvent('1', EventType.MESSAGE, '', '{}') t += 1 except Exception: pass @@ -295,7 +294,7 @@ async def stop(): manager._sse_client = sse_mock async def deferred_shutdown(): - await asyncio.sleep(1) + await asyncio.sleep(2) await manager.stop(True) manager.start() @@ -309,7 +308,10 @@ async def deferred_shutdown(): assert self.token.exp == 2000000 assert self.token.iat == 1000000 - await shutdown_task + try: + await shutdown_task + except: + pass assert not manager._running assert(telemetry_storage._streaming_events._streaming_events[0]._type == StreamingEventTypes.TOKEN_REFRESH.value) assert(telemetry_storage._streaming_events._streaming_events[1]._type == StreamingEventTypes.CONNECTION_ESTABLISHED.value) diff --git a/tests/storage/adapters/test_redis_adapter.py b/tests/storage/adapters/test_redis_adapter.py index a6bc72dc..78d28bbc 100644 --- a/tests/storage/adapters/test_redis_adapter.py +++ b/tests/storage/adapters/test_redis_adapter.py @@ -99,7 +99,7 @@ def test_adapter_building(self, mocker): 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', - 'redisErrors': 'abc', +# 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': True, @@ -126,7 +126,7 @@ def test_adapter_building(self, mocker): unix_socket_path='/tmp/socket', encoding='utf-8', encoding_errors='strict', - errors='abc', +# errors='abc', decode_responses=True, retry_on_timeout=True, ssl=True, @@ -151,7 +151,7 @@ def test_adapter_building(self, mocker): 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', - 'redisErrors': 'abc', +# 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': False,