Skip to content

Commit 97f7588

Browse files
authored
Merge pull request #141 from splitio/gather/input-validation-v2
Gather/input validation v2
2 parents 19f27b0 + f840f06 commit 97f7588

21 files changed

+1309
-188
lines changed

splitio/client/client.py

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,25 @@ def _send_impression_to_listener(self, impression, attributes):
8585
)
8686
self._logger.debug('Error', exc_info=True)
8787

88+
def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=None):
89+
if not self.ready:
90+
return {
91+
'treatment': CONTROL,
92+
'configurations': None,
93+
'impression': {
94+
'label': Label.NOT_READY,
95+
'change_number': None
96+
}
97+
}
98+
99+
return self._evaluator.evaluate_treatment(
100+
feature,
101+
matching_key,
102+
bucketing_key,
103+
attributes
104+
)
105+
106+
88107
def get_treatment_with_config(self, key, feature, attributes=None):
89108
"""
90109
Get the treatment and config for a feature and key, with optional dictionary of attributes.
@@ -109,19 +128,18 @@ def get_treatment_with_config(self, key, feature, attributes=None):
109128
start = int(round(time.time() * 1000))
110129

111130
matching_key, bucketing_key = input_validator.validate_key(key)
112-
feature = input_validator.validate_feature_name(feature)
131+
feature = input_validator.validate_feature_name(
132+
feature,
133+
self.ready,
134+
self._factory._get_storage('splits') #pylint: disable=protected-access
135+
)
113136

114137
if (matching_key is None and bucketing_key is None) \
115138
or feature is None \
116139
or not input_validator.validate_attributes(attributes):
117140
return CONTROL, None
118141

119-
result = self._evaluator.evaluate_treatment(
120-
feature,
121-
matching_key,
122-
bucketing_key,
123-
attributes
124-
)
142+
result = self._evaluate_if_ready(matching_key, bucketing_key, feature, attributes)
125143

126144
impression = self._build_impression(
127145
matching_key,
@@ -204,32 +222,30 @@ def get_treatments_with_config(self, key, features, attributes=None):
204222
if input_validator.validate_attributes(attributes) is False:
205223
return input_validator.generate_control_treatments(features)
206224

207-
features = input_validator.validate_features_get_treatments(features)
225+
features, missing = input_validator.validate_features_get_treatments(
226+
features,
227+
self.ready,
228+
self._factory._get_storage('splits') # pylint: disable=protected-access
229+
)
208230
if features is None:
209231
return {}
210232

211233
bulk_impressions = []
212-
treatments = {}
234+
treatments = {name: (CONTROL, None) for name in missing}
213235

214236
for feature in features:
215237
try:
216-
treatment = self._evaluator.evaluate_treatment(
217-
feature,
218-
matching_key,
219-
bucketing_key,
220-
attributes
221-
)
222-
238+
result = self._evaluate_if_ready(matching_key, bucketing_key, feature, attributes)
223239
impression = self._build_impression(matching_key,
224240
feature,
225-
treatment['treatment'],
226-
treatment['impression']['label'],
227-
treatment['impression']['change_number'],
241+
result['treatment'],
242+
result['impression']['label'],
243+
result['impression']['change_number'],
228244
bucketing_key,
229245
start)
230246

231247
bulk_impressions.append(impression)
232-
treatments[feature] = (treatment['treatment'], treatment['configurations'])
248+
treatments[feature] = (result['treatment'], result['configurations'])
233249

234250
except Exception: # pylint: disable=broad-except
235251
self._logger.error('get_treatments: An exception occured when evaluating '
@@ -338,7 +354,13 @@ def track(self, key, traffic_type, event_type, value=None, properties=None):
338354

339355
key = input_validator.validate_track_key(key)
340356
event_type = input_validator.validate_event_type(event_type)
341-
traffic_type = input_validator.validate_traffic_type(traffic_type)
357+
should_validate_existance = self.ready and self._factory._apikey != 'localhost' #pylint: disable=protected-access
358+
traffic_type = input_validator.validate_traffic_type(
359+
traffic_type,
360+
should_validate_existance,
361+
self._factory._get_storage('splits'), #pylint: disable=protected-access
362+
)
363+
342364
value = input_validator.validate_value(value)
343365
valid, properties, size = input_validator.valid_properties(properties)
344366

splitio/client/factory.py

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
import logging
66
import threading
7-
from enum import Enum
7+
from collections import Counter
88

9+
from enum import Enum
910
import six
1011

1112
from splitio.client.client import Client
@@ -45,6 +46,11 @@
4546
LocalhostSplitSynchronizationTask, LocalhostTelemetryStorage
4647

4748

49+
_LOGGER = logging.getLogger(__name__)
50+
_INSTANTIATED_FACTORIES = Counter()
51+
_INSTANTIATED_FACTORIES_LOCK = threading.RLock()
52+
53+
4854
class Status(Enum):
4955
"""Factory Status."""
5056

@@ -64,6 +70,7 @@ class SplitFactory(object): # pylint: disable=too-many-instance-attributes
6470

6571
def __init__( # pylint: disable=too-many-arguments
6672
self,
73+
apikey,
6774
storages,
6875
labels_enabled,
6976
apis=None,
@@ -87,6 +94,7 @@ def __init__( # pylint: disable=too-many-arguments
8794
:param impression_listener: User custom listener to handle impressions locally.
8895
:type impression_listener: splitio.client.listener.ImpressionListener
8996
"""
97+
self._apikey = apikey
9098
self._logger = logging.getLogger(self.__class__.__name__)
9199
self._storages = storages
92100
self._labels_enabled = labels_enabled
@@ -197,6 +205,8 @@ def _wait_for_tasks_to_stop():
197205
task.stop()
198206
finally:
199207
self._status = Status.DESTROYED
208+
with _INSTANTIATED_FACTORIES_LOCK:
209+
_INSTANTIATED_FACTORIES.subtract([self._apikey])
200210

