1
1
import asyncio
2
2
import random
3
3
import weakref
4
- from typing import AsyncIterator , Iterable , Mapping , Optional , Sequence , Tuple , Type
4
+ import uuid
5
+ from typing import AsyncIterator , Iterable , Mapping , Optional , Sequence , Tuple , Type , Any
5
6
6
7
from redis .asyncio .client import Redis
7
8
from redis .asyncio .connection import (
@@ -65,6 +66,22 @@ async def connect(self):
65
66
self ._connect_retry ,
66
67
lambda error : asyncio .sleep (0 ),
67
68
)
69
+
70
+ async def _connect_to_address_retry (self , host : str , port : int ) -> None :
71
+ if self ._reader :
72
+ return # already connected
73
+ try :
74
+ return await self .connect_to ((host , port ))
75
+ except ConnectionError as exc :
76
+ raise SlaveNotFoundError
77
+
78
+ async def connect_to_address (self , host : str , port : int ) -> None :
79
+ # Connect to the specified host and port
80
+ # instead of connecting to the master / rotated slaves
81
+ return await self .retry .call_with_retry (
82
+ lambda : self ._connect_to_address_retry (host , port ),
83
+ lambda error : asyncio .sleep (0 ),
84
+ )
68
85
69
86
async def read_response (
70
87
self ,
@@ -122,6 +139,7 @@ def __init__(self, service_name, sentinel_manager, **kwargs):
122
139
self .sentinel_manager = sentinel_manager
123
140
self .master_address = None
124
141
self .slave_rr_counter = None
142
+ self ._request_id_to_replica_address = {}
125
143
126
144
def __repr__ (self ):
127
145
return (
@@ -152,6 +170,11 @@ async def get_master_address(self):
152
170
153
171
async def rotate_slaves (self ) -> AsyncIterator :
154
172
"""Round-robin slave balancer"""
173
+ (
174
+ server_host ,
175
+ server_port ,
176
+ ) = self ._request_id_to_replica_address .get (iter_req_id , (None , None ))
177
+
155
178
slaves = await self .sentinel_manager .discover_slaves (self .service_name )
156
179
if slaves :
157
180
if self .slave_rr_counter is None :
@@ -167,6 +190,102 @@ async def rotate_slaves(self) -> AsyncIterator:
167
190
pass
168
191
raise SlaveNotFoundError (f"No slave found for { self .service_name !r} " )
169
192
193
+ async def get_connection (
194
+ self , command_name : str , * keys : Any , ** options : Any
195
+ ) -> SentinelManagedConnection :
196
+ """
197
+ Get a connection from the pool.
198
+ `xxx_scan_iter` commands needs to be handled specially.
199
+ If the client is created using a connection pool, in replica mode,
200
+ all `scan` command-equivalent of the `xxx_scan_iter` commands needs
201
+ to be issued to the same Redis replica.
202
+
203
+ The way each server positions each key is different with one another,
204
+ and the cursor acts as the 'offset' of the scan.
205
+ Hence, all scans coming from a single xxx_scan_iter_channel command
206
+ should go to the same replica.
207
+ """
208
+ # If not an iter command or in master mode, call super()
209
+ # No custom logic for master, because there's only 1 master.
210
+ # The bug is only when Redis has the possibility to connect to multiple replicas
211
+ if not (iter_req_id := options .get ("_iter_req_id" , None )) or self .is_master :
212
+ return await super ().get_connection (command_name , * keys , ** options ) # type: ignore[no-any-return]
213
+
214
+ # Check if this iter request has already been directed to a particular server
215
+ # Check if this iter request has already been directed to a particular server
216
+ (
217
+ server_host ,
218
+ server_port ,
219
+ ) = self ._request_id_to_replica_address .get (iter_req_id , (None , None ))
220
+ connection = None
221
+ # If this is the first scan request of the iter command,
222
+ # get a connection from the pool
223
+ if server_host is None or server_port is None :
224
+ try :
225
+ connection = self ._available_connections .pop () # type: ignore [assignment]
226
+ except IndexError :
227
+ connection = self .make_connection ()
228
+ # If this is not the first scan request of the iter command
229
+ else :
230
+ # Check from the available connections, if any of the connection
231
+ # is connected to the host and port that we want
232
+ # If yes, use that connection
233
+ for available_connection in self ._available_connections .copy ():
234
+ if (
235
+ available_connection .host == server_host
236
+ and available_connection .port == server_port
237
+ ):
238
+ self ._available_connections .remove (available_connection )
239
+ connection = available_connection # type: ignore[assignment]
240
+ # If not, make a new dummy connection object, and set its host and port
241
+ # to the one that we want later in the call to ``connect_to_address``
242
+ if not connection :
243
+ connection = self .make_connection ()
244
+ assert connection
245
+ self ._in_use_connections .add (connection )
246
+ try :
247
+ # ensure this connection is connected to Redis
248
+ # If this is the first scan request,
249
+ # just call the SentinelManagedConnection.connect()
250
+ # This will call rotate_slaves
251
+ # and connect to a random replica
252
+ if server_port is None or server_port is None :
253
+ await connection .connect ()
254
+ # If this is not the first scan request,
255
+ # connect to the particular address and port
256
+ else :
257
+ # This will connect to the host and port that we've specified above
258
+ await connection .connect_to_address (server_host , server_port ) # type: ignore[arg-type]
259
+ # connections that the pool provides should be ready to send
260
+ # a command. if not, the connection was either returned to the
261
+ # pool before all data has been read or the socket has been
262
+ # closed. either way, reconnect and verify everything is good.
263
+ try :
264
+ # type ignore below:
265
+ # attr Not defined in redis stubs and
266
+ # we don't need to create a subclass to help with this single attr
267
+ if await connection .can_read_destructive (): # type: ignore[attr-defined]
268
+ raise ConnectionError ("Connection has data" ) from None
269
+ except (ConnectionError , OSError ):
270
+ await connection .disconnect ()
271
+ await connection .connect ()
272
+ # type ignore below: similar to above
273
+ if await connection .can_read_destructive (): # type: ignore[attr-defined]
274
+ raise ConnectionError ("Connection not ready" ) from None
275
+ except BaseException :
276
+ # release the connection back to the pool so that we don't
277
+ # leak it
278
+ await self .release (connection )
279
+ raise
280
+ # Store the connection to the dictionary
281
+ self ._request_id_to_replica_address [iter_req_id ] = (
282
+ connection .host ,
283
+ connection .port ,
284
+ )
285
+
286
+ return connection
287
+
288
+
170
289
171
290
class Sentinel (AsyncSentinelCommands ):
172
291
"""
0 commit comments