Skip to content

Commit 28f25ae

Browse files
authored
Merge pull request #97 from splitio/development
Development
2 parents 2cc2de1 + f5941c6 commit 28f25ae

14 files changed

+551
-240
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

CHANGES.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
6.0.0 (Aug 29, 2018)
2+
- Add support for redis sentinel
3+
- UWSGI performance boost (breaking change)
4+
- Add support for more c++ compilers in windows for mumur hash extension
5+
5.5.0 (Feb 28, 2018)
6+
- Add support for .track
17
5.4.3 (Jan 7, 2018)
28
- Move impressions listener to it's own thread
39
- Fix bug in localhost client

splitio/brokers.py

Lines changed: 2 additions & 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)
@@ -591,6 +590,7 @@ def __init__(self, uwsgi, config=None):
591590
self._treatment_log = treatment_log
592591
self._metrics = metrics
593592

593+
594594
def get_split_fetcher(self):
595595
"""
596596
Get the split fetcher implementation for the broker.

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)

splitio/tasks.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,30 +80,31 @@ def update_splits(split_cache, split_change_fetcher, split_parser):
8080
If an exception is raised, the process is stopped and it won't try to
8181
update splits again until enabled_updates is called on the splits cache.
8282
"""
83+
added_features = []
84+
removed_features = []
8385
try:
8486
till = split_cache.get_change_number()
8587

8688
while True:
8789
response = split_change_fetcher.fetch(till)
8890

8991
if 'till' not in response:
90-
return
92+
break
9193

9294
if till >= response['till']:
9395
_logger.debug("change_number is greater or equal than 'till'")
94-
return
96+
break
9597

9698
if 'splits' in response and len(response['splits']) > 0:
9799
_logger.debug(
98100
"Splits field in response. response = %s",
99101
response
100102
)
101-
added_features = []
102-
removed_features = []
103103

104104
for split_change in response['splits']:
105105
if Status(split_change['status']) != Status.ACTIVE:
106106
split_cache.remove_split(split_change['name'])
107+
removed_features.append(split_change['name'])
107108
continue
108109

109110
parsed_split = split_parser.parse(split_change)
@@ -129,6 +130,9 @@ def update_splits(split_cache, split_change_fetcher, split_parser):
129130
except:
130131
_logger.exception('Exception caught updating split definitions')
131132
split_cache.disable()
133+
return [], []
134+
135+
return added_features, removed_features
132136

133137

134138
def report_impressions(impressions_cache, sdk_api, listener=None):
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)

0 commit comments

Comments
 (0)