201211
@property
202212
def destroyed(self):
@@ -324,6 +334,7 @@ def segment_ready_task():
324334
segment_completion_thread.setDaemon(True)
325335
segment_completion_thread.start()
326336
return SplitFactory(
337+
api_key,
327338
storages,
328339
cfg['labelsEnabled'],
329340
apis,
@@ -333,7 +344,7 @@ def segment_ready_task():
333344
)
334345

335346

336-
def _build_redis_factory(config):
347+
def _build_redis_factory(api_key, config):
337348
"""Build and return a split factory with redis-based storage."""
338349
cfg = DEFAULT_CONFIG.copy()
339350
cfg.update(config)
@@ -347,13 +358,14 @@ def _build_redis_factory(config):
347358
'telemetry': RedisTelemetryStorage(redis_adapter, sdk_metadata)
348359
}
349360
return SplitFactory(
361+
api_key,
350362
storages,
351363
cfg['labelsEnabled'],
352364
impression_listener=_wrap_impression_listener(cfg['impressionListener'], sdk_metadata)
353365
)
354366

355367

356-
def _build_uwsgi_factory(config):
368+
def _build_uwsgi_factory(api_key, config):
357369
"""Build and return a split factory with redis-based storage."""
358370
cfg = DEFAULT_CONFIG.copy()
359371
cfg.update(config)
@@ -367,6 +379,7 @@ def _build_uwsgi_factory(config):
367379
'telemetry': UWSGITelemetryStorage(uwsgi_adapter)
368380
}
369381
return SplitFactory(
382+
api_key,
370383
storages,
371384
cfg['labelsEnabled'],
372385
impression_listener=_wrap_impression_listener(cfg['impressionListener'], sdk_metadata)
@@ -393,25 +406,47 @@ def _build_localhost_factory(config):
393406
ready_event
394407
)}
395408
tasks['splits'].start()
396-
return SplitFactory(storages, False, None, tasks, ready_event)
409+
return SplitFactory('localhost', storages, False, None, tasks, ready_event)
397410

398411

399412
def get_factory(api_key, **kwargs):
400413
"""Build and return the appropriate factory."""
401-
config = kwargs.get('config', {})
414+
try:
415+
_INSTANTIATED_FACTORIES_LOCK.acquire()
416+
if _INSTANTIATED_FACTORIES:
417+
if api_key in _INSTANTIATED_FACTORIES:
418+
_LOGGER.warning(
419+
"factory instantiation: You already have %d %s with this API Key. "
420+
"We recommend keeping only one instance of the factory at all times "
421+
"(Singleton pattern) and reusing it throughout your application.",
422+
_INSTANTIATED_FACTORIES[api_key],
423+
'factory' if _INSTANTIATED_FACTORIES[api_key] == 1 else 'factories'
424+
)
425+
else:
426+
_LOGGER.warning(
427+
"factory instantiation: You already have an instance of the Split factory. "
428+
"Make sure you definitely want this additional instance. "
429+
"We recommend keeping only one instance of the factory at all times "
430+
"(Singleton pattern) and reusing it throughout your application."
431+
)
402432

403-
if api_key == 'localhost':
404-
return _build_localhost_factory(config)
433+
config = kwargs.get('config', {})
405434

406-
if 'redisHost' in config or 'redisSentinels' in config:
407-
return _build_redis_factory(config)
435+
if api_key == 'localhost':
436+
return _build_localhost_factory(config)
408437

409-
if 'uwsgiClient' in config:
410-
return _build_uwsgi_factory(config)
438+
if 'redisHost' in config or 'redisSentinels' in config:
439+
return _build_redis_factory(api_key, config)
411440

412-
return _build_in_memory_factory(
413-
api_key,
414-
config,
415-
kwargs.get('sdk_api_base_url'),
416-
kwargs.get('events_api_base_url')
417-
)
441+
if 'uwsgiClient' in config:
442+
return _build_uwsgi_factory(api_key, config)
443+
444+
return _build_in_memory_factory(
445+
api_key,
446+
config,
447+
kwargs.get('sdk_api_base_url'),
448+
kwargs.get('events_api_base_url')
449+
)
450+
finally:
451+
_INSTANTIATED_FACTORIES.update([api_key])
452+
_INSTANTIATED_FACTORIES_LOCK.release()

0 commit comments

Comments
 (0)