Skip to content

Commit 5811d9c

Browse files
committed
Updated evaluator
1 parent d9bbce4 commit 5811d9c

File tree

4 files changed

+83
-4
lines changed

4 files changed

+83
-4
lines changed

splitio/client/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
3939
'impressions_disabled': False
4040
}
4141

42-
def __init__(self, factory, recorder, labels_enabled=True):
42+
def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None):
4343
"""
4444
Construct a Client instance.
4545
@@ -64,6 +64,7 @@ def __init__(self, factory, recorder, labels_enabled=True):
6464
self._evaluator = Evaluator(self._splitter)
6565
self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer
6666
self._telemetry_init_producer = self._factory._telemetry_init_producer
67+
self._fallback_treatments_configuration = fallback_treatments_configuration
6768

6869
@property
6970
def ready(self):

splitio/engine/evaluator.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@
2020
class Evaluator(object): # pylint: disable=too-few-public-methods
2121
"""Split Evaluator class."""
2222

23-
def __init__(self, splitter):
23+
def __init__(self, splitter, fallback_treatments_configuration=None):
2424
"""
2525
Construct a Evaluator instance.
2626
2727
:param splitter: partition object.
2828
:type splitter: splitio.engine.splitters.Splitters
2929
"""
3030
self._splitter = splitter
31+
self._fallback_treatments_configuration = fallback_treatments_configuration
3132

3233
def eval_many_with_context(self, key, bucketing, features, attrs, ctx):
3334
"""
@@ -51,6 +52,7 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx):
5152
if not feature:
5253
_LOGGER.warning('Unknown or invalid feature: %s', feature)
5354
label = Label.SPLIT_NOT_FOUND
55+
label, _treatment, config = self._get_fallback_treatment_and_label(feature_name, _treatment, label)
5456
else:
5557
_change_number = feature.change_number
5658
if feature.killed:
@@ -59,17 +61,37 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx):
5961
else:
6062
label, _treatment = self._check_prerequisites(feature, bucketing, key, attrs, ctx, label, _treatment)
6163
label, _treatment = self._get_treatment(feature, bucketing, key, attrs, ctx, label, _treatment)
64+
config = feature.get_configurations_for(_treatment) if feature else None
6265

6366
return {
6467
'treatment': _treatment,
65-
'configurations': feature.get_configurations_for(_treatment) if feature else None,
68+
'configurations': config,
6669
'impression': {
6770
'label': label,
6871
'change_number': _change_number
6972
},
7073
'impressions_disabled': feature.impressions_disabled if feature else None
7174
}
7275

76+
def _get_fallback_treatment_and_label(self, feature_name, treatment, label):
77+
if self._fallback_treatments_configuration == None or self._fallback_treatments_configuration.fallback_config == None:
78+
return label, treatment, None
79+
80+
if self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \
81+
self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None:
82+
_LOGGER.debug('Using Fallback Treatment for feature: %s', feature_name)
83+
return self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \
84+
self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \
85+
self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config
86+
87+
if self._fallback_treatments_configuration.fallback_config.global_fallback_treatment != None:
88+
_LOGGER.debug('Using Global Fallback Treatment.')
89+
return self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \
90+
self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \
91+
self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.config
92+
93+
return label, treatment, None
94+
7395
def _get_treatment(self, feature, bucketing, key, attrs, ctx, label, _treatment):
7496
if _treatment == CONTROL:
7597
treatment, label = self._treatment_for_flag(feature, key, bucketing, attrs, ctx)

splitio/models/fallback_treatment.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def __init__(self, treatment, config=None):
1818
self._config = None
1919
if config != None:
2020
self._config = json.dumps(config)
21+
self._label_prefix = "fallback - "
2122

2223
@property
2324
def treatment(self):
@@ -27,4 +28,9 @@ def treatment(self):
2728
@property
2829
def config(self):
2930
"""Return config."""
30-
return self._config
31+
return self._config
32+
33+
@property
34+
def label_prefix(self):
35+
"""Return label prefix."""
36+
return self._label_prefix

tests/engine/test_evaluator.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from splitio.models.impressions import Label
1212
from splitio.models.grammar import condition
1313
from splitio.models import rule_based_segments
14+
from splitio.models.fallback_treatment import FallbackTreatment
15+
from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration
1416
from splitio.engine import evaluator, splitters
1517
from splitio.engine.evaluator import EvaluationContext
1618
from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, InMemoryRuleBasedSegmentStorage, \
@@ -372,6 +374,54 @@ def test_prerequisites(self):
372374
ctx = evaluation_facctory.context_for('mauro@split.io', ['prereq_chain'])
373375
assert e.eval_with_context('mauro@split.io', 'mauro@split.io', 'prereq_chain', {'email': 'mauro@split.io'}, ctx)['treatment'] == "on_default"
374376

377+
def test_evaluate_treatment_with_fallback(self, mocker):
378+
"""Test that a evaluation return fallback treatment."""
379+
splitter_mock = mocker.Mock(spec=splitters.Splitter)
380+
logger_mock = mocker.Mock(spec=logging.Logger)
381+
evaluator._LOGGER = logger_mock
382+
mocked_split = mocker.Mock(spec=Split)
383+
ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), rbs_segments={})
384+
385+
# should use global fallback
386+
logger_mock.reset_mock()
387+
e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}))))
388+
result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx)
389+
assert result['treatment'] == 'off-global'
390+
assert result['configurations'] == '{"prop": "val"}'
391+
assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND
392+
assert logger_mock.debug.mock_calls[0] == mocker.call("Using Global Fallback Treatment.")
393+
394+
395+
# should use by flag fallback
396+
logger_mock.reset_mock()
397+
e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})))
398+
result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx)
399+
assert result['treatment'] == 'off-some2'
400+
assert result['configurations'] == '{"prop2": "val2"}'
401+
assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND
402+
assert logger_mock.debug.mock_calls[0] == mocker.call("Using Fallback Treatment for feature: %s", "some2")
403+
404+
# should not use any fallback
405+
e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})))
406+
result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx)
407+
assert result['treatment'] == 'control'
408+
assert result['configurations'] == None
409+
assert result['impression']['label'] == Label.SPLIT_NOT_FOUND
410+
411+
# should use by flag fallback
412+
e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})))
413+
result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx)
414+
assert result['treatment'] == 'off-some2'
415+
assert result['configurations'] == '{"prop2": "val2"}'
416+
assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND
417+
418+
# should global flag fallback
419+
e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})))
420+
result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx)
421+
assert result['treatment'] == 'off-global'
422+
assert result['configurations'] == '{"prop": "val"}'
423+
assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND
424+
375425
@pytest.mark.asyncio
376426
async def test_evaluate_treatment_with_rbs_in_condition_async(self):
377427
e = evaluator.Evaluator(splitters.Splitter())

0 commit comments

Comments
 (0)