Skip to content

Commit 53f127d

Browse files
authored
Merge pull request #17 from hectcastro/feature/hmc/graceful-degradation
Add support for when cluster endpoint is not available
2 parents 2084c43 + 384dd31 commit 53f127d

File tree

5 files changed

+88
-16
lines changed

5 files changed

+88
-16
lines changed

README.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ Your cache backend should look something like this::
3636
'default': {
3737
'BACKEND': 'django_elasticache.memcached.ElastiCache',
3838
'LOCATION': 'cache-c.draaaf.cfg.use1.cache.amazonaws.com:11211',
39+
'OPTIONS' {
40+
'IGNORE_CLUSTER_ERRORS': [True,False],
41+
},
3942
}
4043
}
4144

42-
By the first call to cache it connects to cluster (using LOCATION param),
45+
By the first call to cache it connects to cluster (using ``LOCATION`` param),
4346
gets list of all nodes and setup pylibmc client using full
4447
list of nodes. As result your cache will work with all nodes in cluster and
4548
automatically detect new nodes in cluster. List of nodes are stored in class-level
@@ -48,6 +51,10 @@ But if you're using gunicorn or mod_wsgi you usually have max_request settings w
4851
restart process after some count of processed requests, so auto discovery will work
4952
fine.
5053

54+
The ``IGNORE_CLUSTER_ERRORS`` option is useful when ``LOCATION`` doesn't have support
55+
for ``config get cluster``. When set to ``True``, and ``config get cluster`` fails,
56+
it returns a list of a single node with the same endpoint supplied to ``LOCATION``.
57+
5158
Django-elasticache changes default pylibmc params to increase performance.
5259

5360
Another solutions

django_elasticache/cluster_utils.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(self, cmd, response):
1717
'Unexpected response {} for command {}'.format(response, cmd))
1818

1919

