Skip to content

Commit 2dfdd2c

Browse files
committed
make remaining changes to manager & update tests
1 parent 5c8350e commit 2dfdd2c

File tree

8 files changed

+124
-10
lines changed

8 files changed

+124
-10
lines changed

splitio/client/input_validator.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def validate_value(value):
365365
return value
366366

367367

368-
def validate_manager_feature_name(feature_name):
368+
def validate_manager_feature_name(feature_name, should_validate_existance, split_storage):
369369
"""
370370
Check if feature_name is valid for track.
371371
@@ -378,6 +378,15 @@ def validate_manager_feature_name(feature_name):
378378
(not _check_is_string(feature_name, 'feature_name', 'split')) or \
379379
(not _check_string_not_empty(feature_name, 'feature_name', 'split')):
380380
return None
381+
382+
if should_validate_existance and split_storage.get(feature_name) is None:
383+
_LOGGER.error(
384+
"split: you passed \"%s\" that does not exist in this environment, "
385+
"please double check what Splits exist in the web console.",
386+
feature_name
387+
)
388+
return None
389+
381390
return feature_name
382391

383392

splitio/client/manager.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def __init__(self, factory):
1818
"""
1919
self._logger = logging.getLogger(self.__class__.__name__)
2020
self._factory = factory
21-
self._storage = factory._get_storage('splits')
21+
self._storage = factory._get_storage('splits') #pylint: disable=protected-access
2222

2323
def split_names(self):
2424
"""
@@ -60,7 +60,12 @@ def split(self, feature_name):
6060
self._logger.error("Client has already been destroyed - no calls possible.")
6161
return []
6262

63-
feature_name = input_validator.validate_manager_feature_name(feature_name)
63+
feature_name = input_validator.validate_manager_feature_name(
64+
feature_name,
65+
self._factory.ready,
66+
self._storage
67+
)
68+
6469
if feature_name is None:
6570
return None
6671

splitio/storage/adapters/cache_trait.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ def _decorator(user_function):
180180
:rtype: callable
181181
"""
182182
_cache = LocalMemoryCache(key_func, user_function, max_age_seconds, max_size)
183-
wrapper = _cache.get
183+
# The lambda below IS necessary, otherwise update_wrapper fails since the function
184+
# is an instance method and has no reference to the __module__ namespace.
185+
wrapper = lambda *args, **kwargs: _cache.get(*args, **kwargs) #pylint: disable=unnecessary-lambda
184186
return update_wrapper(wrapper, user_function)
185187

186188
return _decorator

splitio/storage/redis.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from splitio.models import splits, segments
1010
from splitio.storage import SplitStorage, SegmentStorage, ImpressionStorage, EventStorage
1111
from splitio.storage.adapters.redis import RedisAdapterException
12-
12+
from splitio.storage.adapters.cache_trait import decorate as add_cache, DEFAULT_MAX_AGE
1313

1414
class RedisSplitStorage(SplitStorage):
1515
"""Redis-based storage for splits."""
@@ -18,7 +18,7 @@ class RedisSplitStorage(SplitStorage):
1818
_SPLIT_TILL_KEY = 'SPLITIO.splits.till'
1919
_TRAFFIC_TYPE_KEY = 'SPLITIO.trafficType.{traffic_type_name}'
2020

21-
def __init__(self, redis_client):
21+
def __init__(self, redis_client, enable_caching=False, max_age=DEFAULT_MAX_AGE):
2222
"""
2323
Class constructor.
2424
@@ -27,6 +27,9 @@ def __init__(self, redis_client):
2727
"""
2828
self._logger = logging.getLogger(self.__class__.__name__)
2929
self._redis = redis_client
30+
if enable_caching:
31+
self.get = add_cache(lambda *p, **_: p[0], max_age)(self.get)
32+
self.is_valid_traffic_type = add_cache(lambda *p, **_: p[0], max_age)(self.is_valid_traffic_type) #pylint: disable=line-too-long
3033

3134
def _get_key(self, split_name):
3235
"""
@@ -52,7 +55,7 @@ def _get_traffic_type_key(self, traffic_type_name):
5255
"""
5356
return self._TRAFFIC_TYPE_KEY.format(traffic_type_name=traffic_type_name)
5457

55-
def get(self, split_name):
58+
def get(self, split_name): #pylint: disable=method-hidden
5659
"""
5760
Retrieve a split.
5861
@@ -70,7 +73,7 @@ def get(self, split_name):
7073
self._logger.debug('Error: ', exc_info=True)
7174
return None
7275

73-
def is_valid_traffic_type(self, traffic_type_name):
76+
def is_valid_traffic_type(self, traffic_type_name): #pylint: disable=method-hidden
7477
"""
7578
Return whether the traffic type exists in at least one split in cache.
7679

tests/client/test_input_validator.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,16 @@ def test_split_(self, mocker):
967967
assert split_mock.to_split_view.mock_calls == [mocker.call()]
968968
assert manager._logger.error.mock_calls == []
969969

970+
manager._logger.reset_mock()
971+
split_mock.reset_mock()
972+
storage_mock.get.return_value = None
973+
manager.split('nonexistant-split')
974+
assert split_mock.to_split_view.mock_calls == []
975+
assert manager._logger.error.mock_calls == [mocker.call(
976+
"split: you passed \"%s\" that does not exist in this environment, "
977+
"please double check what Splits exist in the web console.",
978+
'nonexistant-split'
979+
)]
970980

971981
class FactoryInputValidationTests(object): #pylint: disable=too-few-public-methods
972982
"""Factory instantiation input validation test cases."""

tests/integration/test_client_e2e.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,44 @@ def teardown_method(self):
531531
redis_client.delete(key)
532532

533533

534+
class RedisWithCacheIntegrationTests(RedisIntegrationTests):
535+
"""Run the same tests as RedisIntegratioTests but with LRU/Expirable cache overlay."""
536+
537+
def setup_method(self):
538+
"""Prepare storages with test data."""
539+
metadata = SdkMetadata('python-1.2.3', 'some_ip', 'some_name')
540+
redis_client = RedisAdapter(StrictRedis())
541+
split_storage = RedisSplitStorage(redis_client, True)
542+
segment_storage = RedisSegmentStorage(redis_client)
543+
544+
split_fn = os.path.join(os.path.dirname(__file__), 'files', 'splitChanges.json')
545+
with open(split_fn, 'r') as flo:
546+
data = json.loads(flo.read())
547+
for split in data['splits']:
548+
redis_client.set(split_storage._get_key(split['name']), json.dumps(split))
549+
redis_client.set(split_storage._SPLIT_TILL_KEY, data['till'])
550+
551+
segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentEmployeesChanges.json')
552+
with open(segment_fn, 'r') as flo:
553+
data = json.loads(flo.read())
554+
redis_client.sadd(segment_storage._get_key(data['name']), *data['added'])
555+
redis_client.set(segment_storage._get_till_key(data['name']), data['till'])
556+
557+
segment_fn = os.path.join(os.path.dirname(__file__), 'files', 'segmentHumanBeignsChanges.json')
558+
with open(segment_fn, 'r') as flo:
559+
data = json.loads(flo.read())
560+
redis_client.sadd(segment_storage._get_key(data['name']), *data['added'])
561+
redis_client.set(segment_storage._get_till_key(data['name']), data['till'])
562+
563+
self.factory = SplitFactory('some_api_key', { #pylint:disable=attribute-defined-outside-init
564+
'splits': split_storage,
565+
'segments': segment_storage,
566+
'impressions': RedisImpressionsStorage(redis_client, metadata),
567+
'events': RedisEventsStorage(redis_client, metadata),
568+
'telemetry': RedisTelemetryStorage(redis_client, metadata)
569+
}, True)
570+
571+
534572
class LocalhostIntegrationTests(object): #pylint: disable=too-few-public-methods
535573
"""Client & Manager integration tests."""
536574

