funboost 44.8__py3-none-any.whl → 45.0__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.

Files changed (78) hide show
  1. funboost/__init__.py +1 -1
  2. funboost/constant.py +14 -4
  3. funboost/consumers/base_consumer.py +48 -20
  4. funboost/core/booster.py +15 -0
  5. funboost/core/current_task.py +88 -69
  6. funboost/core/func_params_model.py +8 -0
  7. funboost/function_result_web/__pycache__/functions.cpython-39.pyc +0 -0
  8. funboost/publishers/base_publisher.py +21 -2
  9. funboost/set_frame_config.py +2 -1
  10. funboost/utils/class_utils.py +101 -51
  11. funboost/utils/dependency_packages_in_pythonpath/__pycache__/__init__.cpython-39.pyc +0 -0
  12. funboost/utils/dependency_packages_in_pythonpath/__pycache__/add_to_pythonpath.cpython-39.pyc +0 -0
  13. funboost/utils/dependency_packages_in_pythonpath/aioredis/__init__.py +59 -59
  14. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/__init__.cpython-39.pyc +0 -0
  15. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/client.cpython-39.pyc +0 -0
  16. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/compat.cpython-39.pyc +0 -0
  17. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/connection.cpython-39.pyc +0 -0
  18. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/exceptions.cpython-39.pyc +0 -0
  19. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/lock.cpython-39.pyc +0 -0
  20. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/utils.cpython-39.pyc +0 -0
  21. funboost/utils/dependency_packages_in_pythonpath/aioredis/client.py +4804 -4804
  22. funboost/utils/dependency_packages_in_pythonpath/aioredis/compat.py +8 -8
  23. funboost/utils/dependency_packages_in_pythonpath/aioredis/connection.py +1668 -1668
  24. funboost/utils/dependency_packages_in_pythonpath/aioredis/exceptions.py +96 -96
  25. funboost/utils/dependency_packages_in_pythonpath/aioredis/lock.py +306 -306
  26. funboost/utils/dependency_packages_in_pythonpath/aioredis/log.py +15 -15
  27. funboost/utils/dependency_packages_in_pythonpath/aioredis/sentinel.py +329 -329
  28. funboost/utils/dependency_packages_in_pythonpath/aioredis/utils.py +61 -61
  29. funboost/utils/dependency_packages_in_pythonpath/func_timeout/StoppableThread.py +134 -133
  30. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__init__.py +16 -16
  31. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/StoppableThread.cpython-39.pyc +0 -0
  32. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/__init__.cpython-39.pyc +0 -0
  33. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/dafunc.cpython-39.pyc +0 -0
  34. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/exceptions.cpython-39.pyc +0 -0
  35. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/py3_raise.cpython-39.pyc +0 -0
  36. funboost/utils/dependency_packages_in_pythonpath/func_timeout/dafunc.py +244 -244
  37. funboost/utils/dependency_packages_in_pythonpath/func_timeout/dafunc2222.py +244 -0
  38. funboost/utils/dependency_packages_in_pythonpath/func_timeout/exceptions.py +98 -98
  39. funboost/utils/dependency_packages_in_pythonpath/func_timeout/py2_raise.py +7 -7
  40. funboost/utils/dependency_packages_in_pythonpath/func_timeout/py3_raise.py +7 -7
  41. funboost/utils/times/__init__.py +85 -85
  42. funboost/utils/times/version.py +1 -1
  43. {funboost-44.8.dist-info → funboost-45.0.dist-info}/METADATA +1 -2
  44. {funboost-44.8.dist-info → funboost-45.0.dist-info}/RECORD +48 -76
  45. {funboost-44.8.dist-info → funboost-45.0.dist-info}/entry_points.txt +0 -1
  46. funboost/function_result_web/__pycache__/app.cpython-37.pyc +0 -0
  47. funboost/function_result_web/__pycache__/functions.cpython-37.pyc +0 -0
  48. funboost/utils/dependency_packages_in_pythonpath/__pycache__/__init__.cpython-311.pyc +0 -0
  49. funboost/utils/dependency_packages_in_pythonpath/__pycache__/__init__.cpython-37.pyc +0 -0
  50. funboost/utils/dependency_packages_in_pythonpath/__pycache__/add_to_pythonpath.cpython-311.pyc +0 -0
  51. funboost/utils/dependency_packages_in_pythonpath/__pycache__/add_to_pythonpath.cpython-37.pyc +0 -0
  52. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/__init__.cpython-311.pyc +0 -0
  53. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/__init__.cpython-37.pyc +0 -0
  54. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/client.cpython-311.pyc +0 -0
  55. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/client.cpython-37.pyc +0 -0
  56. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/compat.cpython-311.pyc +0 -0
  57. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/compat.cpython-37.pyc +0 -0
  58. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/connection.cpython-311.pyc +0 -0
  59. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/connection.cpython-37.pyc +0 -0
  60. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/exceptions.cpython-311.pyc +0 -0
  61. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/exceptions.cpython-37.pyc +0 -0
  62. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/lock.cpython-311.pyc +0 -0
  63. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/lock.cpython-37.pyc +0 -0
  64. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/utils.cpython-311.pyc +0 -0
  65. funboost/utils/dependency_packages_in_pythonpath/aioredis/__pycache__/utils.cpython-37.pyc +0 -0
  66. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/StoppableThread.cpython-311.pyc +0 -0
  67. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/StoppableThread.cpython-37.pyc +0 -0
  68. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/__init__.cpython-311.pyc +0 -0
  69. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/__init__.cpython-37.pyc +0 -0
  70. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/dafunc.cpython-311.pyc +0 -0
  71. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/dafunc.cpython-37.pyc +0 -0
  72. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/exceptions.cpython-311.pyc +0 -0
  73. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/exceptions.cpython-37.pyc +0 -0
  74. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/py3_raise.cpython-311.pyc +0 -0
  75. funboost/utils/dependency_packages_in_pythonpath/func_timeout/__pycache__/py3_raise.cpython-37.pyc +0 -0
  76. {funboost-44.8.dist-info → funboost-45.0.dist-info}/LICENSE +0 -0
  77. {funboost-44.8.dist-info → funboost-45.0.dist-info}/WHEEL +0 -0
  78. {funboost-44.8.dist-info → funboost-45.0.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
+ )