11"""Redis-backed transport using ``RPUSH``/``BLPOP`` for tasks and results."""
22
3+ import math
34import time
45import asyncio
56from typing import Any
7+ from urllib .parse import parse_qs , urlparse
68from collections .abc import AsyncIterator
79
810try :
911 import redis .asyncio as _redis
12+ from redis .exceptions import TimeoutError as RedisTimeoutError
1013except ImportError :
1114 raise ImportError (
1215 "redis package is required for RedisBackend. "
@@ -50,7 +53,11 @@ def __init__(
5053 queue_key : str | None = None ,
5154 result_ttl : int | None = None ,
5255 ) -> None :
53- self ._redis : Any = _redis .Redis .from_url (url )
56+ query = parse_qs (urlparse (url ).query )
57+ connect_kwargs : dict [str , Any ] = {}
58+ if "socket_timeout" not in query :
59+ connect_kwargs ["socket_timeout" ] = None
60+ self ._redis : Any = _redis .Redis .from_url (url , ** connect_kwargs )
5461 self ._queue_key = queue_key or self .DEFAULT_QUEUE_KEY
5562 self ._result_ttl = result_ttl or self .DEFAULT_RESULT_TTL
5663
@@ -60,7 +67,13 @@ async def submit(self, task_json: str) -> None:
6067 async def listen (self ) -> AsyncIterator [str ]:
6168 """Block on ``BLPOP`` and yield task JSON strings as they arrive."""
6269 while True :
63- result = await self ._redis .blpop (self ._queue_key )
70+ try :
71+ result = await self ._redis .blpop (self ._queue_key )
72+ except RedisTimeoutError :
73+ task = asyncio .current_task ()
74+ if task is not None and task .cancelling ():
75+ raise asyncio .CancelledError () from None
76+ continue
6477 if result is None :
6578 continue
6679 _ , raw = result
@@ -73,14 +86,34 @@ async def send_result(self, task_id: str, result_json: str) -> None:
7386
7487 async def get_result (self , task_id : str , timeout : float | None = None ) -> str :
7588 key = f"{ self .RESULT_PREFIX } { task_id } "
76- t = int (timeout ) if timeout else 0
77- result = await self ._redis .blpop (key , timeout = t )
78- if result is None :
79- raise TimeoutError (
80- f"Timed out waiting for result of task { task_id } "
81- )
82- _ , raw = result
83- return raw .decode () if isinstance (raw , bytes ) else raw
89+ deadline = None if timeout is None else time .monotonic () + max (0.0 , timeout )
90+ while True :
91+ if deadline is None :
92+ block_seconds = 0
93+ else :
94+ remaining = deadline - time .monotonic ()
95+ if remaining <= 0 :
96+ raise TimeoutError (
97+ f"Timed out waiting for result of task { task_id } "
98+ )
99+ block_seconds = max (1 , math .ceil (remaining ))
100+ try :
101+ result = await self ._redis .blpop (key , timeout = block_seconds )
102+ except RedisTimeoutError :
103+ task = asyncio .current_task ()
104+ if task is not None and task .cancelling ():
105+ raise asyncio .CancelledError () from None
106+ if deadline is not None and time .monotonic () >= deadline :
107+ raise TimeoutError (
108+ f"Timed out waiting for result of task { task_id } "
109+ ) from None
110+ continue
111+ if result is None :
112+ raise TimeoutError (
113+ f"Timed out waiting for result of task { task_id } "
114+ )
115+ _ , raw = result
116+ return raw .decode () if isinstance (raw , bytes ) else raw
84117
85118 async def try_get_result (self , task_id : str ) -> str | None :
86119 """Non-blocking ``LPOP``; returns ``None`` if not yet available."""
0 commit comments