22
33import json
44import logging
5+ from contextlib import asynccontextmanager
56from typing import List , Optional , Tuple
67
78from pydantic import field_validator
@@ -30,7 +31,7 @@ class RedisSentinelSettings(BaseSettings):
3031 @field_validator ("REDIS_DB" )
3132 @classmethod
3233 def validate_db_sentinel (cls , v : int ) -> int :
33- """Validate REDIS_DB is not negative int ."""
34+ """Validate REDIS_DB is not negative integer ."""
3435 if v < 0 :
3536 raise ValueError ("REDIS_DB must be a positive integer" )
3637 return v
@@ -163,11 +164,25 @@ def validate_self_link_ttl_standalone(cls, v: int) -> int:
163164standalone_settings = RedisSettings ()
164165
165166
166- async def connect_redis () -> Optional [aioredis .Redis ]:
167- """Return a Redis connection Redis or Redis Sentinel."""
167+ @asynccontextmanager
168+ async def connect_redis ():
169+ """Create a Redis connection with proper error handling for tests."""
170+ redis = None
168171 try :
172+ if not (
173+ sentinel_settings .REDIS_SENTINEL_HOSTS or standalone_settings .REDIS_HOST
174+ ):
175+ logger .debug ("Redis not configured, skipping connection" )
176+ yield None
177+ return
178+
169179 if sentinel_settings .REDIS_SENTINEL_HOSTS :
170180 sentinel_nodes = sentinel_settings .get_sentinel_nodes ()
181+ if not sentinel_nodes :
182+ logger .warning ("No Redis Sentinel nodes configured" )
183+ yield None
184+ return
185+
171186 sentinel = Sentinel (
172187 sentinel_nodes ,
173188 decode_responses = sentinel_settings .REDIS_DECODE_RESPONSES ,
@@ -185,83 +200,85 @@ async def connect_redis() -> Optional[aioredis.Redis]:
185200 logger .info ("Connected to Redis Sentinel" )
186201
187202 elif standalone_settings .REDIS_HOST :
188- pool = aioredis .ConnectionPool (
203+ redis = aioredis .Redis (
189204 host = standalone_settings .REDIS_HOST ,
190205 port = standalone_settings .REDIS_PORT ,
191206 db = standalone_settings .REDIS_DB ,
192- max_connections = standalone_settings .REDIS_MAX_CONNECTIONS ,
193207 decode_responses = standalone_settings .REDIS_DECODE_RESPONSES ,
194208 retry_on_timeout = standalone_settings .REDIS_RETRY_TIMEOUT ,
209+ client_name = standalone_settings .REDIS_CLIENT_NAME ,
195210 health_check_interval = standalone_settings .REDIS_HEALTH_CHECK_INTERVAL ,
211+ socket_connect_timeout = 5.0 , # Short timeout for tests
212+ socket_timeout = 5.0 ,
196213 )
197- redis = aioredis .Redis (
198- connection_pool = pool , client_name = standalone_settings .REDIS_CLIENT_NAME
199- )
200- logger .info ("Connected to Redis" )
201- else :
202- logger .warning ("No Redis configuration found" )
203- return None
204214
205- return redis
215+ await redis .ping ()
216+ logger .debug ("Connected to Redis" )
217+
218+ yield redis
206219
207- except aioredis .ConnectionError as e :
208- logger .error (f"Redis connection error: { e } " )
209- return None
210- except aioredis .AuthenticationError as e :
211- logger .error (f"Redis authentication error: { e } " )
212- return None
213- except aioredis .TimeoutError as e :
214- logger .error (f"Redis timeout error: { e } " )
215- return None
216220 except Exception as e :
217- logger .error (f"Failed to connect to Redis: { e } " )
218- return None
221+ logger .debug (f"Redis connection failed (this is normal in tests): { e } " )
222+ yield None
223+ finally :
224+ if redis :
225+ await redis .close ()
226+ logger .debug ("Redis connection closed" )
219227
220228
221229async def save_self_link (
222230 redis : aioredis .Redis , token : Optional [str ], self_href : str
223231) -> None :
224232 """Save the self link for the current token."""
225- if token :
233+ if redis and token :
226234 if sentinel_settings .REDIS_SENTINEL_HOSTS :
227235 ttl_seconds = sentinel_settings .REDIS_SELF_LINK_TTL
228236 elif standalone_settings .REDIS_HOST :
229237 ttl_seconds = standalone_settings .REDIS_SELF_LINK_TTL
230- await redis .setex (f"nav:self:{ token } " , ttl_seconds , self_href )
238+ else :
239+ return
240+
241+ try :
242+ await redis .setex (f"nav:self:{ token } " , ttl_seconds , self_href )
243+ except Exception as e :
244+ logger .debug (f"Failed to save self link: { e } " )
231245
232246
233247async def get_prev_link (redis : aioredis .Redis , token : Optional [str ]) -> Optional [str ]:
234248 """Get the previous page link for the current token (if exists)."""
235- if not token :
249+ if not redis or not token :
250+ return None
251+
252+ try :
253+ return await redis .get (f"nav:self:{ token } " )
254+ except Exception as e :
255+ logger .debug (f"Failed to get prev link: { e } " )
236256 return None
237- return await redis .get (f"nav:self:{ token } " )
238257
239258
240259async def redis_pagination_links (
241260 current_url : str , token : str , next_token : str , links : list
242261) -> None :
243262 """Handle Redis pagination."""
244- redis = await connect_redis ()
245- if not redis :
246- logger .warning ("Redis connection failed." )
247- return
248-
249- try :
250- if next_token :
251- await save_self_link (redis , next_token , current_url )
252-
253- prev_link = await get_prev_link (redis , token )
254- if prev_link :
255- links .insert (
256- 0 ,
257- {
258- "rel" : "prev" ,
259- "type" : "application/json" ,
260- "method" : "GET" ,
261- "href" : prev_link ,
262- },
263- )
264- except Exception as e :
265- logger .warning (f"Redis pagination operation failed: { e } " )
266- finally :
267- await redis .close ()
263+ async with connect_redis () as redis :
264+ if not redis :
265+ logger .warning ("Redis connection failed." )
266+ return
267+
268+ try :
269+ if next_token :
270+ await save_self_link (redis , next_token , current_url )
271+
272+ prev_link = await get_prev_link (redis , token )
273+ if prev_link :
274+ links .insert (
275+ 0 ,
276+ {
277+ "rel" : "prev" ,
278+ "type" : "application/json" ,
279+ "method" : "GET" ,
280+ "href" : prev_link ,
281+ },
282+ )
283+ except Exception as e :
284+ logger .warning (f"Redis pagination operation failed: { e } " )
0 commit comments