From aa716959156e51f9e1cc3f45497bebd2f9df949f Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 20 Feb 2026 18:04:01 -0800 Subject: [PATCH] fix(proxy): avoid lock leak race in reconnect lock timeout path --- litellm/proxy/utils.py | 45 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 840d5dfe4a6..8b39eb8c495 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -3651,12 +3651,35 @@ class PrismaClient: async with self._db_reconnect_lock: return await _attempt_reconnect_inside_lock() - try: - await asyncio.wait_for( - self._db_reconnect_lock.acquire(), - timeout=lock_timeout_seconds, - ) - except asyncio.TimeoutError: + lock_acquired_by_timeout_task = False + + async def _acquire_reconnect_lock() -> bool: + nonlocal lock_acquired_by_timeout_task + await self._db_reconnect_lock.acquire() + lock_acquired_by_timeout_task = True + return True + + acquire_task = asyncio.create_task(_acquire_reconnect_lock()) + done, _pending = await asyncio.wait( + {acquire_task}, + timeout=lock_timeout_seconds, + return_when=asyncio.FIRST_COMPLETED, + ) + if acquire_task not in done: + acquire_task.cancel() + try: + await acquire_task + except asyncio.CancelledError: + pass + except Exception: + pass + + # Defensive cleanup for timeout/cancel race on Python 3.9-3.11. + if lock_acquired_by_timeout_task: + try: + self._db_reconnect_lock.release() + except RuntimeError: + pass verbose_proxy_logger.debug( "Skipping DB reconnect attempt due to lock acquisition timeout. reason=%s timeout=%ss", reason, @@ -3664,6 +3687,16 @@ class PrismaClient: ) return False + try: + acquire_task.result() + except Exception as lock_acquire_err: + verbose_proxy_logger.debug( + "Skipping DB reconnect attempt due to lock acquisition error. reason=%s error=%s", + reason, + lock_acquire_err, + ) + return False + try: return await _attempt_reconnect_inside_lock() finally: