Skip to content

Commit b84d495

Browse files
authored
Merge pull request #95 from splitio/feature/RedisSentinelSupport
[SDKS-70]: FEATURE - Add support for Redis Sentinel
2 parents 99d874c + ab054a2 commit b84d495

File tree

10 files changed

+268
-39
lines changed

10 files changed

+268
-39
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var/
2323
*.egg-info/
2424
.installed.cfg
2525
*.egg
26+
.vscode
2627

2728
# PyInstaller
2829
# Usually these files are written by a python script from a template

splitio/brokers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,7 @@ def __init__(self, redis):
513513
split_fetcher = CacheBasedSplitFetcher(split_cache)
514514

515515
impressions_cache = RedisImpressionsCache(redis)
516-
delegate_treatment_log = CacheBasedTreatmentLog(impressions_cache)
517-
treatment_log = AsyncTreatmentLog(delegate_treatment_log)
516+
treatment_log = CacheBasedTreatmentLog(impressions_cache)
518517

519518
metrics_cache = RedisMetricsCache(redis)
520519
delegate_metrics = CacheBasedMetrics(metrics_cache)

splitio/factories.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def __init__(self, api_key, **kwargs):
4040
config = kwargs['config']
4141

4242
labels_enabled = config.get('labelsEnabled', True)
43-
if 'redisHost' in config:
43+
if 'redisHost' in config or 'redisSentinels' in config:
4444
broker = get_redis_broker(api_key, **kwargs)
4545
self._client = Client(broker, labels_enabled)
4646
self._manager = RedisSplitManager(broker)
@@ -91,7 +91,6 @@ def client(self): # pragma: no cover
9191
"""
9292
return self._client
9393

94-
9594
def manager(self): # pragma: no cover
9695
"""Get the split manager implementation.
9796
:return: The split manager implementation.

splitio/redis_support.py

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
try:
1212
from jsonpickle import decode, encode
1313
from redis import StrictRedis
14+
from redis.sentinel import Sentinel
1415
except ImportError:
1516
def missing_redis_dependencies(*args, **kwargs):
1617
raise NotImplementedError('Missing Redis support dependencies.')
@@ -38,6 +39,10 @@ def missing_redis_dependencies(*args, **kwargs):
3839
}
3940

4041

42+
class SentinelConfigurationException(Exception):
43+
pass
44+
45+
4146
class RedisSegmentCache(SegmentCache):
4247
'''
4348
'''
@@ -66,21 +71,25 @@ def register_segment(self, segment_name):
6671
:param segment_name: Name of the segment.
6772
:type segment_name: str
6873
'''
69-
self._redis.sadd(
70-
RedisSegmentCache._KEY_TEMPLATE.format(suffix='registered'),
71-
segment_name
72-
)
74+
# self._redis.sadd(
75+
# RedisSegmentCache._KEY_TEMPLATE.format(suffix='registered'),
76+
# segment_name
77+
# )
78+
# @TODO The Segment logic for redis should be removed.
79+
pass
7380

7481
def unregister_segment(self, segment_name):
7582
'''
7683
Unregister a segment from the automatic update process.
7784
:param segment_name: Name of the segment.
7885
:type segment_name: str
7986
'''
80-
self._redis.srem(
81-
RedisSegmentCache._KEY_TEMPLATE.format(suffix='registered'),
82-
segment_name
83-
)
87+
# self._redis.srem(
88+
# RedisSegmentCache._KEY_TEMPLATE.format(suffix='registered'),
89+
# segment_name
90+
# )
91+
# @TODO The Segment logic for redis should be removed.
92+
pass
8493

8594
def get_registered_segments(self):
8695
'''
@@ -799,8 +808,11 @@ def get_redis(config):
799808
config['redisFactory'], 'redisFactory'
800809
)
801810
return redis_factory()
802-
803-
return default_redis_factory(config)
811+
else:
812+
if 'redisSentinels' in config:
813+
return default_redis_sentinel_factory(config)
814+
else:
815+
return default_redis_factory(config)
804816

