Skip to content

Commit 3656d87

Browse files
Added Redis sentinel support. (#250)
Co-authored-by: Chase Bennett <[email protected]>
1 parent 38ffdeb commit 3656d87

File tree

5 files changed

+706
-17
lines changed

5 files changed

+706
-17
lines changed

.github/workflows/tests.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ jobs:
1818
- 3.7
1919
- 3.8
2020
- 3.9
21-
2221
services:
2322
redis:
2423
image: redis
@@ -29,6 +28,19 @@ jobs:
2928
--health-interval 10s
3029
--health-timeout 5s
3130
--health-retries 5
31+
sentinel:
32+
image: bitnami/redis-sentinel
33+
ports:
34+
- 26379:26379
35+
options: >-
36+
--health-cmd "redis-cli -p 26379 ping"
37+
--health-interval 10s
38+
--health-timeout 5s
39+
--health-retries 5
40+
env:
41+
REDIS_MASTER_HOST: redis
42+
REDIS_MASTER_SET: sentinel
43+
REDIS_SENTINEL_QUORUM: "1"
3244

3345
steps:
3446
- uses: actions/checkout@v2

README.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ The server(s) to connect to, as either URIs, ``(host, port)`` tuples, or dicts c
4040
Defaults to ``['localhost', 6379]``. Pass multiple hosts to enable sharding,
4141
but note that changing the host list will lose some sharded data.
4242

43+
Sentinel connections require dicts conforming to `create_sentinel
44+
<https://aioredis.readthedocs.io/en/v1.3.0/sentinel.html#aioredis.sentinel.
45+
create_sentinel>` with an additional `master_name` key specifying the Sentinel
46+
master set. Plain Redis and Sentinel connections can be mixed and matched if
47+
sharding.
48+
4349
If your server is listening on a UNIX domain socket, you can also use that to connect: ``["unix:///path/to/redis.sock"]``.
4450
This should be slightly faster than a loopback TCP connection.
4551

channels_redis/core.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,15 @@ def _wrapper(self, *args, **kwargs):
4343
class ConnectionPool:
4444
"""
4545
Connection pool manager for the channel layer.
46-
4746
It manages a set of connections for the given host specification and
4847
taking into account asyncio event loops.
4948
"""
5049

5150
def __init__(self, host):
52-
self.host = host
51+
self.host = host.copy()
52+
self.master_name = self.host.pop("master_name", None)
5353
self.conn_map = {}
54+
self.sentinel_map = {}
5455
self.in_use = {}
5556

5657
def _ensure_loop(self, loop):
@@ -68,16 +69,27 @@ def _ensure_loop(self, loop):
6869

6970
return self.conn_map[loop], loop
7071

72+
async def create_conn(self, loop):
73+
# One connection per pool since we are emulating a single connection
74+
kwargs = {"minsize": 1, "maxsize": 1, **self.host}
75+
if not (sys.version_info >= (3, 8, 0) and AIOREDIS_VERSION >= (1, 3, 1)):
76+
kwargs["loop"] = loop
77+
if self.master_name is None:
78+
return await aioredis.create_redis_pool(**kwargs)
79+
else:
80+
kwargs = {"timeout": 2, **kwargs} # aioredis default is way too low
81+
sentinel = await aioredis.sentinel.create_sentinel(**kwargs)
82+
conn = sentinel.master_for(self.master_name)
83+
self.sentinel_map[conn] = sentinel
84+
return conn
85+
7186
async def pop(self, loop=None):
7287
"""
7388
Get a connection for the given identifier and loop.
7489
"""
7590
conns, loop = self._ensure_loop(loop)
7691
if not conns:
77-
if sys.version_info >= (3, 8, 0) and AIOREDIS_VERSION >= (1, 3, 1):
78-
conn = await aioredis.create_redis(**self.host)
79-
else:
80-
conn = await aioredis.create_redis(**self.host, loop=loop)
92+
conn = await self.create_conn(loop)
8193
conns.append(conn)
8294
conn = conns.pop()
8395
if conn.closed:
@@ -96,48 +108,58 @@ def push(self, conn):
96108
conns, _ = self._ensure_loop(loop)
97109
conns.append(conn)
98110

99-
def conn_error(self, conn):
111+
async def conn_error(self, conn):
100112
"""
101113
Handle a connection that produced an error.
102114
"""
103-
conn.close()
115+
await self._close_conn(conn)
104116
del self.in_use[conn]
105117

106118
def reset(self):
107119
"""
108120
Clear all connections from the pool.
109121
"""
110122
self.conn_map = {}
123+
self.sentinel_map = {}
111124
self.in_use = {}
112125

126+
async def _close_conn(self, conn, sentinel_map=None):
127+
if sentinel_map is None:
128+
sentinel_map = self.sentinel_map
129+
if conn in sentinel_map:
130+
sentinel_map[conn].close()
131+
await sentinel_map[conn].wait_closed()
132+
del sentinel_map[conn]
133+
conn.close()
134+
await conn.wait_closed()
135+
113136
async def close_loop(self, loop):
114137
"""
115138
Close all connections owned by the pool on the given loop.
116139
"""
117140
if loop in self.conn_map:
118141
for conn in self.conn_map[loop]:
119-
conn.close()
120-
await conn.wait_closed()
142+
await self._close_conn(conn)
121143
del self.conn_map[loop]
122144

123145
for k, v in self.in_use.items():
124146
if v is loop:
147+
await self._close_conn(k)
125148
self.in_use[k] = None
126149

127150
async def close(self):
128151
"""
129152
Close all connections owned by the pool.
130153
"""
131154
conn_map = self.conn_map
155+
sentinel_map = self.sentinel_map
132156
in_use = self.in_use
133157
self.reset()
134158
for conns in conn_map.values():
135159
for conn in conns:
136-
conn.close()
137-
await conn.wait_closed()
160+
await self._close_conn(conn, sentinel_map)
138161
for conn in in_use:
139-
conn.close()
140-
await conn.wait_closed()
162+
await self._close_conn(conn, sentinel_map)
141163

142164

143165
class ChannelLock:
@@ -262,6 +284,7 @@ def decode_hosts(self, hosts):
262284
raise ValueError(
263285
"You must pass a list of Redis hosts, even if there is only one."
264286
)
287+
265288
# Decode each hosts entry into a kwargs dict
266289
result = []
267290
for entry in hosts:
@@ -888,7 +911,7 @@ async def __aenter__(self):
888911

889912
async def __aexit__(self, exc_type, exc, tb):
890913
if exc:
891-
self.pool.conn_error(self.conn)
914+
await self.pool.conn_error(self.conn)
892915
else:
893916
self.pool.push(self.conn)
894917
self.conn = None

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
test_requires = crypto_requires + [
1313
"pytest",
14-
"pytest-asyncio",
14+
"pytest-asyncio==0.14.0",
1515
"async_generator",
1616
"async-timeout",
1717
]

0 commit comments

Comments
 (0)