22
33import json
44import logging
5- from typing import List , Optional , Tuple
5+ from functools import wraps
6+ from typing import Callable , List , Optional , Tuple
67from urllib .parse import parse_qs , urlencode , urlparse , urlunparse
78
89from pydantic import Field , field_validator
910from pydantic_settings import BaseSettings
1011from redis import asyncio as aioredis
1112from redis .asyncio .sentinel import Sentinel
13+ from redis .exceptions import ConnectionError as RedisConnectionError
14+ from redis .exceptions import TimeoutError as RedisTimeoutError
15+ from retry import retry # type: ignore
1216
1317logger = logging .getLogger (__name__ )
1418
@@ -143,65 +147,104 @@ def validate_self_link_ttl_standalone(cls, v: int) -> int:
143147 return v
144148
145149
150+ class RedisRetrySettings (BaseSettings ):
151+ """Configuration for Redis retry wrapper."""
152+
153+ redis_query_retries_num : int = Field (
154+ default = 3 , alias = "REDIS_QUERY_RETRIES_NUM" , gt = 0
155+ )
156+ redis_query_initial_delay : float = Field (
157+ default = 1.0 , alias = "REDIS_QUERY_INITIAL_DELAY" , gt = 0
158+ )
159+ redis_query_backoff : float = Field (default = 2.0 , alias = "REDIS_QUERY_BACKOFF" , gt = 1 )
160+
161+
146162# Configure only one Redis configuration
147163sentinel_settings = RedisSentinelSettings ()
148164standalone_settings = RedisSettings ()
165+ retry_settings = RedisRetrySettings ()
149166
150167
151- async def connect_redis () -> Optional [aioredis .Redis ]:
168+ def redis_retry (func : Callable ) -> Callable :
169+ """Wrap function in retry with back-off logic."""
170+
171+ @wraps (func )
172+ @retry (
173+ exceptions = (RedisConnectionError , RedisTimeoutError ),
174+ tries = retry_settings .redis_query_retries_num ,
175+ delay = retry_settings .redis_query_initial_delay ,
176+ backoff = retry_settings .redis_query_backoff ,
177+ logger = logger ,
178+ )
179+ async def wrapper (* args , ** kwargs ):
180+ return await func (* args , ** kwargs )
181+
182+ return wrapper
183+
184+
185+ @redis_retry
186+ async def _connect_redis_internal () -> Optional [aioredis .Redis ]:
152187 """Return a Redis connection Redis or Redis Sentinel."""
153- try :
154- if sentinel_settings .REDIS_SENTINEL_HOSTS :
155- sentinel_nodes = sentinel_settings .get_sentinel_nodes ()
156- sentinel = Sentinel (
157- sentinel_nodes ,
158- decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
159- )
188+ if sentinel_settings .REDIS_SENTINEL_HOSTS :
189+ sentinel_nodes = sentinel_settings .get_sentinel_nodes ()
190+ sentinel = Sentinel (
191+ sentinel_nodes ,
192+ decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
193+ )
160194
161- redis = sentinel .master_for (
162- service_name = sentinel_settings .REDIS_SENTINEL_MASTER_NAME ,
163- db = sentinel_settings .REDIS_DB ,
164- decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
165- retry_on_timeout = sentinel_settings .REDIS_RETRY_TIMEOUT ,
166- client_name = sentinel_settings .REDIS_CLIENT_NAME ,
167- max_connections = sentinel_settings .REDIS_MAX_CONNECTIONS ,
168- health_check_interval = sentinel_settings .REDIS_HEALTH_CHECK_INTERVAL ,
169- )
170- logger .info ("Connected to Redis Sentinel" )
195+ redis = sentinel .master_for (
196+ service_name = sentinel_settings .REDIS_SENTINEL_MASTER_NAME ,
197+ db = sentinel_settings .REDIS_DB ,
198+ decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
199+ retry_on_timeout = sentinel_settings .REDIS_RETRY_TIMEOUT ,
200+ client_name = sentinel_settings .REDIS_CLIENT_NAME ,
201+ max_connections = sentinel_settings .REDIS_MAX_CONNECTIONS ,
202+ health_check_interval = sentinel_settings .REDIS_HEALTH_CHECK_INTERVAL ,
203+ )
204+ logger .info ("Connected to Redis Sentinel" )
205+
206+ elif standalone_settings .REDIS_HOST :
207+ pool = aioredis .ConnectionPool (
208+ host = standalone_settings .REDIS_HOST ,
209+ port = standalone_settings .REDIS_PORT ,
210+ db = standalone_settings .REDIS_DB ,
211+ max_connections = standalone_settings .REDIS_MAX_CONNECTIONS ,
212+ decode_responses = standalone_settings .REDIS_DECODE_RESPONSES ,
213+ retry_on_timeout = standalone_settings .REDIS_RETRY_TIMEOUT ,
214+ health_check_interval = standalone_settings .REDIS_HEALTH_CHECK_INTERVAL ,
215+ )
216+ redis = aioredis .Redis (
217+ connection_pool = pool , client_name = standalone_settings .REDIS_CLIENT_NAME
218+ )
219+ logger .info ("Connected to Redis" )
220+ else :
221+ logger .warning ("No Redis configuration found" )
222+ return None
171223
172- elif standalone_settings .REDIS_HOST :
173- pool = aioredis .ConnectionPool (
174- host = standalone_settings .REDIS_HOST ,
175- port = standalone_settings .REDIS_PORT ,
176- db = standalone_settings .REDIS_DB ,
177- max_connections = standalone_settings .REDIS_MAX_CONNECTIONS ,
178- decode_responses = standalone_settings .REDIS_DECODE_RESPONSES ,
179- retry_on_timeout = standalone_settings .REDIS_RETRY_TIMEOUT ,
180- health_check_interval = standalone_settings .REDIS_HEALTH_CHECK_INTERVAL ,
181- )
182- redis = aioredis .Redis (
183- connection_pool = pool , client_name = standalone_settings .REDIS_CLIENT_NAME
184- )
185- logger .info ("Connected to Redis" )
186- else :
187- logger .warning ("No Redis configuration found" )
188- return None
224+ return redis
189225
190- return redis
191226
227+ async def connect_redis () -> Optional [aioredis .Redis ]:
228+ """Handle Redis connection."""
229+ try :
230+ return await _connect_redis_internal ()
231+ except (
232+ aioredis .ConnectionError ,
233+ aioredis .TimeoutError ,
234+ ) as e :
235+ logger .error (f"Redis connection failed after retries: { e } " )
192236 except aioredis .ConnectionError as e :
193237 logger .error (f"Redis connection error: { e } " )
194238 return None
195239 except aioredis .AuthenticationError as e :
196240 logger .error (f"Redis authentication error: { e } " )
197241 return None
198- except aioredis .TimeoutError as e :
199- logger .error (f"Redis timeout error: { e } " )
200- return None
201242 except Exception as e :
202243 logger .error (f"Failed to connect to Redis: { e } " )
203244 return None
204245
246+ return None
247+
205248
206249def get_redis_key (url : str , token : str ) -> str :
207250 """Create Redis key using URL path and token."""
@@ -230,6 +273,7 @@ def build_url_with_token(base_url: str, token: str) -> str:
230273 )
231274
232275
276+ @redis_retry
233277async def save_prev_link (
234278 redis : aioredis .Redis , next_url : str , current_url : str , next_token : str
235279) -> None :
@@ -243,6 +287,7 @@ async def save_prev_link(
243287 await redis .setex (key , ttl_seconds , current_url )
244288
245289
290+ @redis_retry
246291async def get_prev_link (
247292 redis : aioredis .Redis , current_url : str , current_token : str
248293) -> Optional [str ]:
0 commit comments