Skip to content

Commit 4546046

Browse files
authored
Merge pull request #137 from splitio/feature/input-validation-v2-multiple-factories
Feature/input validation v2 multiple factories
2 parents cd2f4b2 + 18fd148 commit 4546046

File tree

3 files changed

+122
-22
lines changed

3 files changed

+122
-22
lines changed

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):
@@ -321,6 +331,7 @@ def segment_ready_task():
321331
segment_completion_thread.setDaemon(True)
322332
segment_completion_thread.start()
323333
return SplitFactory(
334+
api_key,
324335
storages,
325336
cfg['labelsEnabled'],
326337
apis,
@@ -330,7 +341,7 @@ def segment_ready_task():
330341
)
331342

332343

333-
def _build_redis_factory(config):
344+
def _build_redis_factory(api_key, config):
334345
"""Build and return a split factory with redis-based storage."""
335346
cfg = DEFAULT_CONFIG.copy()
336347
cfg.update(config)
@@ -344,13 +355,14 @@ def _build_redis_factory(config):
344355
'telemetry': RedisTelemetryStorage(redis_adapter, sdk_metadata)
345356
}
346357
return SplitFactory(
358+
api_key,
347359
storages,
348360
cfg['labelsEnabled'],
349361
impression_listener=_wrap_impression_listener(cfg['impressionListener'], sdk_metadata)
350362
)
351363

352364

353-
def _build_uwsgi_factory(config):
365+
def _build_uwsgi_factory(api_key, config):
354366
"""Build and return a split factory with redis-based storage."""
355367
cfg = DEFAULT_CONFIG.copy()
356368
cfg.update(config)
@@ -364,6 +376,7 @@ def _build_uwsgi_factory(config):
364376
'telemetry': UWSGITelemetryStorage(uwsgi_adapter)
365377
}
366378
return SplitFactory(
379+
api_key,
367380
storages,
368381
cfg['labelsEnabled'],
369382
impression_listener=_wrap_impression_listener(cfg['impressionListener'], sdk_metadata)
@@ -390,25 +403,47 @@ def _build_localhost_factory(config):
390403
ready_event
391404
)}
392405
tasks['splits'].start()
393-
return SplitFactory(storages, False, None, tasks, ready_event)
406+
return SplitFactory('localhost', storages, False, None, tasks, ready_event)
394407

395408

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

400-
if api_key == 'localhost':
401-
return _build_localhost_factory(config)
430+
config = kwargs.get('config', {})
402431

403-
if 'redisHost' in config or 'redisSentinels' in config:
404-
return _build_redis_factory(config)
432+
if api_key == 'localhost':
433+
return _build_localhost_factory(config)
405434

406-
if 'uwsgiClient' in config:
407-
return _build_uwsgi_factory(config)
435+
if 'redisHost' in config or 'redisSentinels' in config:
436+
return _build_redis_factory(api_key, config)
408437

409-
return _build_in_memory_factory(
410-
api_key,
411-
config,
412-
kwargs.get('sdk_api_base_url'),
413-
kwargs.get('events_api_base_url')
414-
)
438+
if 'uwsgiClient' in config:
439+
return _build_uwsgi_factory(api_key, config)
440+
441+
return _build_in_memory_factory(
442+
api_key,
443+
config,
444+
kwargs.get('sdk_api_base_url'),
445+
kwargs.get('events_api_base_url')
446+
)
447+
finally:
448+
_INSTANTIATED_FACTORIES.update([api_key])
449+
_INSTANTIATED_FACTORIES_LOCK.release()