805817

806818
def default_redis_factory(config):
@@ -860,3 +872,82 @@ def default_redis_factory(config):
860872
max_connections=max_connections
861873
)
862874
return PrefixDecorator(redis, prefix=prefix)
875+
876+
877+
def default_redis_sentinel_factory(config):
878+
'''
879+
Default redis client factory for sentinel mode.
880+
:param config: A dict with the Redis configuration parameters
881+
:type config: dict
882+
:return: A Sentinel object using the provided config values
883+
:rtype: Sentinel
884+
'''
885+
sentinels = config.get('redisSentinels')
886+
887+
if (sentinels is None):
888+
raise SentinelConfigurationException('redisSentinels must be specified.')
889+
if (not isinstance(sentinels, list)):
890+
raise SentinelConfigurationException('Sentinels must be an array of elements in the form of'
891+
' [(ip, port)].')
892+
if (len(sentinels) == 0):
893+
raise SentinelConfigurationException('It must be at least one sentinel.')
894+
if not all(isinstance(s, tuple) for s in sentinels):
895+
raise SentinelConfigurationException('Sentinels must respect the tuple structure'
896+
'[(ip, port)].')
897+
898+
master_service = config.get('redisMasterService')
899+
900+
if (master_service is None):
901+
raise SentinelConfigurationException('redisMasterService must be specified.')
902+
903+
db = config.get('redisDb', 0)
904+
password = config.get('redisPassword', None)
905+
socket_timeout = config.get('redisSocketTimeout', None)
906+
socket_connect_timeout = config.get('redisSocketConnectTimeout', None)
907+
socket_keepalive = config.get('redisSocketKeepalive', None)
908+
socket_keepalive_options = config.get('redisSocketKeepaliveOptions', None)
909+
connection_pool = config.get('redisConnectionPool', None)
910+
unix_socket_path = config.get('redisUnixSocketPath', None)
911+
encoding = config.get('redisEncoding', 'utf-8')
912+
encoding_errors = config.get('redisEncodingErrors', 'strict')
913+
charset = config.get('redisCharset', None)
914+
errors = config.get('redisErrors', None)
915+
decode_responses = config.get('redisDecodeResponses', False)
916+
retry_on_timeout = config.get('redisRetryOnTimeout', False)
917+
ssl = config.get('redisSsl', False)
918+
ssl_keyfile = config.get('redisSslKeyfile', None)
919+
ssl_certfile = config.get('redisSslCertfile', None)
920+
ssl_cert_reqs = config.get('redisSslCertReqs', None)
921+
ssl_ca_certs = config.get('redisSslCaCerts', None)
922+
max_connections = config.get('redisMaxConnections', None)
923+
prefix = config.get('redisPrefix')
924+
925+
sentinel = Sentinel(
926+
sentinels,
927+
0,
928+
{
929+
'db': db,
930+
'password': password,
931+
'socket_timeout': socket_timeout,
932+
'socket_connect_timeout': socket_connect_timeout,
933+
'socket_keepalive': socket_keepalive,
934+
'socket_keepalive_options': socket_keepalive_options,
935+
'connection_pool': connection_pool,
936+
'unix_socket_path': unix_socket_path,
937+
'encoding': encoding,
938+
'encoding_errors': encoding_errors,
939+
'charset': charset,
940+
'errors': errors,
941+
'decode_responses': decode_responses,
942+
'retry_on_timeout': retry_on_timeout,
943+
'ssl': ssl,
944+
'ssl_keyfile': ssl_keyfile,
945+
'ssl_certfile': ssl_certfile,
946+
'ssl_cert_reqs': ssl_cert_reqs,
947+
'ssl_ca_certs': ssl_ca_certs,
948+
'max_connections': max_connections
949+
}
950+
)
951+
952+
redis = sentinel.master_for(master_service)
953+
return PrefixDecorator(redis, prefix=prefix)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"splits": [
3+
{
4+
"orgId": null,
5+
"environment": null,
6+
"trafficTypeId": null,
7+
"trafficTypeName": null,
8+
"name": "test_read_only_1",
9+
"seed": -1329591480,
10+
"status": "ACTIVE",
11+
"killed": false,
12+
"defaultTreatment": "off",
13+
"changeNumber": 1325599980,
14+
"conditions": [
15+
{
16+
"matcherGroup": {
17+
"combiner": "AND",
18+
"matchers": [
19+
{
20+
"keySelector": null,
21+
"matcherType": "WHITELIST",
22+
"negate": false,
23+
"userDefinedSegmentMatcherData": null,
24+
"whitelistMatcherData": {
25+
"whitelist": [
26+
"valid"
27+
]
28+
},
29+
"unaryNumericMatcherData": null,
30+
"betweenMatcherData": null
31+
}
32+
]
33+
},
34+
"partitions": [
35+
{
36+
"treatment": "on",
37+
"size": 100
38+
}
39+
]
40+
}
41+
]
42+
}
43+
],
44+
"since": -1,
45+
"till": 1461957424937
46+
}