tests/storage/adapters/test_cache_trait.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def test_decorate(self, mocker):
9797
user_func = mocker.Mock()
9898

9999
cache_trait.decorate(key_func)(user_func)
100-
assert update_wrapper_mock.mock_calls == [mocker.call(returned_instance_mock.get, user_func)]
100+
assert update_wrapper_mock.mock_calls == [mocker.call(mocker.ANY, user_func)]
101101
assert local_memory_cache_mock.mock_calls == [
102102
mocker.call(key_func, user_func, cache_trait.DEFAULT_MAX_AGE, cache_trait.DEFAULT_MAX_SIZE)
103103
]

tests/storage/test_redis.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#pylint: disable=no-self-use
33

44
import json
5+
import time
56

67
from splitio.client.util import get_metadata
78
from splitio.storage.redis import RedisEventsStorage, RedisImpressionsStorage, \
@@ -32,12 +33,42 @@ def test_get_split(self, mocker):
3233
adapter.reset_mock()
3334
from_raw.reset_mock()
3435
adapter.get.return_value = None
35-
3636
result = storage.get('some_split')
3737
assert result is None
3838
assert adapter.get.mock_calls == [mocker.call('SPLITIO.split.some_split')]
3939
assert not from_raw.mock_calls
4040

41+
def test_get_split_with_cache(self, mocker):
42+
"""Test retrieving a split works."""
43+
adapter = mocker.Mock(spec=RedisAdapter)
44+
adapter.get.return_value = '{"name": "some_split"}'
45+
from_raw = mocker.Mock()
46+
mocker.patch('splitio.models.splits.from_raw', new=from_raw)
47+
48+
storage = RedisSplitStorage(adapter, True, 1)
49+
storage.get('some_split')
50+
assert adapter.get.mock_calls == [mocker.call('SPLITIO.split.some_split')]
51+
assert from_raw.mock_calls == [mocker.call({"name": "some_split"})]
52+
53+
# hit the cache:
54+
storage.get('some_split')
55+
storage.get('some_split')
56+
storage.get('some_split')
57+
assert adapter.get.mock_calls == [mocker.call('SPLITIO.split.some_split')]
58+
assert from_raw.mock_calls == [mocker.call({"name": "some_split"})]
59+
60+
# Test that a missing split returns None and doesn't call from_raw
61+
adapter.reset_mock()
62+
from_raw.reset_mock()
63+
adapter.get.return_value = None
64+
65+
result = storage.get('some_split')
66+
assert result is not None
67+
time.sleep(1) # wait for expiration
68+
result = storage.get('some_split')
69+
assert result is None
70+
assert adapter.get.mock_calls == [mocker.call('SPLITIO.split.some_split')]
71+
assert not from_raw.mock_calls
4172

4273
def test_get_changenumber(self, mocker):
4374
"""Test fetching changenumber."""
@@ -100,6 +131,22 @@ def test_is_valid_traffic_type(self, mocker):
100131
adapter.get.return_value = None
101132
assert storage.is_valid_traffic_type('any') is False
102133

134+
def test_is_valid_traffic_type_with_cache(self, mocker):
135+
"""Test that traffic type validation works."""
136+
adapter = mocker.Mock(spec=RedisAdapter)
137+
storage = RedisSplitStorage(adapter, True, 1)
138+
139+
adapter.get.return_value = '1'
140+
assert storage.is_valid_traffic_type('any') is True
141+
142+
adapter.get.return_value = '0'
143+
assert storage.is_valid_traffic_type('any') is True
144+
time.sleep(1)
145+
assert storage.is_valid_traffic_type('any') is False
146+
147+
adapter.get.return_value = None
148+
time.sleep(1)
149+
assert storage.is_valid_traffic_type('any') is False
103150

104151

105152
class RedisSegmentStorageTests(object):

0 commit comments

Comments
 (0)