Skip to content

Commit 6a52e8b

Browse files
Add support for Redis Sentinel (#1448)
* Add support for Redis Sentinel * more unit tests
1 parent 537630b commit 6a52e8b

File tree

3 files changed

+98
-10
lines changed

3 files changed

+98
-10
lines changed

src/socketio/async_redis_manager.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
RedisError = None
1414

1515
from .async_pubsub_manager import AsyncPubSubManager
16+
from .redis_manager import parse_redis_sentinel_url
1617

1718

1819
class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
@@ -29,15 +30,18 @@ class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover
2930
client_manager=socketio.AsyncRedisManager(url))
3031
3132
:param url: The connection URL for the Redis server. For a default Redis
32-
store running on the same host, use ``redis://``. To use an
33-
SSL connection, use ``rediss://``.
33+
store running on the same host, use ``redis://``. To use a
34+
TLS connection, use ``rediss://``. To use Redis Sentinel, use
35+
``redis+sentinel://`` with a comma-separated list of hosts
36+
and the service name after the db in the URL path. Example:
37+
``redis+sentinel://user:pw@host1:1234,host2:2345/0/myredis``.
3438
:param channel: The channel name on which the server sends and receives
3539
notifications. Must be the same in all the servers.
3640
:param write_only: If set to ``True``, only initialize to emit events. The
3741
default of ``False`` initializes the class for emitting
3842
and receiving.
3943
:param redis_options: additional keyword arguments to be passed to
40-
``aioredis.from_url()``.
44+
``Redis.from_url()`` or ``Sentinel()``.
4145
"""
4246
name = 'aioredis'
4347

@@ -54,8 +58,16 @@ def __init__(self, url='redis://localhost:6379/0', channel='socketio',
5458
super().__init__(channel=channel, write_only=write_only, logger=logger)
5559

5660
def _redis_connect(self):
57-
self.redis = aioredis.Redis.from_url(self.redis_url,
58-
**self.redis_options)
61+
if not self.redis_url.startswith('redis+sentinel://'):
62+
self.redis = aioredis.Redis.from_url(self.redis_url,
63+
**self.redis_options)
64+
else:
65+
sentinels, service_name, connection_kwargs = \
66+
parse_redis_sentinel_url(self.redis_url)
67+
kwargs = self.redis_options
68+
kwargs.update(connection_kwargs)
69+
sentinel = aioredis.sentinel.Sentinel(sentinels, **kwargs)
70+
self.redis = sentinel.master_for(service_name or self.channel)
5971
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
6072

6173
async def _publish(self, data):

src/socketio/redis_manager.py

+43-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import pickle
33
import time
4+
from urllib.parse import urlparse
45

56
try:
67
import redis
@@ -12,6 +13,32 @@
1213
logger = logging.getLogger('socketio')
1314

1415

16+
def parse_redis_sentinel_url(url):
17+
"""Parse a Redis Sentinel URL with the format:
18+
redis+sentinel://[:password]@host1:port1,host2:port2,.../db/service_name
19+
"""
20+
parsed_url = urlparse(url)
21+
if parsed_url.scheme != 'redis+sentinel':
22+
raise ValueError('Invalid Redis Sentinel URL')
23+
sentinels = []
24+
for host_port in parsed_url.netloc.split('@')[-1].split(','):
25+
host, port = host_port.rsplit(':', 1)
26+
sentinels.append((host, int(port)))
27+
kwargs = {}
28+
if parsed_url.username:
29+
kwargs['username'] = parsed_url.username
30+
if parsed_url.password:
31+
kwargs['password'] = parsed_url.password
32+
service_name = None
33+
if parsed_url.path:
34+
parts = parsed_url.path.split('/')
35+
if len(parts) >= 2 and parts[1] != '':
36+
kwargs['db'] = int(parts[1])
37+
if len(parts) >= 3 and parts[2] != '':
38+
service_name = parts[2]
39+
return sentinels, service_name, kwargs
40+
41+
1542
class RedisManager(PubSubManager): # pragma: no cover
1643
"""Redis based client manager.
1744
@@ -27,15 +54,18 @@ class RedisManager(PubSubManager): # pragma: no cover
2754
server = socketio.Server(client_manager=socketio.RedisManager(url))
2855
2956
:param url: The connection URL for the Redis server. For a default Redis
30-
store running on the same host, use ``redis://``. To use an
31-
SSL connection, use ``rediss://``.
57+
store running on the same host, use ``redis://``. To use a
58+
TLS connection, use ``rediss://``. To use Redis Sentinel, use
59+
``redis+sentinel://`` with a comma-separated list of hosts
60+
and the service name after the db in the URL path. Example:
61+
``redis+sentinel://user:pw@host1:1234,host2:2345/0/myredis``.
3262
:param channel: The channel name on which the server sends and receives
3363
notifications. Must be the same in all the servers.
3464
:param write_only: If set to ``True``, only initialize to emit events. The
3565
default of ``False`` initializes the class for emitting
3666
and receiving.
3767
:param redis_options: additional keyword arguments to be passed to
38-
``Redis.from_url()``.
68+
``Redis.from_url()`` or ``Sentinel()``.
3969
"""
4070
name = 'redis'
4171

@@ -66,8 +96,16 @@ def initialize(self):
6696
'with ' + self.server.async_mode)
6797

6898
def _redis_connect(self):
69-
self.redis = redis.Redis.from_url(self.redis_url,
70-
**self.redis_options)
99+
if not self.redis_url.startswith('redis+sentinel://'):
100+
self.redis = redis.Redis.from_url(self.redis_url,
101+
**self.redis_options)
102+
else:
103+
sentinels, service_name, connection_kwargs = \
104+
parse_redis_sentinel_url(self.redis_url)
105+
kwargs = self.redis_options
106+
kwargs.update(connection_kwargs)
107+
sentinel = redis.sentinel.Sentinel(sentinels, **kwargs)
108+
self.redis = sentinel.master_for(service_name or self.channel)
71109
self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True)
72110

73111
def _publish(self, data):

tests/common/test_redis_manager.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
3+
from socketio.redis_manager import parse_redis_sentinel_url
4+
5+
6+
class TestPubSubManager:
7+
def test_sentinel_url_parser(self):
8+
with pytest.raises(ValueError):
9+
parse_redis_sentinel_url('redis://localhost:6379/0')
10+
11+
assert parse_redis_sentinel_url(
12+
'redis+sentinel://localhost:6379'
13+
) == (
14+
[('localhost', 6379)],
15+
None,
16+
{}
17+
)
18+
assert parse_redis_sentinel_url(
19+
'redis+sentinel://192.168.0.1:6379,192.168.0.2:6379/'
20+
) == (
21+
[('192.168.0.1', 6379), ('192.168.0.2', 6379)],
22+
None,
23+
{}
24+
)
25+
assert parse_redis_sentinel_url(
26+
'redis+sentinel://h1:6379,h2:6379/0'
27+
) == (
28+
[('h1', 6379), ('h2', 6379)],
29+
None,
30+
{'db': 0}
31+
)
32+
assert parse_redis_sentinel_url(
33+
'redis+sentinel://user:password@h1:6379,h2:6379,h1:6380/0/myredis'
34+
) == (
35+
[('h1', 6379), ('h2', 6379), ('h1', 6380)],
36+
'myredis',
37+
{'username': 'user', 'password': 'password', 'db': 0}
38+
)

0 commit comments

Comments
 (0)