splitio/tests/test_clients.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,6 +1165,7 @@ def setUp(self):
11651165
'splitio.tests.test_clients.LocalhostBroker._build_split_fetcher')
11661166

11671167
self.open_mock = self.patch_builtin('open')
1168+
self.threading_mock = self.patch('threading.Thread')
11681169
self.broker = LocalhostBroker()
11691170

11701171
def test_skips_comment_lines(self):

splitio/tests/test_factories.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import absolute_import, division, print_function, unicode_literals
2+
3+
try:
4+
from unittest import mock
5+
except ImportError:
6+
# Python 2
7+
import mock
8+
9+
from unittest import TestCase
10+
11+
from splitio import get_factory
12+
from splitio.redis_support import SentinelConfigurationException
13+
14+
15+
class RedisSentinelFactory(TestCase):
16+
def test_redis_factory_with_empty_sentinels_array(self):
17+
config = {
18+
'redisDb': 0,
19+
'redisPrefix': 'test',
20+
'redisSentinels': [],
21+
'redisMasterService': 'mymaster',
22+
'redisSocketTimeout': 3
23+
}
24+
25+
with self.assertRaises(SentinelConfigurationException):
26+
get_factory('abc', config=config)
27+
28+
def test_redis_factory_with_wrong_type_in_sentinels_array(self):
29+
config = {
30+
'redisDb': 0,
31+
'redisPrefix': 'test',
32+
'redisSentinels': 'abc',
33+
'redisMasterService': 'mymaster',
34+
'redisSocketTimeout': 3
35+
}
36+
37+
with self.assertRaises(SentinelConfigurationException):
38+
get_factory('abc', config=config)
39+
40+
def test_redis_factory_with_wrong_data_in_sentinels_array(self):
41+
config = {
42+
'redisDb': 0,
43+
'redisPrefix': 'test',
44+
'redisSentinels': ['asdasd'],
45+
'redisMasterService': 'mymaster',
46+
'redisSocketTimeout': 3
47+
}
48+
49+
with self.assertRaises(SentinelConfigurationException):
50+
get_factory('abc', config=config)
51+
52+
def test_redis_factory_with_without_master_service(self):
53+
config = {
54+
'redisDb': 0,
55+
'redisPrefix': 'test',
56+
'redisSentinels': [('test', 1234)],
57+
'redisSocketTimeout': 3
58+
}
59+
60+
with self.assertRaises(SentinelConfigurationException):
61+
get_factory('abc', config=config)

splitio/tests/test_redis_cache.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88

99
from os.path import dirname, join
1010
from unittest import TestCase
11+
from splitio.tests.utils import MockUtilsMixin
1112
from json import load
1213

1314
from splitio.redis_support import (RedisSplitCache, RedisSegmentCache, get_redis)
15+
from redis import StrictRedis
16+
from splitio.clients import Client
17+
from splitio.prefix_decorator import PrefixDecorator
18+
from splitio.brokers import RedisBroker
1419