20-
def get_cluster_info(host, port):
20+
def get_cluster_info(host, port, ignore_cluster_errors=False):
2121
"""
2222
return dict with info about nodes in cluster and current version
2323
{
@@ -40,8 +40,21 @@ def get_cluster_info(host, port):
4040
else:
4141
cmd = b'get AmazonElastiCache:cluster\n'
4242
client.write(cmd)
43-
res = client.read_until(b'\n\r\nEND\r\n')
43+
regex_index, match_object, res = client.expect([
44+
re.compile(b'\n\r\nEND\r\n'),
45+
re.compile(b'ERROR\r\n')
46+
])
4447
client.close()
48+
49+
if res == b'ERROR\r\n' and ignore_cluster_errors:
50+
return {
51+
'version': version,
52+
'nodes': [
53+
'{}:{}'.format(smart_text(host),
54+
smart_text(port))
55+
]
56+
}
57+
4558
ls = list(filter(None, re.compile(br'\r?\n').split(res)))
4659
if len(ls) != 4:
4760
raise WrongProtocolData(cmd, res)

django_elasticache/memcached.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ def __init__(self, server, params):
3838
raise InvalidCacheBackendError(
3939
'Server configuration should be in format IP:port')
4040

41+
self._ignore_cluster_errors = self._options.get(
42+
'IGNORE_CLUSTER_ERRORS', False)
43+
4144
def update_params(self, params):
4245
"""
4346
update connection params to maximize performance
@@ -67,7 +70,8 @@ def get_cluster_nodes(self):
6770
server, port = self._servers[0].split(':')
6871
try:
6972
self._cluster_nodes_cache = (
70-
get_cluster_info(server, port)['nodes'])
73+
get_cluster_info(server, port,
74+
self._ignore_cluster_errors)['nodes'])
7175
except (socket.gaierror, socket.timeout) as err:
7276
raise Exception('Cannot connect to cluster {} ({})'.format(
7377
self._servers[0], err

tests/test_backend.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def test_split_servers(get_cluster_info):
4747
}
4848
backend._lib.Client = Mock()
4949
assert backend._cache
50-
get_cluster_info.assert_called_once_with('h', '0')
50+
get_cluster_info.assert_called_once_with('h', '0', False)
5151
backend._lib.Client.assert_called_once_with(servers)
5252

5353

@@ -70,7 +70,7 @@ def test_node_info_cache(get_cluster_info):
7070
eq_(backend._cache.get.call_count, 2)
7171
eq_(backend._cache.set.call_count, 2)
7272

73-
get_cluster_info.assert_called_once_with('h', '0')
73+
get_cluster_info.assert_called_once_with('h', '0', False)
7474

7575

7676
@patch('django.conf.settings', global_settings)

tests/test_protocol.py

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,44 @@
88
from unittest.mock import patch, call, MagicMock
99

1010

11-
TEST_PROTOCOL_1 = [
11+
TEST_PROTOCOL_1_READ_UNTIL = [
1212
b'VERSION 1.4.14',
13-
b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n',
1413
]
1514

16-
TEST_PROTOCOL_2 = [
15+
TEST_PROTOCOL_1_EXPECT = [
16+
(0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n'), # NOQA
17+
]
18+
19+
TEST_PROTOCOL_2_READ_UNTIL = [
1720
b'VERSION 1.4.13',
18-
b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n',
1921
]
2022

21-
TEST_PROTOCOL_3 = [
23+
TEST_PROTOCOL_2_EXPECT = [
24+
(0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n'), # NOQA
25+
]
26+
27+
TEST_PROTOCOL_3_READ_UNTIL = [
2228
b'VERSION 1.4.14 (Ubuntu)',
23-
b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n',
29+
]
30+
31+
TEST_PROTOCOL_3_EXPECT = [
32+
(0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n'), # NOQA
33+
]
34+
35+
TEST_PROTOCOL_4_READ_UNTIL = [
36+
b'VERSION 1.4.34',
37+
]
38+
39+
TEST_PROTOCOL_4_EXPECT = [
40+
(0, None, b'ERROR\r\n'),
2441
]
2542

2643

2744
@patch('django_elasticache.cluster_utils.Telnet')
2845
def test_happy_path(Telnet):
2946
client = Telnet.return_value
30-
client.read_until.side_effect = TEST_PROTOCOL_1
47+
client.read_until.side_effect = TEST_PROTOCOL_1_READ_UNTIL
48+
client.expect.side_effect = TEST_PROTOCOL_1_EXPECT
3149
info = get_cluster_info('', 0)
3250
eq_(info['version'], 1)
3351
eq_(info['nodes'], ['ip:port', 'host:port'])
@@ -42,7 +60,8 @@ def test_bad_protocol():
4260
@patch('django_elasticache.cluster_utils.Telnet')
4361
def test_last_versions(Telnet):
4462
client = Telnet.return_value
45-
client.read_until.side_effect = TEST_PROTOCOL_1
63+
client.read_until.side_effect = TEST_PROTOCOL_1_READ_UNTIL
64+
client.expect.side_effect = TEST_PROTOCOL_1_EXPECT
4665
get_cluster_info('', 0)
4766
client.write.assert_has_calls([
4867
call(b'version\n'),
@@ -53,7 +72,8 @@ def test_last_versions(Telnet):
5372
@patch('django_elasticache.cluster_utils.Telnet')
5473
def test_prev_versions(Telnet):
5574
client = Telnet.return_value
56-
client.read_until.side_effect = TEST_PROTOCOL_2
75+
client.read_until.side_effect = TEST_PROTOCOL_2_READ_UNTIL
76+
client.expect.side_effect = TEST_PROTOCOL_2_EXPECT
5777
get_cluster_info('', 0)
5878
client.write.assert_has_calls([
5979
call(b'version\n'),
@@ -64,7 +84,8 @@ def test_prev_versions(Telnet):
6484
@patch('django_elasticache.cluster_utils.Telnet')
6585
def test_ubuntu_protocol(Telnet):
6686
client = Telnet.return_value
67-
client.read_until.side_effect = TEST_PROTOCOL_3
87+
client.read_until.side_effect = TEST_PROTOCOL_3_READ_UNTIL
88+
client.expect.side_effect = TEST_PROTOCOL_3_EXPECT
6889

6990
try:
7091
get_cluster_info('', 0)
@@ -75,3 +96,30 @@ def test_ubuntu_protocol(Telnet):
7596
call(b'version\n'),
7697
call(b'config get cluster\n'),
7798
])
99+
100+
101+
@patch('django_elasticache.cluster_utils.Telnet')
102+
def test_no_configuration_protocol_support_with_errors_ignored(Telnet):
103+
client = Telnet.return_value
104+
client.read_until.side_effect = TEST_PROTOCOL_4_READ_UNTIL
105+
client.expect.side_effect = TEST_PROTOCOL_4_EXPECT
106+
info = get_cluster_info('test', 0, ignore_cluster_errors=True)
107+
client.write.assert_has_calls([
108+
call(b'version\n'),
109+
call(b'config get cluster\n'),
110+
])
111+
eq_(info['version'], '1.4.34')
112+
eq_(info['nodes'], ['test:0'])
113+
114+
115+
@raises(WrongProtocolData)
116+
@patch('django_elasticache.cluster_utils.Telnet')
117+
def test_no_configuration_protocol_support_with_errors(Telnet):
118+
client = Telnet.return_value
119+
client.read_until.side_effect = TEST_PROTOCOL_4_READ_UNTIL
120+
client.expect.side_effect = TEST_PROTOCOL_4_EXPECT
121+
get_cluster_info('test', 0, ignore_cluster_errors=False)
122+
client.write.assert_has_calls([
123+
call(b'version\n'),
124+
call(b'config get cluster\n'),
125+
])

0 commit comments

Comments
 (0)