4
4
import inspect
5
5
import logging
6
6
import socket
7
+ import time
7
8
from typing import (
8
9
Any ,
9
10
AsyncIterator ,
21
22
22
23
import redis .exceptions
23
24
import yarl
24
- from redis .asyncio import Redis
25
+ from redis .asyncio import ConnectionPool , Redis
25
26
from redis .asyncio .client import Pipeline , PubSub
26
27
from redis .asyncio .sentinel import MasterNotFoundError , Sentinel , SlaveNotFoundError
27
28
from redis .backoff import ExponentialBackoff
63
64
redis .exceptions .TimeoutError ,
64
65
],
65
66
}
67
+ _default_conn_pool_opts : Mapping [str , Any ] = {
68
+ "max_connections" : 16 ,
69
+ # "timeout": 20.0, # for redis-py 5.0+
70
+ }
66
71
67
72
_scripts : Dict [str , str ] = {}
68
73
@@ -73,11 +78,6 @@ class ConnectionNotAvailable(Exception):
73
78
pass
74
79
75
80
76
- def _parse_stream_msg_id (msg_id : bytes ) -> Tuple [int , int ]:
77
- timestamp , _ , sequence = msg_id .partition (b"-" )
78
- return int (timestamp ), int (sequence )
79
-
80
-
81
81
async def subscribe (channel : PubSub , * , reconnect_poll_interval : float = 0.3 ) -> AsyncIterator [Any ]:
82
82
"""
83
83
An async-generator wrapper for pub-sub channel subscription.
@@ -130,7 +130,7 @@ async def blpop(
130
130
redis_obj : RedisConnectionInfo ,
131
131
key : str ,
132
132
* ,
133
- service_name : str = None ,
133
+ service_name : Optional [ str ] = None ,
134
134
) -> AsyncIterator [Any ]:
135
135
"""
136
136
An async-generator wrapper for blpop (blocking left pop).
@@ -175,8 +175,9 @@ async def execute(
175
175
redis_obj : RedisConnectionInfo ,
176
176
func : Callable [[Redis ], Awaitable [Any ]],
177
177
* ,
178
- service_name : str = None ,
178
+ service_name : Optional [ str ] = None ,
179
179
encoding : Optional [str ] = None ,
180
+ command_timeout : Optional [float ] = None ,
180
181
) -> Any :
181
182
"""
182
183
Executes a function that issues Redis commands or returns a pipeline/transaction of commands,
@@ -187,9 +188,25 @@ async def execute(
187
188
"""
188
189
redis_client = redis_obj .client
189
190
service_name = service_name or redis_obj .service_name
190
- reconnect_poll_interval = float (
191
- cast (str , redis_obj .redis_helper_config .get ("reconnect_poll_timeout" ))
192
- )
191
+ reconnect_poll_interval = redis_obj .redis_helper_config .get ("reconnect_poll_timeout" , 0.0 )
192
+
193
+ first_trial = time .perf_counter ()
194
+ retry_log_count = 0
195
+ last_log_time = first_trial
196
+
197
+ def show_retry_warning (e : Exception , warn_on_first_attempt : bool = True ) -> None :
198
+ nonlocal retry_log_count , last_log_time
199
+ now = time .perf_counter ()
200
+ if (warn_on_first_attempt and retry_log_count == 0 ) or now - last_log_time >= 10.0 :
201
+ log .warning (
202
+ "Retrying due to interruption of Redis connection "
203
+ "({}, conn-pool: {}, retrying-for: {:.3f}s)" ,
204
+ repr (e ),
205
+ redis_obj .name ,
206
+ now - first_trial ,
207
+ )
208
+ retry_log_count += 1
209
+ last_log_time = now
193
210
194
211
while True :
195
212
try :
@@ -228,21 +245,33 @@ async def execute(
228
245
MasterNotFoundError ,
229
246
SlaveNotFoundError ,
230
247
redis .exceptions .ReadOnlyError ,
248
+ redis .exceptions .ConnectionError ,
231
249
ConnectionResetError ,
232
- ):
250
+ ) as e :
251
+ warn_on_first_attempt = True
252
+ if (
253
+ isinstance (e , redis .exceptions .ConnectionError )
254
+ and "Too many connections" in e .args [0 ]
255
+ ): # connection pool is full
256
+ warn_on_first_attempt = False
257
+ show_retry_warning (e , warn_on_first_attempt )
233
258
await asyncio .sleep (reconnect_poll_interval )
234
259
continue
235
- except redis .exceptions .ConnectionError as e :
236
- log .error (f"execute(): Connecting to redis failed: { e } " )
237
- await asyncio .sleep (reconnect_poll_interval )
260
+ except (
261
+ redis .exceptions .TimeoutError ,
262
+ asyncio .TimeoutError ,
263
+ ) as e :
264
+ if command_timeout is not None :
265
+ now = time .perf_counter ()
266
+ if now - first_trial >= command_timeout + 1.0 :
267
+ show_retry_warning (e )
238
268
continue
239
269
except redis .exceptions .ResponseError as e :
240
270
if "NOREPLICAS" in e .args [0 ]:
271
+ show_retry_warning (e )
241
272
await asyncio .sleep (reconnect_poll_interval )
242
273
continue
243
274
raise
244
- except (redis .exceptions .TimeoutError , asyncio .TimeoutError ):
245
- continue
246
275
except asyncio .CancelledError :
247
276
raise
248
277
finally :
@@ -317,6 +346,7 @@ async def read_stream(
317
346
{stream_key : last_id },
318
347
block = block_timeout ,
319
348
),
349
+ command_timeout = block_timeout / 1000 ,
320
350
)
321
351
if not reply :
322
352
continue
@@ -367,6 +397,7 @@ async def read_stream_by_group(
367
397
str (autoclaim_idle_timeout ),
368
398
autoclaim_start_id ,
369
399
),
400
+ command_timeout = autoclaim_idle_timeout / 1000 ,
370
401
)
371
402
for msg_id , msg_data in reply [1 ]:
372
403
messages .append ((msg_id , msg_data ))
@@ -381,6 +412,7 @@ async def read_stream_by_group(
381
412
{stream_key : b">" }, # fetch messages not seen by other consumers
382
413
block = block_timeout ,
383
414
),
415
+ command_timeout = block_timeout / 1000 ,
384
416
)
385
417
if len (reply ) == 0 :
386
418
continue
@@ -422,13 +454,32 @@ async def read_stream_by_group(
422
454
423
455
def get_redis_object (
424
456
redis_config : EtcdRedisConfig ,
457
+ * ,
458
+ name : str ,
425
459
db : int = 0 ,
426
460
** kwargs ,
427
461
) -> RedisConnectionInfo :
428
462
redis_helper_config : RedisHelperConfig = cast (
429
463
RedisHelperConfig , redis_config .get ("redis_helper_config" )
430
464
)
431
-
465
+ conn_opts = {
466
+ ** _default_conn_opts ,
467
+ ** kwargs ,
468
+ # "lib_name": None, # disable implicit "CLIENT SETINFO" (for redis-py 5.0+)
469
+ # "lib_version": None, # disable implicit "CLIENT SETINFO" (for redis-py 5.0+)
470
+ }
471
+ conn_pool_opts = {
472
+ ** _default_conn_pool_opts ,
473
+ }
474
+ if socket_timeout := redis_helper_config .get ("socket_timeout" ):
475
+ conn_opts ["socket_timeout" ] = float (socket_timeout )
476
+ if socket_connect_timeout := redis_helper_config .get ("socket_connect_timeout" ):
477
+ conn_opts ["socket_connect_timeout" ] = float (socket_connect_timeout )
478
+ if max_connections := redis_helper_config .get ("max_connections" ):
479
+ conn_pool_opts ["max_connections" ] = int (max_connections )
480
+ # for redis-py 5.0+
481
+ # if connection_ready_timeout := redis_helper_config.get("connection_ready_timeout"):
482
+ # conn_pool_opts["timeout"] = float(connection_ready_timeout)
432
483
if _sentinel_addresses := redis_config .get ("sentinel" ):
433
484
sentinel_addresses : Any = None
434
485
if isinstance (_sentinel_addresses , str ):
@@ -453,19 +504,14 @@ def get_redis_object(
453
504
** kwargs ,
454
505
},
455
506
)
456
-
457
- conn_opts = {
458
- ** _default_conn_opts ,
459
- ** kwargs ,
460
- "socket_timeout" : float (cast (str , redis_helper_config .get ("socket_timeout" ))),
461
- "socket_connect_timeout" : float (
462
- cast (str , redis_helper_config .get ("socket_connect_timeout" ))
463
- ),
464
- }
465
-
466
507
return RedisConnectionInfo (
467
- client = sentinel .master_for (service_name = service_name , password = password , ** conn_opts ),
508
+ client = sentinel .master_for (
509
+ service_name = service_name ,
510
+ password = password ,
511
+ ** conn_opts ,
512
+ ),
468
513
sentinel = sentinel ,
514
+ name = name ,
469
515
service_name = service_name ,
470
516
redis_helper_config = redis_helper_config ,
471
517
)
@@ -475,10 +521,18 @@ def get_redis_object(
475
521
url = yarl .URL ("redis://host" ).with_host (str (redis_url [0 ])).with_port (
476
522
redis_url [1 ]
477
523
).with_password (redis_config .get ("password" )) / str (db )
478
-
479
524
return RedisConnectionInfo (
480
- client = Redis .from_url (str (url ), ** kwargs ),
525
+ # In redis-py 5.0.1+, we should migrate to `Redis.from_pool()` API
526
+ client = Redis (
527
+ connection_pool = ConnectionPool .from_url (
528
+ str (url ),
529
+ ** conn_pool_opts ,
530
+ ),
531
+ ** conn_opts ,
532
+ auto_close_connection_pool = True ,
533
+ ),
481
534
sentinel = None ,
535
+ name = name ,
482
536
service_name = None ,
483
537
redis_helper_config = redis_helper_config ,
484
538
)
0 commit comments