1520
class CacheInterfacesTests(TestCase):
1621
def setUp(self):
@@ -42,20 +47,58 @@ def test_split_cache_interface(self):
4247
self._redis_split_cache.set_change_number(1212)
4348
self.assertEqual(1212, self._redis_split_cache.get_change_number())
4449

50+
# @TODO This tests should be removed regarding that this is not supported by redis now.
51+
# def testSegmentCacheInterface(self):
52+
# with open(self._segment_changes_file_name) as f:
53+
# self._json = load(f)
54+
# segment_name = self._json['name']
55+
# segment_change_number = self._json['till']
56+
# segment_keys = self._json['added']
4557

58+
# self._redis_segment_cache.set_change_number(segment_name, segment_change_number)
59+
# self.assertEqual(segment_change_number, self._redis_segment_cache.get_change_number(segment_name))
4660

47-
def testSegmentCacheInterface(self):
48-
with open(self._segment_changes_file_name) as f:
61+
# self._redis_segment_cache.add_keys_to_segment(segment_name, segment_keys)
62+
# self.assertTrue(self._redis_segment_cache.is_in_segment(segment_name, segment_keys[0]))
63+
64+
# self._redis_segment_cache.remove_keys_from_segment(segment_name, [segment_keys[0]])
65+
# self.assertFalse(self._redis_segment_cache.is_in_segment(segment_name, segment_keys[0]))
66+
67+
class ReadOnlyRedisMock(PrefixDecorator):
68+
69+
def __init__(self, *args, **kwargs):
70+
"""
71+
Bases on PrefixDecorator.
72+
"""
73+
PrefixDecorator.__init__(self, *args, **kwargs)
74+
75+
def sadd(self, name, *values):
76+
"""
77+
Decorated sadd to simulate read only exception error.
78+
"""
79+
raise Exception('ReadOnlyError')
80+
81+
class RedisReadOnlyTest(TestCase, MockUtilsMixin):
82+
def setUp(self):
83+
self._split_changes_file_name = join(dirname(__file__), 'splitChangesReadOnly.json')
84+
85+
with open(self._split_changes_file_name) as f:
4986
self._json = load(f)
50-
segment_name = self._json['name']
51-
segment_change_number = self._json['till']
52-
segment_keys = self._json['added']
87+
split_definition = self._json['splits'][0]
88+
split_name = split_definition['name']
89+
90+
self._redis = get_redis({'redisPrefix': 'test'})
5391

54-
self._redis_segment_cache.set_change_number(segment_name, segment_change_number)
55-
self.assertEqual(segment_change_number, self._redis_segment_cache.get_change_number(segment_name))
92+
self._mocked_redis = ReadOnlyRedisMock(self._redis)
93+
self._redis_split_cache = RedisSplitCache(self._redis)
94+
self._redis_split_cache.add_split(split_name, split_definition)
95+
self._client = Client(RedisBroker(self._mocked_redis))
5696

57-
self._redis_segment_cache.add_keys_to_segment(segment_name, segment_keys)
58-
self.assertTrue(self._redis_segment_cache.is_in_segment(segment_name, segment_keys[0]))
97+
self._impression = mock.MagicMock()
98+
self._start = mock.MagicMock()
99+
self._operation = mock.MagicMock()
59100

60-
self._redis_segment_cache.remove_keys_from_segment(segment_name, [segment_keys[0]])
61-
self.assertFalse(self._redis_segment_cache.is_in_segment(segment_name, segment_keys[0]))
101+
def test_redis_read_only_mode(self):
102+
self.assertEqual(self._client.get_treatment('valid', 'test_read_only_1'), 'on')
103+
self.assertEqual(self._client.get_treatment('invalid', 'test_read_only_1'), 'off')
104+
self.assertEqual(self._client.get_treatment('valid', 'test_read_only_1_invalid'), 'control')

0 commit comments

Comments
 (0)