funboost 45.4__py3-none-any.whl → 45.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of funboost might be problematic. Click here for more details.
- funboost/__init__.py +1 -1
- funboost/consumers/base_consumer.py +9 -3
- funboost/consumers/local_python_queue_consumer.py +2 -2
- funboost/core/current_task.py +1 -1
- funboost/core/func_params_model.py +2 -4
- funboost/function_result_web/__pycache__/functions.cpython-39.pyc +0 -0
- funboost/publishers/base_publisher.py +3 -0
- funboost/publishers/local_python_queue_publisher.py +8 -7
- funboost/queues/memory_queues_map.py +11 -0
- funboost/utils/dependency_packages_in_pythonpath/__pycache__/__init__.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/__pycache__/add_to_pythonpath.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__init__.py +59 -59
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/__init__.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/client.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/compat.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/connection.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/exceptions.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/lock.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/utils.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/client.py +4804 -4804
- funboost/utils/dependency_packages_in_pythonpath/aioredis/compat.py +8 -8
- funboost/utils/dependency_packages_in_pythonpath/aioredis/connection.py +1668 -1668
- funboost/utils/dependency_packages_in_pythonpath/aioredis/exceptions.py +96 -96
- funboost/utils/dependency_packages_in_pythonpath/aioredis/lock.py +306 -306
- funboost/utils/dependency_packages_in_pythonpath/aioredis/log.py +15 -15
- funboost/utils/dependency_packages_in_pythonpath/aioredis/sentinel.py +329 -329
- funboost/utils/dependency_packages_in_pythonpath/aioredis/utils.py +61 -61
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__init__.py +16 -16
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/StoppableThread.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/__init__.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/dafunc.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/exceptions.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/py3_raise.cpython-39.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/exceptions.py +98 -98
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/py2_raise.py +7 -7
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/py3_raise.py +7 -7
- funboost/utils/times/__init__.py +85 -85
- funboost/utils/times/version.py +1 -1
- {funboost-45.4.dist-info → funboost-45.7.dist-info}/METADATA +20 -10
- {funboost-45.4.dist-info → funboost-45.7.dist-info}/RECORD +44 -72
- {funboost-45.4.dist-info → funboost-45.7.dist-info}/entry_points.txt +0 -1
- funboost/function_result_web/__pycache__/app.cpython-37.pyc +0 -0
- funboost/function_result_web/__pycache__/functions.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/__pycache__/__init__.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/__pycache__/__init__.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/__pycache__/add_to_pythonpath.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/__pycache__/add_to_pythonpath.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/__init__.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/__init__.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/client.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/client.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/compat.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/compat.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/connection.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/connection.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/exceptions.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/exceptions.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/lock.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/lock.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/utils.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/utils.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/StoppableThread.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/StoppableThread.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/__init__.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/__init__.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/dafunc.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/dafunc.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/exceptions.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/exceptions.cpython-37.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/py3_raise.cpython-311.pyc +0 -0
- funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/py3_raise.cpython-37.pyc +0 -0
- {funboost-45.4.dist-info → funboost-45.7.dist-info}/LICENSE +0 -0
- {funboost-45.4.dist-info → funboost-45.7.dist-info}/WHEEL +0 -0
- {funboost-45.4.dist-info → funboost-45.7.dist-info}/top_level.txt +0 -0
|
@@ -1,329 +1,329 @@
|
|
|
1
|
-
import random
|
|
2
|
-
import weakref
|
|
3
|
-
from typing import AsyncIterator, Iterable, Mapping, Sequence, Tuple, Type
|
|
4
|
-
|
|
5
|
-
from aioredis.client import Redis
|
|
6
|
-
from aioredis.connection import Connection, ConnectionPool, EncodableT, SSLConnection
|
|
7
|
-
from aioredis.exceptions import (
|
|
8
|
-
ConnectionError,
|
|
9
|
-
ReadOnlyError,
|
|
10
|
-
ResponseError,
|
|
11
|
-
TimeoutError,
|
|
12
|
-
)
|
|
13
|
-
from aioredis.utils import str_if_bytes
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class MasterNotFoundError(ConnectionError):
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class SlaveNotFoundError(ConnectionError):
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class SentinelManagedConnection(SSLConnection):
|
|
25
|
-
def __init__(self, **kwargs):
|
|
26
|
-
self.connection_pool = kwargs.pop("connection_pool")
|
|
27
|
-
if not kwargs.pop("ssl", False):
|
|
28
|
-
# use constructor from Connection class
|
|
29
|
-
super(SSLConnection, self).__init__(**kwargs)
|
|
30
|
-
else:
|
|
31
|
-
# use constructor from SSLConnection class
|
|
32
|
-
super().__init__(**kwargs)
|
|
33
|
-
|
|
34
|
-
def __repr__(self):
|
|
35
|
-
pool = self.connection_pool
|
|
36
|
-
s = f"{self.__class__.__name__}<service={pool.service_name}"
|
|
37
|
-
if self.host:
|
|
38
|
-
host_info = f",host={self.host},port={self.port}"
|
|
39
|
-
s += host_info
|
|
40
|
-
return s + ">"
|
|
41
|
-
|
|
42
|
-
async def connect_to(self, address):
|
|
43
|
-
self.host, self.port = address
|
|
44
|
-
await super().connect()
|
|
45
|
-
if self.connection_pool.check_connection:
|
|
46
|
-
await self.send_command("PING")
|
|
47
|
-
if str_if_bytes(await self.read_response()) != "PONG":
|
|
48
|
-
raise ConnectionError("PING failed")
|
|
49
|
-
|
|
50
|
-
async def connect(self):
|
|
51
|
-
if self._reader:
|
|
52
|
-
return # already connected
|
|
53
|
-
if self.connection_pool.is_master:
|
|
54
|
-
await self.connect_to(await self.connection_pool.get_master_address())
|
|
55
|
-
else:
|
|
56
|
-
async for slave in self.connection_pool.rotate_slaves():
|
|
57
|
-
try:
|
|
58
|
-
return await self.connect_to(slave)
|
|
59
|
-
except ConnectionError:
|
|
60
|
-
continue
|
|
61
|
-
raise SlaveNotFoundError # Never be here
|
|
62
|
-
|
|
63
|
-
async def read_response(self):
|
|
64
|
-
try:
|
|
65
|
-
return await super().read_response()
|
|
66
|
-
except ReadOnlyError:
|
|
67
|
-
if self.connection_pool.is_master:
|
|
68
|
-
# When talking to a master, a ReadOnlyError when likely
|
|
69
|
-
# indicates that the previous master that we're still connected
|
|
70
|
-
# to has been demoted to a slave and there's a new master.
|
|
71
|
-
# calling disconnect will force the connection to re-query
|
|
72
|
-
# sentinel during the next connect() attempt.
|
|
73
|
-
await self.disconnect()
|
|
74
|
-
raise ConnectionError("The previous master is now a slave")
|
|
75
|
-
raise
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class SentinelConnectionPool(ConnectionPool):
|
|
79
|
-
"""
|
|
80
|
-
Sentinel backed connection pool.
|
|
81
|
-
|
|
82
|
-
If ``check_connection`` flag is set to True, SentinelManagedConnection
|
|
83
|
-
sends a PING command right after establishing the connection.
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
def __init__(self, service_name, sentinel_manager, **kwargs):
|
|
87
|
-
kwargs["connection_class"] = kwargs.get(
|
|
88
|
-
"connection_class", SentinelManagedConnection
|
|
89
|
-
)
|
|
90
|
-
self.is_master = kwargs.pop("is_master", True)
|
|
91
|
-
self.check_connection = kwargs.pop("check_connection", False)
|
|
92
|
-
super().__init__(**kwargs)
|
|
93
|
-
self.connection_kwargs["connection_pool"] = weakref.proxy(self)
|
|
94
|
-
self.service_name = service_name
|
|
95
|
-
self.sentinel_manager = sentinel_manager
|
|
96
|
-
self.master_address = None
|
|
97
|
-
self.slave_rr_counter = None
|
|
98
|
-
|
|
99
|
-
def __repr__(self):
|
|
100
|
-
return (
|
|
101
|
-
f"{self.__class__.__name__}"
|
|
102
|
-
f"<service={self.service_name}({self.is_master and 'master' or 'slave'})>"
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
def reset(self):
|
|
106
|
-
super().reset()
|
|
107
|
-
self.master_address = None
|
|
108
|
-
self.slave_rr_counter = None
|
|
109
|
-
|
|
110
|
-
def owns_connection(self, connection: Connection):
|
|
111
|
-
check = not self.is_master or (
|
|
112
|
-
self.is_master and self.master_address == (connection.host, connection.port)
|
|
113
|
-
)
|
|
114
|
-
return check and super().owns_connection(connection)
|
|
115
|
-
|
|
116
|
-
async def get_master_address(self):
|
|
117
|
-
master_address = await self.sentinel_manager.discover_master(self.service_name)
|
|
118
|
-
if self.is_master:
|
|
119
|
-
if self.master_address != master_address:
|
|
120
|
-
self.master_address = master_address
|
|
121
|
-
# disconnect any idle connections so that they reconnect
|
|
122
|
-
# to the new master the next time that they are used.
|
|
123
|
-
await self.disconnect(inuse_connections=False)
|
|
124
|
-
return master_address
|
|
125
|
-
|
|
126
|
-
async def rotate_slaves(self) -> AsyncIterator:
|
|
127
|
-
"""Round-robin slave balancer"""
|
|
128
|
-
slaves = await self.sentinel_manager.discover_slaves(self.service_name)
|
|
129
|
-
if slaves:
|
|
130
|
-
if self.slave_rr_counter is None:
|
|
131
|
-
self.slave_rr_counter = random.randint(0, len(slaves) - 1)
|
|
132
|
-
for _ in range(len(slaves)):
|
|
133
|
-
self.slave_rr_counter = (self.slave_rr_counter + 1) % len(slaves)
|
|
134
|
-
slave = slaves[self.slave_rr_counter]
|
|
135
|
-
yield slave
|
|
136
|
-
# Fallback to the master connection
|
|
137
|
-
try:
|
|
138
|
-
yield await self.get_master_address()
|
|
139
|
-
except MasterNotFoundError:
|
|
140
|
-
pass
|
|
141
|
-
raise SlaveNotFoundError(f"No slave found for {self.service_name!r}")
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
class Sentinel:
|
|
145
|
-
"""
|
|
146
|
-
Redis Sentinel cluster client
|
|
147
|
-
|
|
148
|
-
>>> from aioredis.sentinel import Sentinel
|
|
149
|
-
>>> sentinel = Sentinel([('localhost', 26379)], socket_timeout=0.1)
|
|
150
|
-
>>> master = sentinel.master_for('mymaster', socket_timeout=0.1)
|
|
151
|
-
>>> await master.set('foo', 'bar')
|
|
152
|
-
>>> slave = sentinel.slave_for('mymaster', socket_timeout=0.1)
|
|
153
|
-
>>> await slave.get('foo')
|
|
154
|
-
b'bar'
|
|
155
|
-
|
|
156
|
-
``sentinels`` is a list of sentinel nodes. Each node is represented by
|
|
157
|
-
a pair (hostname, port).
|
|
158
|
-
|
|
159
|
-
``min_other_sentinels`` defined a minimum number of peers for a sentinel.
|
|
160
|
-
When querying a sentinel, if it doesn't meet this threshold, responses
|
|
161
|
-
from that sentinel won't be considered valid.
|
|
162
|
-
|
|
163
|
-
``sentinel_kwargs`` is a dictionary of connection arguments used when
|
|
164
|
-
connecting to sentinel instances. Any argument that can be passed to
|
|
165
|
-
a normal Redis connection can be specified here. If ``sentinel_kwargs`` is
|
|
166
|
-
not specified, any socket_timeout and socket_keepalive options specified
|
|
167
|
-
in ``connection_kwargs`` will be used.
|
|
168
|
-
|
|
169
|
-
``connection_kwargs`` are keyword arguments that will be used when
|
|
170
|
-
establishing a connection to a Redis server.
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
def __init__(
|
|
174
|
-
self,
|
|
175
|
-
sentinels,
|
|
176
|
-
min_other_sentinels=0,
|
|
177
|
-
sentinel_kwargs=None,
|
|
178
|
-
**connection_kwargs,
|
|
179
|
-
):
|
|
180
|
-
# if sentinel_kwargs isn't defined, use the socket_* options from
|
|
181
|
-
# connection_kwargs
|
|
182
|
-
if sentinel_kwargs is None:
|
|
183
|
-
sentinel_kwargs = {
|
|
184
|
-
k: v for k, v in connection_kwargs.items() if k.startswith("socket_")
|
|
185
|
-
}
|
|
186
|
-
self.sentinel_kwargs = sentinel_kwargs
|
|
187
|
-
|
|
188
|
-
self.sentinels = [
|
|
189
|
-
Redis(host=hostname, port=port, **self.sentinel_kwargs)
|
|
190
|
-
for hostname, port in sentinels
|
|
191
|
-
]
|
|
192
|
-
self.min_other_sentinels = min_other_sentinels
|
|
193
|
-
self.connection_kwargs = connection_kwargs
|
|
194
|
-
|
|
195
|
-
def __repr__(self):
|
|
196
|
-
sentinel_addresses = []
|
|
197
|
-
for sentinel in self.sentinels:
|
|
198
|
-
sentinel_addresses.append(
|
|
199
|
-
f"{sentinel.connection_pool.connection_kwargs['host']}:"
|
|
200
|
-
f"{sentinel.connection_pool.connection_kwargs['port']}"
|
|
201
|
-
)
|
|
202
|
-
return f"{self.__class__.__name__}<sentinels=[{','.join(sentinel_addresses)}]>"
|
|
203
|
-
|
|
204
|
-
def check_master_state(self, state: dict, service_name: str) -> bool:
|
|
205
|
-
if not state["is_master"] or state["is_sdown"] or state["is_odown"]:
|
|
206
|
-
return False
|
|
207
|
-
# Check if our sentinel doesn't see other nodes
|
|
208
|
-
if state["num-other-sentinels"] < self.min_other_sentinels:
|
|
209
|
-
return False
|
|
210
|
-
return True
|
|
211
|
-
|
|
212
|
-
async def discover_master(self, service_name: str):
|
|
213
|
-
"""
|
|
214
|
-
Asks sentinel servers for the Redis master's address corresponding
|
|
215
|
-
to the service labeled ``service_name``.
|
|
216
|
-
|
|
217
|
-
Returns a pair (address, port) or raises MasterNotFoundError if no
|
|
218
|
-
master is found.
|
|
219
|
-
"""
|
|
220
|
-
for sentinel_no, sentinel in enumerate(self.sentinels):
|
|
221
|
-
try:
|
|
222
|
-
masters = await sentinel.sentinel_masters()
|
|
223
|
-
except (ConnectionError, TimeoutError):
|
|
224
|
-
continue
|
|
225
|
-
state = masters.get(service_name)
|
|
226
|
-
if state and self.check_master_state(state, service_name):
|
|
227
|
-
# Put this sentinel at the top of the list
|
|
228
|
-
self.sentinels[0], self.sentinels[sentinel_no] = (
|
|
229
|
-
sentinel,
|
|
230
|
-
self.sentinels[0],
|
|
231
|
-
)
|
|
232
|
-
return state["ip"], state["port"]
|
|
233
|
-
raise MasterNotFoundError(f"No master found for {service_name!r}")
|
|
234
|
-
|
|
235
|
-
def filter_slaves(
|
|
236
|
-
self, slaves: Iterable[Mapping]
|
|
237
|
-
) -> Sequence[Tuple[EncodableT, EncodableT]]:
|
|
238
|
-
"""Remove slaves that are in an ODOWN or SDOWN state"""
|
|
239
|
-
slaves_alive = []
|
|
240
|
-
for slave in slaves:
|
|
241
|
-
if slave["is_odown"] or slave["is_sdown"]:
|
|
242
|
-
continue
|
|
243
|
-
slaves_alive.append((slave["ip"], slave["port"]))
|
|
244
|
-
return slaves_alive
|
|
245
|
-
|
|
246
|
-
async def discover_slaves(
|
|
247
|
-
self, service_name: str
|
|
248
|
-
) -> Sequence[Tuple[EncodableT, EncodableT]]:
|
|
249
|
-
"""Returns a list of alive slaves for service ``service_name``"""
|
|
250
|
-
for sentinel in self.sentinels:
|
|
251
|
-
try:
|
|
252
|
-
slaves = await sentinel.sentinel_slaves(service_name)
|
|
253
|
-
except (ConnectionError, ResponseError, TimeoutError):
|
|
254
|
-
continue
|
|
255
|
-
slaves = self.filter_slaves(slaves)
|
|
256
|
-
if slaves:
|
|
257
|
-
return slaves
|
|
258
|
-
return []
|
|
259
|
-
|
|
260
|
-
def master_for(
|
|
261
|
-
self,
|
|
262
|
-
service_name: str,
|
|
263
|
-
redis_class: Type[Redis] = Redis,
|
|
264
|
-
connection_pool_class: Type[SentinelConnectionPool] = SentinelConnectionPool,
|
|
265
|
-
**kwargs,
|
|
266
|
-
):
|
|
267
|
-
"""
|
|
268
|
-
Returns a redis client instance for the ``service_name`` master.
|
|
269
|
-
|
|
270
|
-
A :py:class:`~redis.sentinel.SentinelConnectionPool` class is
|
|
271
|
-
used to retrive the master's address before establishing a new
|
|
272
|
-
connection.
|
|
273
|
-
|
|
274
|
-
NOTE: If the master's address has changed, any cached connections to
|
|
275
|
-
the old master are closed.
|
|
276
|
-
|
|
277
|
-
By default clients will be a :py:class:`~redis.Redis` instance.
|
|
278
|
-
Specify a different class to the ``redis_class`` argument if you
|
|
279
|
-
desire something different.
|
|
280
|
-
|
|
281
|
-
The ``connection_pool_class`` specifies the connection pool to
|
|
282
|
-
use. The :py:class:`~redis.sentinel.SentinelConnectionPool`
|
|
283
|
-
will be used by default.
|
|
284
|
-
|
|
285
|
-
All other keyword arguments are merged with any connection_kwargs
|
|
286
|
-
passed to this class and passed to the connection pool as keyword
|
|
287
|
-
arguments to be used to initialize Redis connections.
|
|
288
|
-
"""
|
|
289
|
-
kwargs["is_master"] = True
|
|
290
|
-
connection_kwargs = dict(self.connection_kwargs)
|
|
291
|
-
connection_kwargs.update(kwargs)
|
|
292
|
-
return redis_class(
|
|
293
|
-
connection_pool=connection_pool_class(
|
|
294
|
-
service_name, self, **connection_kwargs
|
|
295
|
-
)
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
def slave_for(
|
|
299
|
-
self,
|
|
300
|
-
service_name: str,
|
|
301
|
-
redis_class: Type[Redis] = Redis,
|
|
302
|
-
connection_pool_class: Type[SentinelConnectionPool] = SentinelConnectionPool,
|
|
303
|
-
**kwargs,
|
|
304
|
-
):
|
|
305
|
-
"""
|
|
306
|
-
Returns redis client instance for the ``service_name`` slave(s).
|
|
307
|
-
|
|
308
|
-
A SentinelConnectionPool class is used to retrive the slave's
|
|
309
|
-
address before establishing a new connection.
|
|
310
|
-
|
|
311
|
-
By default clients will be a :py:class:`~redis.Redis` instance.
|
|
312
|
-
Specify a different class to the ``redis_class`` argument if you
|
|
313
|
-
desire something different.
|
|
314
|
-
|
|
315
|
-
The ``connection_pool_class`` specifies the connection pool to use.
|
|
316
|
-
The SentinelConnectionPool will be used by default.
|
|
317
|
-
|
|
318
|
-
All other keyword arguments are merged with any connection_kwargs
|
|
319
|
-
passed to this class and passed to the connection pool as keyword
|
|
320
|
-
arguments to be used to initialize Redis connections.
|
|
321
|
-
"""
|
|
322
|
-
kwargs["is_master"] = False
|
|
323
|
-
connection_kwargs = dict(self.connection_kwargs)
|
|
324
|
-
connection_kwargs.update(kwargs)
|
|
325
|
-
return redis_class(
|
|
326
|
-
connection_pool=connection_pool_class(
|
|
327
|
-
service_name, self, **connection_kwargs
|
|
328
|
-
)
|
|
329
|
-
)
|
|
1
|
+
import random
|
|
2
|
+
import weakref
|
|
3
|
+
from typing import AsyncIterator, Iterable, Mapping, Sequence, Tuple, Type
|
|
4
|
+
|
|
5
|
+
from aioredis.client import Redis
|
|
6
|
+
from aioredis.connection import Connection, ConnectionPool, EncodableT, SSLConnection
|
|
7
|
+
from aioredis.exceptions import (
|
|
8
|
+
ConnectionError,
|
|
9
|
+
ReadOnlyError,
|
|
10
|
+
ResponseError,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
)
|
|
13
|
+
from aioredis.utils import str_if_bytes
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MasterNotFoundError(ConnectionError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SlaveNotFoundError(ConnectionError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SentinelManagedConnection(SSLConnection):
|
|
25
|
+
def __init__(self, **kwargs):
|
|
26
|
+
self.connection_pool = kwargs.pop("connection_pool")
|
|
27
|
+
if not kwargs.pop("ssl", False):
|
|
28
|
+
# use constructor from Connection class
|
|
29
|
+
super(SSLConnection, self).__init__(**kwargs)
|
|
30
|
+
else:
|
|
31
|
+
# use constructor from SSLConnection class
|
|
32
|
+
super().__init__(**kwargs)
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
pool = self.connection_pool
|
|
36
|
+
s = f"{self.__class__.__name__}<service={pool.service_name}"
|
|
37
|
+
if self.host:
|
|
38
|
+
host_info = f",host={self.host},port={self.port}"
|
|
39
|
+
s += host_info
|
|
40
|
+
return s + ">"
|
|
41
|
+
|
|
42
|
+
async def connect_to(self, address):
|
|
43
|
+
self.host, self.port = address
|
|
44
|
+
await super().connect()
|
|
45
|
+
if self.connection_pool.check_connection:
|
|
46
|
+
await self.send_command("PING")
|
|
47
|
+
if str_if_bytes(await self.read_response()) != "PONG":
|
|
48
|
+
raise ConnectionError("PING failed")
|
|
49
|
+
|
|
50
|
+
async def connect(self):
|
|
51
|
+
if self._reader:
|
|
52
|
+
return # already connected
|
|
53
|
+
if self.connection_pool.is_master:
|
|
54
|
+
await self.connect_to(await self.connection_pool.get_master_address())
|
|
55
|
+
else:
|
|
56
|
+
async for slave in self.connection_pool.rotate_slaves():
|
|
57
|
+
try:
|
|
58
|
+
return await self.connect_to(slave)
|
|
59
|
+
except ConnectionError:
|
|
60
|
+
continue
|
|
61
|
+
raise SlaveNotFoundError # Never be here
|
|
62
|
+
|
|
63
|
+
async def read_response(self):
|
|
64
|
+
try:
|
|
65
|
+
return await super().read_response()
|
|
66
|
+
except ReadOnlyError:
|
|
67
|
+
if self.connection_pool.is_master:
|
|
68
|
+
# When talking to a master, a ReadOnlyError when likely
|
|
69
|
+
# indicates that the previous master that we're still connected
|
|
70
|
+
# to has been demoted to a slave and there's a new master.
|
|
71
|
+
# calling disconnect will force the connection to re-query
|
|
72
|
+
# sentinel during the next connect() attempt.
|
|
73
|
+
await self.disconnect()
|
|
74
|
+
raise ConnectionError("The previous master is now a slave")
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SentinelConnectionPool(ConnectionPool):
|
|
79
|
+
"""
|
|
80
|
+
Sentinel backed connection pool.
|
|
81
|
+
|
|
82
|
+
If ``check_connection`` flag is set to True, SentinelManagedConnection
|
|
83
|
+
sends a PING command right after establishing the connection.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, service_name, sentinel_manager, **kwargs):
|
|
87
|
+
kwargs["connection_class"] = kwargs.get(
|
|
88
|
+
"connection_class", SentinelManagedConnection
|
|
89
|
+
)
|
|
90
|
+
self.is_master = kwargs.pop("is_master", True)
|
|
91
|
+
self.check_connection = kwargs.pop("check_connection", False)
|
|
92
|
+
super().__init__(**kwargs)
|
|
93
|
+
self.connection_kwargs["connection_pool"] = weakref.proxy(self)
|
|
94
|
+
self.service_name = service_name
|
|
95
|
+
self.sentinel_manager = sentinel_manager
|
|
96
|
+
self.master_address = None
|
|
97
|
+
self.slave_rr_counter = None
|
|
98
|
+
|
|
99
|
+
def __repr__(self):
|
|
100
|
+
return (
|
|
101
|
+
f"{self.__class__.__name__}"
|
|
102
|
+
f"<service={self.service_name}({self.is_master and 'master' or 'slave'})>"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def reset(self):
|
|
106
|
+
super().reset()
|
|
107
|
+
self.master_address = None
|
|
108
|
+
self.slave_rr_counter = None
|
|
109
|
+
|
|
110
|
+
def owns_connection(self, connection: Connection):
|
|
111
|
+
check = not self.is_master or (
|
|
112
|
+
self.is_master and self.master_address == (connection.host, connection.port)
|
|
113
|
+
)
|
|
114
|
+
return check and super().owns_connection(connection)
|
|
115
|
+
|
|
116
|
+
async def get_master_address(self):
|
|
117
|
+
master_address = await self.sentinel_manager.discover_master(self.service_name)
|
|
118
|
+
if self.is_master:
|
|
119
|
+
if self.master_address != master_address:
|
|
120
|
+
self.master_address = master_address
|
|
121
|
+
# disconnect any idle connections so that they reconnect
|
|
122
|
+
# to the new master the next time that they are used.
|
|
123
|
+
await self.disconnect(inuse_connections=False)
|
|
124
|
+
return master_address
|
|
125
|
+
|
|
126
|
+
async def rotate_slaves(self) -> AsyncIterator:
|
|
127
|
+
"""Round-robin slave balancer"""
|
|
128
|
+
slaves = await self.sentinel_manager.discover_slaves(self.service_name)
|
|
129
|
+
if slaves:
|
|
130
|
+
if self.slave_rr_counter is None:
|
|
131
|
+
self.slave_rr_counter = random.randint(0, len(slaves) - 1)
|
|
132
|
+
for _ in range(len(slaves)):
|
|
133
|
+
self.slave_rr_counter = (self.slave_rr_counter + 1) % len(slaves)
|
|
134
|
+
slave = slaves[self.slave_rr_counter]
|
|
135
|
+
yield slave
|
|
136
|
+
# Fallback to the master connection
|
|
137
|
+
try:
|
|
138
|
+
yield await self.get_master_address()
|
|
139
|
+
except MasterNotFoundError:
|
|
140
|
+
pass
|
|
141
|
+
raise SlaveNotFoundError(f"No slave found for {self.service_name!r}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Sentinel:
|
|
145
|
+
"""
|
|
146
|
+
Redis Sentinel cluster client
|
|
147
|
+
|
|
148
|
+
>>> from aioredis.sentinel import Sentinel
|
|
149
|
+
>>> sentinel = Sentinel([('localhost', 26379)], socket_timeout=0.1)
|
|
150
|
+
>>> master = sentinel.master_for('mymaster', socket_timeout=0.1)
|
|
151
|
+
>>> await master.set('foo', 'bar')
|
|
152
|
+
>>> slave = sentinel.slave_for('mymaster', socket_timeout=0.1)
|
|
153
|
+
>>> await slave.get('foo')
|
|
154
|
+
b'bar'
|
|
155
|
+
|
|
156
|
+
``sentinels`` is a list of sentinel nodes. Each node is represented by
|
|
157
|
+
a pair (hostname, port).
|
|
158
|
+
|
|
159
|
+
``min_other_sentinels`` defined a minimum number of peers for a sentinel.
|
|
160
|
+
When querying a sentinel, if it doesn't meet this threshold, responses
|
|
161
|
+
from that sentinel won't be considered valid.
|
|
162
|
+
|
|
163
|
+
``sentinel_kwargs`` is a dictionary of connection arguments used when
|
|
164
|
+
connecting to sentinel instances. Any argument that can be passed to
|
|
165
|
+
a normal Redis connection can be specified here. If ``sentinel_kwargs`` is
|
|
166
|
+
not specified, any socket_timeout and socket_keepalive options specified
|
|
167
|
+
in ``connection_kwargs`` will be used.
|
|
168
|
+
|
|
169
|
+
``connection_kwargs`` are keyword arguments that will be used when
|
|
170
|
+
establishing a connection to a Redis server.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
sentinels,
|
|
176
|
+
min_other_sentinels=0,
|
|
177
|
+
sentinel_kwargs=None,
|
|
178
|
+
**connection_kwargs,
|
|
179
|
+
):
|
|
180
|
+
# if sentinel_kwargs isn't defined, use the socket_* options from
|
|
181
|
+
# connection_kwargs
|
|
182
|
+
if sentinel_kwargs is None:
|
|
183
|
+
sentinel_kwargs = {
|
|
184
|
+
k: v for k, v in connection_kwargs.items() if k.startswith("socket_")
|
|
185
|
+
}
|
|
186
|
+
self.sentinel_kwargs = sentinel_kwargs
|
|
187
|
+
|
|
188
|
+
self.sentinels = [
|
|
189
|
+
Redis(host=hostname, port=port, **self.sentinel_kwargs)
|
|
190
|
+
for hostname, port in sentinels
|
|
191
|
+
]
|
|
192
|
+
self.min_other_sentinels = min_other_sentinels
|
|
193
|
+
self.connection_kwargs = connection_kwargs
|
|
194
|
+
|
|
195
|
+
def __repr__(self):
|
|
196
|
+
sentinel_addresses = []
|
|
197
|
+
for sentinel in self.sentinels:
|
|
198
|
+
sentinel_addresses.append(
|
|
199
|
+
f"{sentinel.connection_pool.connection_kwargs['host']}:"
|
|
200
|
+
f"{sentinel.connection_pool.connection_kwargs['port']}"
|
|
201
|
+
)
|
|
202
|
+
return f"{self.__class__.__name__}<sentinels=[{','.join(sentinel_addresses)}]>"
|
|
203
|
+
|
|
204
|
+
def check_master_state(self, state: dict, service_name: str) -> bool:
|
|
205
|
+
if not state["is_master"] or state["is_sdown"] or state["is_odown"]:
|
|
206
|
+
return False
|
|
207
|
+
# Check if our sentinel doesn't see other nodes
|
|
208
|
+
if state["num-other-sentinels"] < self.min_other_sentinels:
|
|
209
|
+
return False
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
async def discover_master(self, service_name: str):
|
|
213
|
+
"""
|
|
214
|
+
Asks sentinel servers for the Redis master's address corresponding
|
|
215
|
+
to the service labeled ``service_name``.
|
|
216
|
+
|
|
217
|
+
Returns a pair (address, port) or raises MasterNotFoundError if no
|
|
218
|
+
master is found.
|
|
219
|
+
"""
|
|
220
|
+
for sentinel_no, sentinel in enumerate(self.sentinels):
|
|
221
|
+
try:
|
|
222
|
+
masters = await sentinel.sentinel_masters()
|
|
223
|
+
except (ConnectionError, TimeoutError):
|
|
224
|
+
continue
|
|
225
|
+
state = masters.get(service_name)
|
|
226
|
+
if state and self.check_master_state(state, service_name):
|
|
227
|
+
# Put this sentinel at the top of the list
|
|
228
|
+
self.sentinels[0], self.sentinels[sentinel_no] = (
|
|
229
|
+
sentinel,
|
|
230
|
+
self.sentinels[0],
|
|
231
|
+
)
|
|
232
|
+
return state["ip"], state["port"]
|
|
233
|
+
raise MasterNotFoundError(f"No master found for {service_name!r}")
|
|
234
|
+
|
|
235
|
+
def filter_slaves(
|
|
236
|
+
self, slaves: Iterable[Mapping]
|
|
237
|
+
) -> Sequence[Tuple[EncodableT, EncodableT]]:
|
|
238
|
+
"""Remove slaves that are in an ODOWN or SDOWN state"""
|
|
239
|
+
slaves_alive = []
|
|
240
|
+
for slave in slaves:
|
|
241
|
+
if slave["is_odown"] or slave["is_sdown"]:
|
|
242
|
+
continue
|
|
243
|
+
slaves_alive.append((slave["ip"], slave["port"]))
|
|
244
|
+
return slaves_alive
|
|
245
|
+
|
|
246
|
+
async def discover_slaves(
|
|
247
|
+
self, service_name: str
|
|
248
|
+
) -> Sequence[Tuple[EncodableT, EncodableT]]:
|
|
249
|
+
"""Returns a list of alive slaves for service ``service_name``"""
|
|
250
|
+
for sentinel in self.sentinels:
|
|
251
|
+
try:
|
|
252
|
+
slaves = await sentinel.sentinel_slaves(service_name)
|
|
253
|
+
except (ConnectionError, ResponseError, TimeoutError):
|
|
254
|
+
continue
|
|
255
|
+
slaves = self.filter_slaves(slaves)
|
|
256
|
+
if slaves:
|
|
257
|
+
return slaves
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
def master_for(
|
|
261
|
+
self,
|
|
262
|
+
service_name: str,
|
|
263
|
+
redis_class: Type[Redis] = Redis,
|
|
264
|
+
connection_pool_class: Type[SentinelConnectionPool] = SentinelConnectionPool,
|
|
265
|
+
**kwargs,
|
|
266
|
+
):
|
|
267
|
+
"""
|
|
268
|
+
Returns a redis client instance for the ``service_name`` master.
|
|
269
|
+
|
|
270
|
+
A :py:class:`~redis.sentinel.SentinelConnectionPool` class is
|
|
271
|
+
used to retrive the master's address before establishing a new
|
|
272
|
+
connection.
|
|
273
|
+
|
|
274
|
+
NOTE: If the master's address has changed, any cached connections to
|
|
275
|
+
the old master are closed.
|
|
276
|
+
|
|
277
|
+
By default clients will be a :py:class:`~redis.Redis` instance.
|
|
278
|
+
Specify a different class to the ``redis_class`` argument if you
|
|
279
|
+
desire something different.
|
|
280
|
+
|
|
281
|
+
The ``connection_pool_class`` specifies the connection pool to
|
|
282
|
+
use. The :py:class:`~redis.sentinel.SentinelConnectionPool`
|
|
283
|
+
will be used by default.
|
|
284
|
+
|
|
285
|
+
All other keyword arguments are merged with any connection_kwargs
|
|
286
|
+
passed to this class and passed to the connection pool as keyword
|
|
287
|
+
arguments to be used to initialize Redis connections.
|
|
288
|
+
"""
|
|
289
|
+
kwargs["is_master"] = True
|
|
290
|
+
connection_kwargs = dict(self.connection_kwargs)
|
|
291
|
+
connection_kwargs.update(kwargs)
|
|
292
|
+
return redis_class(
|
|
293
|
+
connection_pool=connection_pool_class(
|
|
294
|
+
service_name, self, **connection_kwargs
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def slave_for(
|
|
299
|
+
self,
|
|
300
|
+
service_name: str,
|
|
301
|
+
redis_class: Type[Redis] = Redis,
|
|
302
|
+
connection_pool_class: Type[SentinelConnectionPool] = SentinelConnectionPool,
|
|
303
|
+
**kwargs,
|
|
304
|
+
):
|
|
305
|
+
"""
|
|
306
|
+
Returns redis client instance for the ``service_name`` slave(s).
|
|
307
|
+
|
|
308
|
+
A SentinelConnectionPool class is used to retrive the slave's
|
|
309
|
+
address before establishing a new connection.
|
|
310
|
+
|
|
311
|
+
By default clients will be a :py:class:`~redis.Redis` instance.
|
|
312
|
+
Specify a different class to the ``redis_class`` argument if you
|
|
313
|
+
desire something different.
|
|
314
|
+
|
|
315
|
+
The ``connection_pool_class`` specifies the connection pool to use.
|
|
316
|
+
The SentinelConnectionPool will be used by default.
|
|
317
|
+
|
|
318
|
+
All other keyword arguments are merged with any connection_kwargs
|
|
319
|
+
passed to this class and passed to the connection pool as keyword
|
|
320
|
+
arguments to be used to initialize Redis connections.
|
|
321
|
+
"""
|
|
322
|
+
kwargs["is_master"] = False
|
|
323
|
+
connection_kwargs = dict(self.connection_kwargs)
|
|
324
|
+
connection_kwargs.update(kwargs)
|
|
325
|
+
return redis_class(
|
|
326
|
+
connection_pool=connection_pool_class(
|
|
327
|
+
service_name, self, **connection_kwargs
|
|
328
|
+
)
|
|
329
|
+
)
|