tests/client/test_factory.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Split factory test module."""
2-
#pylint: disable=no-self-use,protected-access
2+
#pylint: disable=no-self-use,protected-access,line-too-long,too-many-statements
3+
#pylint: disable=too-many-locals, too-many-arguments
34

45
import time
56
import threading
67
from splitio.client.listener import ImpressionListenerWrapper
7-
from splitio.client.factory import get_factory
8+
from splitio.client.factory import get_factory, SplitFactory, _INSTANTIATED_FACTORIES
89
from splitio.client.config import DEFAULT_CONFIG
910
from splitio.storage import redis, inmemmory, uwsgi
1011
from splitio.tasks import events_sync, impressions_sync, split_sync, segment_sync, telemetry_sync
@@ -338,3 +339,67 @@ def _telemetry_task_init_mock(self, api, storage, refresh_rate):
338339

339340
assert event.is_set()
340341
assert factory.destroyed
342+
343+
def test_multiple_factories(self, mocker):
344+
"""Test multiple factories instantiation and tracking."""
345+
def _make_factory_with_apikey(apikey, *_, **__):
346+
return SplitFactory(apikey, {}, True)
347+
348+
factory_module_logger = mocker.Mock()
349+
build_in_memory = mocker.Mock()
350+
build_in_memory.side_effect = _make_factory_with_apikey
351+
build_redis = mocker.Mock()
352+
build_redis.side_effect = _make_factory_with_apikey
353+
build_uwsgi = mocker.Mock()
354+
build_uwsgi.side_effect = _make_factory_with_apikey
355+
build_localhost = mocker.Mock()
356+
build_localhost.side_effect = _make_factory_with_apikey
357+
mocker.patch('splitio.client.factory._LOGGER', new=factory_module_logger)
358+
mocker.patch('splitio.client.factory._build_in_memory_factory', new=build_in_memory)
359+
mocker.patch('splitio.client.factory._build_redis_factory', new=build_redis)
360+
mocker.patch('splitio.client.factory._build_uwsgi_factory', new=build_uwsgi)
361+
mocker.patch('splitio.client.factory._build_localhost_factory', new=build_localhost)
362+
363+
_INSTANTIATED_FACTORIES.clear() # Clear all factory counters for testing purposes
364+
365+
factory1 = get_factory('some_api_key')
366+
assert _INSTANTIATED_FACTORIES['some_api_key'] == 1
367+
assert factory_module_logger.warning.mock_calls == []
368+
369+
get_factory('some_api_key')
370+
assert _INSTANTIATED_FACTORIES['some_api_key'] == 2
371+
assert factory_module_logger.warning.mock_calls == [mocker.call(
372+
"factory instantiation: You already have %d %s with this API Key. "
373+
"We recommend keeping only one instance of the factory at all times "
374+
"(Singleton pattern) and reusing it throughout your application.",
375+
1,
376+
'factory'
377+
)]
378+
379+
factory_module_logger.reset_mock()
380+
get_factory('some_api_key')
381+
assert _INSTANTIATED_FACTORIES['some_api_key'] == 3
382+
assert factory_module_logger.warning.mock_calls == [mocker.call(
383+
"factory instantiation: You already have %d %s with this API Key. "
384+
"We recommend keeping only one instance of the factory at all times "
385+
"(Singleton pattern) and reusing it throughout your application.",
386+
2,
387+
'factories'
388+
)]
389+
390+
factory_module_logger.reset_mock()
391+
get_factory('some_other_api_key')
392+
assert _INSTANTIATED_FACTORIES['some_api_key'] == 3
393+
assert _INSTANTIATED_FACTORIES['some_other_api_key'] == 1
394+
assert factory_module_logger.warning.mock_calls == [mocker.call(
395+
"factory instantiation: You already have an instance of the Split factory. "
396+
"Make sure you definitely want this additional instance. "
397+
"We recommend keeping only one instance of the factory at all times "
398+
"(Singleton pattern) and reusing it throughout your application."
399+
)]
400+
401+
event = threading.Event()
402+
factory1.destroy(event)
403+
event.wait()
404+
assert _INSTANTIATED_FACTORIES['some_other_api_key'] == 1
405+
assert _INSTANTIATED_FACTORIES['some_api_key'] == 2

tests/integration/test_client_e2e.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def setup_method(self):
3939
data = json.loads(flo.read())
4040
segment_storage.put(segments.from_raw(data))
4141

42-
self.factory = SplitFactory({ #pylint:disable=attribute-defined-outside-init
42+
self.factory = SplitFactory('some_api_key', { #pylint:disable=attribute-defined-outside-init
4343
'splits': split_storage,
4444
'segments': segment_storage,
4545
'impressions': InMemoryImpressionStorage(5000),
@@ -286,7 +286,7 @@ def setup_method(self):
286286
redis_client.sadd(segment_storage._get_key(data['name']), *data['added'])
287287
redis_client.set(segment_storage._get_till_key(data['name']), data['till'])
288288

289-
self.factory = SplitFactory({ #pylint:disable=attribute-defined-outside-init
289+
self.factory = SplitFactory('some_api_key', { #pylint:disable=attribute-defined-outside-init
290290
'splits': split_storage,
291291
'segments': segment_storage,
292292
'impressions': RedisImpressionsStorage(redis_client, metadata),
@@ -535,7 +535,7 @@ def teardown_method(self):
535535
redis_client.delete(key)
536536

537537

538-
class LocalhostIntegrationTests(object):
538+
class LocalhostIntegrationTests(object): #pylint: disable=too-few-public-methods
539539
"""Client & Manager integration tests."""
540540

541541
def test_localhost_e2e(self):

0 commit comments

Comments
 (0)