Skip to content

Commit 9cfc88f

Browse files
authored
Merge pull request #410 from splitio/async-engine-evaluator
Updated engine evaluator class
2 parents 4bc41c7 + bc2f463 commit 9cfc88f

File tree

2 files changed

+58
-133
lines changed

2 files changed

+58
-133
lines changed

splitio/engine/evaluator.py

Lines changed: 31 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Split evaluator module."""
22
import logging
3-
from splitio.models.grammar.condition import ConditionType
43
from splitio.models.impressions import Label
54

65

@@ -13,38 +12,30 @@
1312
class Evaluator(object): # pylint: disable=too-few-public-methods
1413
"""Split Evaluator class."""
1514

16-
def __init__(self, feature_flag_storage, segment_storage, splitter):
15+
def __init__(self, splitter):
1716
"""
1817
Construct a Evaluator instance.
1918
20-
:param feature_flag_storage: feature_flag storage.
21-
:type feature_flag_storage: splitio.storage.SplitStorage
22-
23-
:param segment_storage: Segment storage.
24-
:type segment_storage: splitio.storage.SegmentStorage
19+
:param splitter: partition object.
20+
:type splitter: splitio.engine.splitters.Splitters
2521
"""
26-
self._feature_flag_storage = feature_flag_storage
27-
self._segment_storage = segment_storage
2822
self._splitter = splitter
2923

30-
def _evaluate_treatment(self, feature_flag_name, matching_key, bucketing_key, attributes, feature_flag):
24+
def _evaluate_treatment(self, feature_flag, matching_key, bucketing_key, condition_matchers):
3125
"""
3226
Evaluate the user submitted data against a feature and return the resulting treatment.
3327
34-
:param feature_flag_name: The feature flag for which to get the treatment
35-
:type feature: str
28+
:param feature_flag: Split object
29+
:type feature_flag: splitio.models.splits.Split|None
3630
3731
:param matching_key: The matching_key for which to get the treatment
3832
:type matching_key: str
3933
4034
:param bucketing_key: The bucketing_key for which to get the treatment
4135
:type bucketing_key: str
4236
43-
:param attributes: An optional dictionary of attributes
44-
:type attributes: dict
45-
46-
:param feature_flag: Split object
47-
:type attributes: splitio.models.splits.Split|None
37+
:param condition_matchers: array of condition matchers for passed feature_flag
38+
:type bucketing_key: Dict
4839
4940
:return: The treatment for the key and feature flag
5041
:rtype: object
@@ -54,19 +45,19 @@ def _evaluate_treatment(self, feature_flag_name, matching_key, bucketing_key, at
5445
_change_number = -1
5546

5647
if feature_flag is None:
57-
_LOGGER.warning('Unknown or invalid feature: %s', feature_flag_name)
48+
_LOGGER.warning('Unknown or invalid feature: %s', feature_flag.name)
5849
label = Label.SPLIT_NOT_FOUND
5950
else:
6051
_change_number = feature_flag.change_number
6152
if feature_flag.killed:
6253
label = Label.KILLED
6354
_treatment = feature_flag.default_treatment
6455
else:
65-
treatment, label = self._get_treatment_for_split(
56+
treatment, label = self._get_treatment_for_feature_flag(
6657
feature_flag,
6758
matching_key,
6859
bucketing_key,
69-
attributes
60+
condition_matchers
7061
)
7162
if treatment is None:
7263
label = Label.NO_CONDITION_MATCHED
@@ -83,61 +74,58 @@ def _evaluate_treatment(self, feature_flag_name, matching_key, bucketing_key, at
8374
}
8475
}
8576

86-
def evaluate_feature(self, feature_flag_name, matching_key, bucketing_key, attributes=None):
77+
def evaluate_feature(self, feature_flag, matching_key, bucketing_key, condition_matchers):
8778
"""
8879
Evaluate the user submitted data against a feature and return the resulting treatment.
8980
90-
:param feature_flag_name: The feature flag for which to get the treatment
91-
:type feature: str
81+
:param feature_flag: Split object
82+
:type feature_flag: splitio.models.splits.Split|None
9283
9384
:param matching_key: The matching_key for which to get the treatment
9485
:type matching_key: str
9586
9687
:param bucketing_key: The bucketing_key for which to get the treatment
9788
:type bucketing_key: str
9889
99-
:param attributes: An optional dictionary of attributes
100-
:type attributes: dict
90+
:param condition_matchers: array of condition matchers for passed feature_flag
91+
:type bucketing_key: Dict
10192
10293
:return: The treatment for the key and split
10394
:rtype: object
10495
"""
105-
# Fetching Split definition
106-
feature_flag = self._feature_flag_storage.get(feature_flag_name)
107-
10896
# Calling evaluation
109-
evaluation = self._evaluate_treatment(feature_flag_name, matching_key,
110-
bucketing_key, attributes, feature_flag)
97+
evaluation = self._evaluate_treatment(feature_flag, matching_key,
98+
bucketing_key, condition_matchers)
11199

112100
return evaluation
113101

114-
def evaluate_features(self, feature_flag_names, matching_key, bucketing_key, attributes=None):
102+
def evaluate_features(self, feature_flags, matching_key, bucketing_key, condition_matchers):
115103
"""
116104
Evaluate the user submitted data against multiple features and return the resulting
117105
treatment.
118106
119-
:param feature_flag_names: The feature flags for which to get the treatments
120-
:type feature: list(str)
107+
:param feature_flags: array of Split objects
108+
:type feature_flags: [splitio.models.splits.Split|None]
121109
122110
:param matching_key: The matching_key for which to get the treatment
123111
:type matching_key: str
124112
125113
:param bucketing_key: The bucketing_key for which to get the treatment
126114
:type bucketing_key: str
127115
128-
:param attributes: An optional dictionary of attributes
129-
:type attributes: dict
116+
:param condition_matchers: array of condition matchers for passed feature_flag
117+
:type bucketing_key: Dict
130118
131119
:return: The treatments for the key and feature flags
132120
:rtype: object
133121
"""
134122
return {
135-
feature_flag_name: self._evaluate_treatment(feature_flag_name, matching_key,
136-
bucketing_key, attributes, feature_flag)
137-
for (feature_flag_name, feature_flag) in self._feature_flag_storage.fetch_many(feature_flag_names).items()
123+
feature_flag.name: self._evaluate_treatment(feature_flag, matching_key,
124+
bucketing_key, condition_matchers)
125+
for (feature_flag) in feature_flags
138126
}
139127

140-
def _get_treatment_for_split(self, feature_flag, matching_key, bucketing_key, attributes=None):
128+
def _get_treatment_for_feature_flag(self, feature_flag, matching_key, bucketing_key, condition_matchers):
141129
"""
142130
Evaluate the feature considering the conditions.
143131
@@ -153,43 +141,17 @@ def _get_treatment_for_split(self, feature_flag, matching_key, bucketing_key, at
153141
:param bucketing_key: The key for which to get the treatment
154142
:type key: str
155143
156-
:param attributes: An optional dictionary of attributes
157-
:type attributes: dict
144+
:param condition_matchers: array of condition matchers for passed feature_flag
145+
:type bucketing_key: Dict
158146
159147
:return: The resulting treatment and label
160148
:rtype: tuple
161149
"""
162150
if bucketing_key is None:
163151
bucketing_key = matching_key
164152

165-
roll_out = False
166-
167-
context = {
168-
'segment_storage': self._segment_storage,
169-
'evaluator': self,
170-
'bucketing_key': bucketing_key
171-
}
172-
173-
for condition in feature_flag.conditions:
174-
if (not roll_out and
175-
condition.condition_type == ConditionType.ROLLOUT):
176-
if feature_flag.traffic_allocation < 100:
177-
bucket = self._splitter.get_bucket(
178-
bucketing_key,
179-
feature_flag.traffic_allocation_seed,
180-
feature_flag.algo
181-
)
182-
if bucket > feature_flag.traffic_allocation:
183-
return feature_flag.default_treatment, Label.NOT_IN_SPLIT
184-
roll_out = True
185-
186-
condition_matches = condition.matches(
187-
matching_key,
188-
attributes=attributes,
189-
context=context
190-
)
191-
192-
if condition_matches:
153+
for condition_matcher, condition in condition_matchers:
154+
if condition_matcher:
193155
return self._splitter.get_treatment(
194156
bucketing_key,
195157
feature_flag.seed,

tests/engine/test_evaluator.py

Lines changed: 27 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,18 @@
55
from splitio.models.grammar.condition import Condition, ConditionType
66
from splitio.models.impressions import Label
77
from splitio.engine import evaluator, splitters
8-
from splitio.storage import SplitStorage, SegmentStorage
9-
108

119
class EvaluatorTests(object):
1210
"""Test evaluator behavior."""
1311

1412
def _build_evaluator_with_mocks(self, mocker):
1513
"""Build an evaluator with mocked dependencies."""
16-
split_storage_mock = mocker.Mock(spec=SplitStorage)
1714
splitter_mock = mocker.Mock(spec=splitters.Splitter)
18-
segment_storage_mock = mocker.Mock(spec=SegmentStorage)
1915
logger_mock = mocker.Mock(spec=logging.Logger)
20-
e = evaluator.Evaluator(split_storage_mock, segment_storage_mock, splitter_mock)
16+
e = evaluator.Evaluator(splitter_mock)
2117
evaluator._LOGGER = logger_mock
2218
return e
2319

24-
def test_evaluate_treatment_missing_split(self, mocker):
25-
"""Test that a missing split logs and returns CONTROL."""
26-
e = self._build_evaluator_with_mocks(mocker)
27-
e._feature_flag_storage.get.return_value = None
28-
result = e.evaluate_feature('feature1', 'some_key', 'some_bucketing_key', {'attr1': 1})
29-
assert result['configurations'] == None
30-
assert result['treatment'] == evaluator.CONTROL
31-
assert result['impression']['change_number'] == -1
32-
assert result['impression']['label'] == Label.SPLIT_NOT_FOUND
33-
3420
def test_evaluate_treatment_killed_split(self, mocker):
3521
"""Test that a killed split returns the default treatment."""
3622
e = self._build_evaluator_with_mocks(mocker)
@@ -39,8 +25,7 @@ def test_evaluate_treatment_killed_split(self, mocker):
3925
mocked_split.killed = True
4026
mocked_split.change_number = 123
4127
mocked_split.get_configurations_for.return_value = '{"some_property": 123}'
42-
e._feature_flag_storage.get.return_value = mocked_split
43-
result = e.evaluate_feature('feature1', 'some_key', 'some_bucketing_key', {'attr1': 1})
28+
result = e.evaluate_feature(mocked_split, 'some_key', 'some_bucketing_key', mocker.Mock())
4429
assert result['treatment'] == 'off'
4530
assert result['configurations'] == '{"some_property": 123}'
4631
assert result['impression']['change_number'] == 123
@@ -50,15 +35,14 @@ def test_evaluate_treatment_killed_split(self, mocker):
5035
def test_evaluate_treatment_ok(self, mocker):
5136
"""Test that a non-killed split returns the appropriate treatment."""
5237
e = self._build_evaluator_with_mocks(mocker)
53-
e._get_treatment_for_split = mocker.Mock()
54-
e._get_treatment_for_split.return_value = ('on', 'some_label')
38+
e._get_treatment_for_feature_flag = mocker.Mock()
39+
e._get_treatment_for_feature_flag.return_value = ('on', 'some_label')
5540
mocked_split = mocker.Mock(spec=Split)
5641
mocked_split.default_treatment = 'off'
5742
mocked_split.killed = False
5843
mocked_split.change_number = 123
5944
mocked_split.get_configurations_for.return_value = '{"some_property": 123}'
60-
e._feature_flag_storage.get.return_value = mocked_split
61-
result = e.evaluate_feature('feature1', 'some_key', 'some_bucketing_key', {'attr1': 1})
45+
result = e.evaluate_feature(mocked_split, 'some_key', 'some_bucketing_key', mocker.Mock())
6246
assert result['treatment'] == 'on'
6347
assert result['configurations'] == '{"some_property": 123}'
6448
assert result['impression']['change_number'] == 123
@@ -69,15 +53,14 @@ def test_evaluate_treatment_ok(self, mocker):
6953
def test_evaluate_treatment_ok_no_config(self, mocker):
7054
"""Test that a killed split returns the default treatment."""
7155
e = self._build_evaluator_with_mocks(mocker)
72-
e._get_treatment_for_split = mocker.Mock()
73-
e._get_treatment_for_split.return_value = ('on', 'some_label')
56+
e._get_treatment_for_feature_flag = mocker.Mock()
57+
e._get_treatment_for_feature_flag.return_value = ('on', 'some_label')
7458
mocked_split = mocker.Mock(spec=Split)
7559
mocked_split.default_treatment = 'off'
7660
mocked_split.killed = False
7761
mocked_split.change_number = 123
7862
mocked_split.get_configurations_for.return_value = None
79-
e._feature_flag_storage.get.return_value = mocked_split
80-
result = e.evaluate_feature('feature1', 'some_key', 'some_bucketing_key', {'attr1': 1})
63+
result = e.evaluate_feature(mocked_split, 'some_key', 'some_bucketing_key', mocker.Mock())
8164
assert result['treatment'] == 'on'
8265
assert result['configurations'] == None
8366
assert result['impression']['change_number'] == 123
@@ -87,24 +70,28 @@ def test_evaluate_treatment_ok_no_config(self, mocker):
8770
def test_evaluate_treatments(self, mocker):
8871
"""Test that a missing split logs and returns CONTROL."""
8972
e = self._build_evaluator_with_mocks(mocker)
90-
e._get_treatment_for_split = mocker.Mock()
91-
e._get_treatment_for_split.return_value = ('on', 'some_label')
73+
e._get_treatment_for_feature_flag = mocker.Mock()
74+
e._get_treatment_for_feature_flag.return_value = ('on', 'some_label')
9275
mocked_split = mocker.Mock(spec=Split)
9376
mocked_split.name = 'feature2'
9477
mocked_split.default_treatment = 'off'
9578
mocked_split.killed = False
9679
mocked_split.change_number = 123
9780
mocked_split.get_configurations_for.return_value = '{"some_property": 123}'
98-
e._feature_flag_storage.fetch_many.return_value = {
99-
'feature1': None,
100-
'feature2': mocked_split,
101-
}
102-
results = e.evaluate_features(['feature1', 'feature2'], 'some_key', 'some_bucketing_key', None)
103-
result = results['feature1']
81+
82+
mocked_split2 = mocker.Mock(spec=Split)
83+
mocked_split2.name = 'feature4'
84+
mocked_split2.default_treatment = 'on'
85+
mocked_split2.killed = False
86+
mocked_split2.change_number = 123
87+
mocked_split2.get_configurations_for.return_value = None
88+
89+
results = e.evaluate_features([mocked_split, mocked_split2], 'some_key', 'some_bucketing_key', mocker.Mock())
90+
result = results['feature4']
10491
assert result['configurations'] == None
105-
assert result['treatment'] == evaluator.CONTROL
106-
assert result['impression']['change_number'] == -1
107-
assert result['impression']['label'] == Label.SPLIT_NOT_FOUND
92+
assert result['treatment'] == 'on'
93+
assert result['impression']['change_number'] == 123
94+
assert result['impression']['label'] == 'some_label'
10895
result = results['feature2']
10996
assert result['configurations'] == '{"some_property": 123}'
11097
assert result['treatment'] == 'on'
@@ -115,12 +102,9 @@ def test_get_gtreatment_for_split_no_condition_matches(self, mocker):
115102
"""Test no condition matches."""
116103
e = self._build_evaluator_with_mocks(mocker)
117104
e._splitter.get_treatment.return_value = 'on'
118-
conditions_mock = mocker.PropertyMock()
119-
conditions_mock.return_value = []
120105
mocked_split = mocker.Mock(spec=Split)
121106
mocked_split.killed = False
122-
type(mocked_split).conditions = conditions_mock
123-
treatment, label = e._get_treatment_for_split(mocked_split, 'some_key', 'some_bucketing', {'attr1': 1})
107+
treatment, label = e._get_treatment_for_feature_flag(mocked_split, 'some_key', 'some_bucketing', [])
124108
assert treatment == None
125109
assert label == None
126110

@@ -132,30 +116,9 @@ def test_get_gtreatment_for_split_non_rollout(self, mocker):
132116
mocked_condition_1.condition_type = ConditionType.WHITELIST
133117
mocked_condition_1.label = 'some_label'
134118
mocked_condition_1.matches.return_value = True
135-
conditions_mock = mocker.PropertyMock()
136-
conditions_mock.return_value = [mocked_condition_1]
137119
mocked_split = mocker.Mock(spec=Split)
138120
mocked_split.killed = False
139-
type(mocked_split).conditions = conditions_mock
140-
treatment, label = e._get_treatment_for_split(mocked_split, 'some_key', 'some_bucketing', {'attr1': 1})
121+
condition_matchers = [(True, mocked_condition_1)]
122+
treatment, label = e._get_treatment_for_feature_flag(mocked_split, 'some_key', 'some_bucketing', condition_matchers)
141123
assert treatment == 'on'
142-
assert label == 'some_label'
143-
144-
def test_get_treatment_for_split_rollout(self, mocker):
145-
"""Test rollout condition returns default treatment."""
146-
e = self._build_evaluator_with_mocks(mocker)
147-
e._splitter.get_bucket.return_value = 60
148-
mocked_condition_1 = mocker.Mock(spec=Condition)
149-
mocked_condition_1.condition_type = ConditionType.ROLLOUT
150-
mocked_condition_1.label = 'some_label'
151-
mocked_condition_1.matches.return_value = True
152-
conditions_mock = mocker.PropertyMock()
153-
conditions_mock.return_value = [mocked_condition_1]
154-
mocked_split = mocker.Mock(spec=Split)
155-
mocked_split.traffic_allocation = 50
156-
mocked_split.default_treatment = 'almost-on'
157-
mocked_split.killed = False
158-
type(mocked_split).conditions = conditions_mock
159-
treatment, label = e._get_treatment_for_split(mocked_split, 'some_key', 'some_bucketing', {'attr1': 1})
160-
assert treatment == 'almost-on'
161-
assert label == Label.NOT_IN_SPLIT
124+
assert label == 'some_label'

0 commit comments

Comments
 (0)