Skip to content

fix(proxy): self-heal Prisma connection for auth and runtime - #21706

Merged
ishaan-jaff merged 22 commits into
mainfrom
ishaan-jaffer/prisma-reconnect-fix
Feb 21, 2026
Merged

fix(proxy): self-heal Prisma connection for auth and runtime#21706
ishaan-jaff merged 22 commits into
mainfrom
ishaan-jaffer/prisma-reconnect-fix

Conversation

@ishaan-jaff

@ishaan-jaff ishaan-jaff commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds Prisma self-healing for the outage pattern where DB connectivity drops, network recovers, but LiteLLM continues failing auth lookups until process restart.

Changes

  • Add Prisma reconnect primitive with cooldown + singleflight lock in PrismaClient
  • Add background DB health watcher task (start on startup, stop on shutdown). If it looses connection, it will self heal / reconnect
  • Add auth / critical path reconnect-once behavior in get_key_object for DB-connection-class errors

Why

During transient network failures, Prisma can remain in a degraded connection state. Before this, there was no proactive runtime reconnect loop, and auth DB lookups could continue failing after network recovery.

E2E: Live Proxy + Postgres

Scenario Endpoint Observed Status Notes
DB up GET /key/info 200 Healthy baseline
DB down GET /key/info 500 DB unavailable
Recovered GET /key/info 200 Recovered in ~14s
DB up (virtual key auth) GET /v1/models 200 Healthy baseline
DB down (virtual key auth) GET /v1/models 401 Auth lookup fails while DB down
Recovered (virtual key auth) GET /v1/models 200 Recovered in ~15s
Readiness while DB up/down/recovered GET /health/readiness 200 Reported db=connected in all sampled states

Live log proof captured on this branch

Source: .context/e2e_compare/after_pr_proof_lines.txt

386: ... "GET /key/info?..." 500 Internal Server Error
387: ... Attempting Prisma DB reconnect. reason=auth_get_key_object_lookup_failure
459: ... "GET /v1/models HTTP/1.1" 401 Unauthorized
765: ... Attempting Prisma DB reconnect. reason=db_health_watchdog_connection_error
767: ... Prisma DB reconnect succeeded. reason=db_health_watchdog_connection_error
769: ... "GET /key/info?..." 200 OK
770: ... "GET /v1/models HTTP/1.1" 200 OK

@vercel

vercel Bot commented Feb 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 21, 2026 2:05am

Request Review

@greptile-apps

greptile-apps Bot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Prisma DB self-healing to address an outage pattern where DB connectivity drops and recovers, but the Prisma client remains in a degraded state until process restart. The implementation includes three key components:

  • Reconnect primitive (attempt_db_reconnect): A singleflight reconnect operation with cooldown enforcement, lock-based concurrency control, and configurable timeout budgets. Uses asyncio.wait instead of asyncio.wait_for to avoid a known lock-leak race on Python 3.9-3.11.
  • Background health watchdog (_db_health_watchdog_loop): Probes DB connectivity at a configurable interval (default 30s) and triggers reconnect on connection failures or probe timeouts. Started on proxy startup, stopped on shutdown.
  • Auth-path reconnect-once (_fetch_key_object_from_db_with_reconnect): On connection error during key lookup in get_key_object, attempts a single reconnect (with a tight 2s timeout and 0.1s lock wait) before retrying the query.
  • Narrowed is_database_connection_error: Previously, all PrismaError subtypes (including DataError, UniqueViolationError, RecordNotFoundError) were classified as connection errors. Now only truly connection-related errors are matched — this is a correctness fix that prevents allow_requests_on_db_unavailable from silently swallowing non-connection DB errors.

All new functionality is well-tested with mock-only unit tests covering success/failure paths, cooldown behavior, lock timeout races, timeout budgets, and watchdog lifecycle. E2E proof is provided in the PR description.

Confidence Score: 4/5

  • This PR is safe to merge with low risk — the reconnect paths are well-bounded by timeouts, the behavioral change in error classification is a correctness improvement, and the new code has comprehensive test coverage.
  • Score of 4 reflects: (1) well-designed reconnect primitive with proper concurrency control, cooldown, and timeout bounds; (2) comprehensive mock-only test coverage; (3) the narrowing of is_database_connection_error is a correctness fix with positive impact on allow_requests_on_db_unavailable behavior; (4) E2E proof provided. Minor deduction for the inherent complexity of async lock/timeout race handling, and the auth-path reconnect still adds up to ~4s latency in worst case (2s connect + 2s query, as noted in previous threads).
  • litellm/proxy/utils.py deserves the most attention given the complexity of the lock acquisition race condition handling and the reconnect cycle logic. litellm/proxy/db/exception_handler.py contains a behavioral change (narrowed error classification) that affects existing callers of is_database_connection_error.

Important Files Changed

Filename Overview
litellm/proxy/db/exception_handler.py Narrows is_database_connection_error to only match actual connection-related Prisma errors (keyword-based matching for generic PrismaError, plus explicit checks for ClientNotConnectedError and HTTPClientClosedError). This is a correctness improvement - previously all PrismaError subtypes (including DataError, UniqueViolationError, etc.) were incorrectly classified as connection errors.
litellm/proxy/utils.py Adds Prisma self-healing infrastructure to PrismaClient: reconnect primitive with cooldown + singleflight lock, background DB health watchdog loop, and extensive configuration via env vars. Lock acquisition uses asyncio.wait with defensive race-condition cleanup for Python 3.9-3.11 compatibility. All reconnect paths have explicit timeout bounds.
litellm/proxy/auth/auth_checks.py Adds _fetch_key_object_from_db_with_reconnect helper that wraps the DB lookup in get_key_object with a single reconnect-on-connection-error retry. Uses short lock/reconnect timeouts (0.1s/2.0s) to minimize auth path latency impact.
litellm/proxy/proxy_server.py Adds watchdog lifecycle hooks: starts the health watchdog task after Prisma client initialization and stops it during shutdown. Changes are minimal and defensive (hasattr guards). Import sorting is cosmetic.
tests/test_litellm/proxy/auth/test_auth_checks.py Adds two mock-only tests for the auth reconnect path: one verifying successful reconnect-and-retry, one verifying error propagation when reconnect fails. No real network calls.
tests/test_litellm/proxy/db/test_exception_handler.py Updates tests to reflect the narrowed is_database_connection_error behavior: connection-specific Prisma errors are now tested separately from non-connection Prisma errors, with the latter now expected to return False.
tests/test_litellm/proxy/db/test_prisma_self_heal.py New comprehensive test file covering reconnect success, cooldown skip, lock timeout, lock-leak race condition, cooldown timestamp placement, direct DB ops usage, timeout budgets, watchdog reconnect triggers (DB error and probe timeout), and start/stop lifecycle. All mock-only.

Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthChecks as auth_checks.py
    participant PrismaClient as PrismaClient
    participant Watchdog as Health Watchdog
    participant DB as Prisma DB

    Note over Watchdog,DB: Background health probe (every 30s)
    loop Every _db_health_watchdog_interval_seconds
        Watchdog->>DB: query_raw("SELECT 1")
        alt DB healthy
            DB-->>Watchdog: OK
        else DB connection error or timeout
            Watchdog->>PrismaClient: attempt_db_reconnect(reason=watchdog)
            PrismaClient->>DB: disconnect()
            PrismaClient->>DB: connect()
            PrismaClient->>DB: query_raw("SELECT 1")
            DB-->>PrismaClient: OK / error
        end
    end

    Note over Client,DB: Auth request path
    Client->>AuthChecks: get_key_object(hashed_token)
    AuthChecks->>PrismaClient: get_data(token, combined_view)
    alt DB lookup succeeds
        PrismaClient-->>AuthChecks: key object
    else DB connection error
        PrismaClient-->>AuthChecks: connection error
        AuthChecks->>PrismaClient: attempt_db_reconnect(reason=auth, timeout=2s, lock_timeout=0.1s)
        alt Reconnect succeeds
            PrismaClient-->>AuthChecks: True
            AuthChecks->>PrismaClient: get_data(token, combined_view) [retry]
            PrismaClient-->>AuthChecks: key object
        else Reconnect fails or skipped (cooldown/lock)
            PrismaClient-->>AuthChecks: False
            AuthChecks-->>Client: raise original error
        end
    end
Loading

Last reviewed commit: 8b8c58a

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread litellm/proxy/utils.py Outdated
Comment on lines +3593 to +3606
try:
await self.connect()
await self.health_check()
verbose_proxy_logger.info(
"Prisma DB reconnect succeeded. reason=%s", reason
)
return True
except Exception as reconnect_err:
verbose_proxy_logger.error(
"Prisma DB reconnect failed. reason=%s error=%s",
reason,
reconnect_err,
)
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconnect on auth path can block ~30s

attempt_db_reconnect calls disconnect(), connect(), and health_check() — each decorated with @backoff.on_exception(max_tries=3, max_time=10). In the worst case (DB still unreachable), a single auth request triggering this path could block for up to ~30 seconds before returning a failure. Meanwhile, other concurrent auth requests will queue on _db_reconnect_lock.

Consider either:

  1. Using shorter timeouts/fewer retries when called from the auth path (e.g. a timeout parameter), or
  2. Letting only the watchdog perform full reconnects and having the auth path just surface the error immediately (relying on the watchdog to heal in the background).

Context Used: Rule from dashboard - What: Avoid creating new database requests or Router objects in the critical request path.

Why: Cre... (source)

Comment thread litellm/proxy/utils.py Outdated
)
return False

self._db_last_reconnect_attempt_ts = now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cooldown starts before reconnect completes

_db_last_reconnect_attempt_ts is set at the start of the reconnect attempt. If the reconnect takes, say, 20 seconds and fails, the 15-second default cooldown has already elapsed by the time the failure returns — meaning the next request will immediately trigger another reconnect attempt. Consider updating the timestamp after the reconnect attempt completes (on both success and failure paths) to ensure the full cooldown elapses between attempts.

Suggested change
self._db_last_reconnect_attempt_ts = now
self._db_last_reconnect_attempt_ts = now # will be updated again after attempt

Comment thread litellm/proxy/auth/auth_checks.py Outdated
proxy_logging_obj=proxy_logging_obj,
)
except Exception as e:
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline import in hot auth path

The PrismaDBExceptionHandler import is placed inside the except block. Per the project's code style guidelines, imports should be at the top of the file. There's no circular import risk here since litellm.proxy.db.exception_handler doesn't import from auth_checks. Moving this to the top-level imports would be consistent with how auth_exception_handler.py imports the same class.

Suggested change
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler # TODO: move to top-level imports

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

Addressed review feedback:\n- Removed inline import in \n- Added module-level import\n- Extracted DB reconnect/retry logic into helper \n- Re-ran reconnect auth tests:

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

Addressed review feedback:

  • Removed inline import in get_key_object
  • Added module-level PrismaDBExceptionHandler import
  • Extracted DB reconnect/retry logic into helper _fetch_key_object_from_db_with_reconnect
  • Re-ran reconnect auth tests: 2 passed

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

Addressed Greptile feedback on this PR:

  1. Cooldown timing fix (litellm/proxy/utils.py)
  • Moved reconnect cooldown timestamp update to after reconnect attempt completion (success/failure), so cooldown is measured between completed attempts.
  1. Auth hot-path latency reduction (litellm/proxy/utils.py, litellm/proxy/auth/auth_checks.py)
  • Added optional timeout_seconds to attempt_db_reconnect(...).
  • Added fast reconnect cycle path (direct db.connect + SELECT 1 under timeout) for auth-triggered reconnects.
  • Auth key lookup reconnect now uses bounded timeout (_db_auth_reconnect_timeout_seconds, default 2.0s).
  1. Tests updated/added
  • tests/test_litellm/proxy/db/test_prisma_self_heal.py:
    • Added test to verify cooldown timestamp is set after reconnect attempt completes.
  • tests/test_litellm/proxy/auth/test_auth_checks.py:
    • Updated reconnect assertions to verify timeout argument is passed.

Validation run:

  • pytest -q tests/test_litellm/proxy/db/test_prisma_self_heal.py tests/test_litellm/proxy/auth/test_auth_checks.py -k "reconnect_once_on_db_connection_error or reconnect_fails_on_db_connection_error or attempt_db_reconnect_should_succeed or attempt_db_reconnect_should_skip_when_in_cooldown or attempt_db_reconnect_should_set_cooldown_after_attempt or db_health_watchdog_should_trigger_reconnect_on_db_error or db_health_watchdog_start_stop_lifecycle"
  • Result: 7 passed

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

Validated the Greptile 3/5 items against current PR head ():\n\n1. Auth-path reconnect latency: addressed via bounded auth reconnect timeout () + fast reconnect cycle path () with default .\n2. Cooldown timing: addressed by updating in after reconnect attempt completes.\n3. Inline import in auth hot path: addressed with top-level import and helper extraction ().\n\nRe-ran targeted tests on current branch:\n- \n\nCommand:\n....... [100%]
=============================== warnings summary ===============================
../../../../Library/Caches/pypoetry/virtualenvs/litellm-Ipb51NnJ-py3.11/lib/python3.11/site-packages/_pytest/config/init.py:1373
/Users/ishaanjaffer/Library/Caches/pypoetry/virtualenvs/litellm-Ipb51NnJ-py3.11/lib/python3.11/site-packages/_pytest/config/init.py:1373: PytestConfigWarning: Unknown config option: asyncio_default_fixture_loop_scope

self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
7 passed, 48 deselected, 1 warning in 2.18s\n\nGreptile summary appears based on older reviewed commit ().

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

Validated the Greptile 3/5 items against current PR head (67809b8981):

  1. Auth-path reconnect latency: addressed via bounded auth reconnect timeout (timeout_seconds) + fast reconnect cycle path (db.connect + SELECT 1) with default PRISMA_AUTH_RECONNECT_TIMEOUT_SECONDS=2.0.
  2. Cooldown timing: addressed by updating _db_last_reconnect_attempt_ts in finally after reconnect attempt completes.
  3. Inline import in auth hot path: addressed with top-level PrismaDBExceptionHandler import and helper extraction (_fetch_key_object_from_db_with_reconnect).

Re-ran targeted tests on current branch:

  • 7 passed, 48 deselected

Command:
poetry run pytest -q tests/test_litellm/proxy/db/test_prisma_self_heal.py tests/test_litellm/proxy/auth/test_auth_checks.py -k "reconnect_once_on_db_connection_error or reconnect_fails_on_db_connection_error or attempt_db_reconnect_should_succeed or attempt_db_reconnect_should_skip_when_in_cooldown or attempt_db_reconnect_should_set_cooldown_after_attempt or db_health_watchdog_should_trigger_reconnect_on_db_error or db_health_watchdog_start_stop_lifecycle"

Greptile summary appears based on older reviewed commit (b560164).

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

@greptile-apps please re-review this PR on latest head 67809b8981ba142b5b2d782c64b8ea33b9e8bdb9.

The previously flagged points (auth reconnect latency bound, cooldown timestamp timing, and inline import in auth path) have been addressed and validated with targeted tests.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread litellm/proxy/utils.py Outdated
Comment on lines +3558 to +3568
if timeout_seconds is None:
try:
await self.disconnect()
except Exception as disconnect_err:
verbose_proxy_logger.debug(
"Prisma DB disconnect before reconnect failed (ignored): %s",
disconnect_err,
)
await self.connect()
await self.health_check()
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watchdog reconnect may silently skip connect() after failed disconnect()

When timeout_seconds is None (the watchdog path), if self.disconnect() raises an exception (caught and ignored on line 3561-3565), the Prisma client's internal is_connected() flag may still be True. Since self.connect() at line 3566 guards with if self.db.is_connected() is False, it would silently skip the actual reconnection attempt. The subsequent health_check() would then run against the same stale connection.

The timeout path (lines 3570-3579) correctly bypasses this by calling self.db.disconnect() / self.db.connect() directly. Consider applying the same approach for the watchdog path — calling self.db.disconnect() and self.db.connect() directly rather than going through the backoff-decorated wrappers that have the is_connected() guard.

Comment thread litellm/proxy/utils.py Outdated
Comment on lines +3578 to +3579
await asyncio.wait_for(self.db.connect(), timeout=timeout_seconds)
await asyncio.wait_for(self.db.query_raw("SELECT 1"), timeout=timeout_seconds)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth reconnect timeout applies per-step, actual max is 2x

timeout_seconds is applied to both self.db.connect() and self.db.query_raw("SELECT 1") independently. With the default auth reconnect timeout of 2.0s, the total reconnect cycle can block the auth request for up to 4 seconds (2s connect + 2s query), not 2s as the config name suggests. Consider either documenting this or wrapping both steps in a single overall timeout.

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

@greptile-apps please re-review latest head 1144b7fe1a.

Addressed the two latest comments on 67809b8:

  1. Watchdog reconnect now uses direct db.disconnect/db.connect/query_raw to avoid wrapper is_connected() no-op risk.
  2. Auth reconnect timeout now applies as a single overall budget for the full reconnect cycle (not per-step).

Validation:
poetry run pytest -q tests/test_litellm/proxy/db/test_prisma_self_heal.py tests/test_litellm/proxy/auth/test_auth_checks.py
Result: 57 passed.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread litellm/proxy/utils.py Outdated
Comment on lines +3571 to +3573
if timeout_seconds is None:
await _do_direct_reconnect()
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watchdog reconnect path has no timeout bound

When timeout_seconds is None (the watchdog path), _do_direct_reconnect() is called without any asyncio.wait_for wrapper. If self.db.connect() or self.db.query_raw("SELECT 1") hangs (e.g., TCP-level timeout waiting for an unreachable host), attempt_db_reconnect will hold _db_reconnect_lock indefinitely. While the lock is held, auth-path reconnect attempts will also block waiting on it, effectively stalling auth lookups.

Consider applying a generous upper-bound timeout for the watchdog path as well, e.g.:

Suggested change
if timeout_seconds is None:
await _do_direct_reconnect()
return
if timeout_seconds is None:
await asyncio.wait_for(_do_direct_reconnect(), timeout=30.0)
return

Context Used: Rule from dashboard - What: Avoid creating new database requests or Router objects in the critical request path.

Why: Cre... (source)

Comment thread litellm/proxy/utils.py
Comment on lines +3671 to +3688
async def _db_health_watchdog_loop(self) -> None:
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler

while True:
try:
await asyncio.sleep(self._db_health_watchdog_interval_seconds)
await self.health_check()
except asyncio.CancelledError:
break
except Exception as e:
if PrismaDBExceptionHandler.is_database_connection_error(e):
await self.attempt_db_reconnect(
reason="db_health_watchdog_connection_error"
)
else:
verbose_proxy_logger.debug(
"Prisma DB health watchdog observed non-DB error: %s", e
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watchdog health_check uses backoff retries before reaching reconnect

health_check() is decorated with @backoff.on_exception(max_tries=3, max_time=10), so each watchdog iteration will retry the health check with exponential backoff for up to 10 seconds before the exception propagates here. Combined with the unbounded attempt_db_reconnect call, a single watchdog cycle when the DB is down could take 10s (health_check retries) + unbounded time (reconnect with no timeout) before the loop can sleep and try again.

Consider calling self.db.query_raw("SELECT 1") directly instead of self.health_check() to avoid the backoff retries in the watchdog loop, since the watchdog already has its own retry-via-sleep cadence.

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@ishaan-jaff

ishaan-jaff commented Feb 21, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by the next comment with full details (formatting issue in this comment body).

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

@greptile-apps please re-review latest head b53c34280a.

Addressed the remaining 4/5 summary deductions:

  1. Auth reconnect latency under contention
  • Added lock wait budget for auth reconnect attempts:
    • new config: PRISMA_AUTH_RECONNECT_LOCK_TIMEOUT_SECONDS (default 0.1)
  • attempt_db_reconnect(...) now accepts lock_timeout_seconds; auth path passes this so requests fail fast if reconnect lock is busy instead of queueing behind long reconnects.
  1. Over-broad Prisma connection error classification
  • PrismaDBExceptionHandler.is_database_connection_error(...) now treats Prisma errors as connection errors only when:
    • specific transport/connection classes (ClientNotConnectedError, HTTPClientClosedError), or
    • generic PrismaError with clear connection/network keywords.
  • Non-connection Prisma errors (e.g. constraint/schema/query validation classes) are no longer treated as DB connection outages.

Validation:

  • poetry run pytest -q tests/test_litellm/proxy/db/test_exception_handler.py tests/test_litellm/proxy/db/test_prisma_self_heal.py tests/test_litellm/proxy/auth/test_auth_checks.py
  • Result: 80 passed

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread litellm/proxy/utils.py Outdated
Comment on lines +3654 to +3658
try:
await asyncio.wait_for(
self._db_reconnect_lock.acquire(),
timeout=lock_timeout_seconds,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait_for(lock.acquire()) can leak the lock on timeout

On Python 3.9–3.11, asyncio.wait_for(lock.acquire(), timeout=...) has a known race condition: if the lock becomes available at the exact moment the timeout fires, the lock is acquired but TimeoutError is still raised. Since the lock is never released, all subsequent callers will deadlock on it permanently.

This was fixed in CPython 3.12 (bpo-98048), but since litellm supports Python ≥3.9, this path is vulnerable.

A safer pattern is to use asyncio.wait_for with a wrapper that releases on cancellation, or use a manual approach:

Suggested change
try:
await asyncio.wait_for(
self._db_reconnect_lock.acquire(),
timeout=lock_timeout_seconds,
)
result = asyncio.get_event_loop().create_future()
async def _acquire_with_timeout():
await self._db_reconnect_lock.acquire()
if not result.done():
result.set_result(True)
task = asyncio.ensure_future(_acquire_with_timeout())
try:
await asyncio.wait_for(asyncio.shield(task), timeout=lock_timeout_seconds)
except asyncio.TimeoutError:
task.cancel()
# If the lock was acquired between timeout and cancel, release it
if self._db_reconnect_lock.locked():
try:
self._db_reconnect_lock.release()
except RuntimeError:
pass

Alternatively, a simpler approach: use a asyncio.wait_for only on Python 3.12+ and fall back to a manual timer on older versions, or just use asyncio.timeout() (3.11+).

Context Used: Rule from dashboard - What: Avoid creating new database requests or Router objects in the critical request path.

Why: Cre... (source)

@ishaan-jaff

Copy link
Copy Markdown
Contributor Author

@greptile-apps please re-review latest head 8b8c58aab0.

Addressed latest comment about wait_for(lock.acquire()) lock leak race on Python 3.9-3.11:

  • Replaced await asyncio.wait_for(self._db_reconnect_lock.acquire(), timeout=...) with a safer acquire-task + asyncio.wait(..., timeout=...) pattern.
  • Added explicit timeout-race cleanup: if timeout/cancel race still acquires lock, we defensively release it.
  • Kept lock-timeout behavior (auth path can fail fast instead of waiting behind long reconnects) while preventing permanent deadlock risk.

Tests:

  • Added regression test: test_attempt_db_reconnect_should_not_leak_lock_on_timeout_race
  • Ran:
    poetry run pytest -q tests/test_litellm/proxy/db/test_prisma_self_heal.py tests/test_litellm/proxy/auth/test_auth_checks.py tests/test_litellm/proxy/db/test_exception_handler.py
  • Result: 81 passed

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@ishaan-jaff
ishaan-jaff merged commit e012971 into main Feb 21, 2026
12 of 34 checks passed
ishaan-jaff added a commit that referenced this pull request Feb 21, 2026
…ore broad is_database_connection_error

#21706 narrowed is_database_connection_error to only match PrismaErrors
with connectivity keywords. That was correct for the reconnect use case
but broke the allow_requests_on_db_unavailable behavior as a side effect.

Fix: add is_database_transport_error with the narrow keyword-gated check
and use it in the two reconnect call sites (auth_checks key lookup retry,
utils.py watchdog). Restore is_database_connection_error to its original
broad behavior (any PrismaError) so allow_requests_on_db_unavailable works
correctly.

Update the tests added in #21706 to assert against is_database_transport_error
instead of is_database_connection_error.
ishaan-jaff added a commit that referenced this pull request Feb 21, 2026
…se_transport_error for reconnect

Any PrismaError should be treated as a DB connection error for the
allow_requests_on_db_unavailable feature and 503 responses. The narrow
keyword-based check is now in is_database_transport_error, which is
what the reconnect logic in auth_checks.py should use.

Fixes test_delete_access_group_503_on_db_connection_error and
test_handle_authentication_error_db_unavailable failures caused by
PR #21706 narrowing is_database_connection_error.
ishaan-jaff added a commit that referenced this pull request Feb 21, 2026
…se_transport_error for reconnect

Any PrismaError should be treated as a DB connection error for the
allow_requests_on_db_unavailable feature and 503 responses. The narrow
keyword-based check is now in is_database_transport_error, which is
what the reconnect logic in auth_checks.py should use.

Fixes test_delete_access_group_503_on_db_connection_error and
test_handle_authentication_error_db_unavailable failures caused by
PR #21706 narrowing is_database_connection_error.
ishaan-jaff added a commit that referenced this pull request Feb 21, 2026
…se_transport_error for reconnect (#21796)

Any PrismaError should be treated as a DB connection error for the
allow_requests_on_db_unavailable feature and 503 responses. The narrow
keyword-based check is now in is_database_transport_error, which is
what the reconnect logic in auth_checks.py should use.

Fixes test_delete_access_group_503_on_db_connection_error and
test_handle_authentication_error_db_unavailable failures caused by
PR #21706 narrowing is_database_connection_error.
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…#21706)

* fix(proxy): add prisma reconnect primitive and db watchdog

* fix(proxy): start and stop prisma watchdog in lifecycle

* fix(auth): retry key lookup once after prisma reconnect

* test(proxy): add prisma self-heal watchdog coverage

* test(auth): cover reconnect-once behavior for key lookup

* refactor(auth): extract db reconnect helper and remove inline import

* fix(proxy): apply reconnect cooldown after attempt and add auth timeout path

* fix(auth): bound reconnect latency on key lookup path

* test(auth): assert reconnect timeout argument in key lookup

* test(proxy): verify reconnect cooldown timestamp set after attempt

* fix(proxy): harden prisma reconnect cycle semantics

* test(proxy): cover watchdog reconnect + timeout budget

* fix(proxy): bound watchdog probe and reconnect paths

* test(proxy): cover watchdog timeout and probe behavior

* fix(proxy): narrow prisma db connection error classification

* fix(proxy): add auth reconnect lock timeout budget

* fix(auth): pass lock timeout for db reconnect retries

* test(proxy): cover narrow prisma connection error detection

* test(proxy): add reconnect lock-timeout behavior coverage

* test(auth): assert reconnect lock timeout argument

* fix(proxy): avoid lock leak race in reconnect lock timeout path

* test(proxy): cover reconnect lock-timeout race cleanup
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…se_transport_error for reconnect (BerriAI#21796)

Any PrismaError should be treated as a DB connection error for the
allow_requests_on_db_unavailable feature and 503 responses. The narrow
keyword-based check is now in is_database_transport_error, which is
what the reconnect logic in auth_checks.py should use.

Fixes test_delete_access_group_503_on_db_connection_error and
test_handle_authentication_error_db_unavailable failures caused by
PR BerriAI#21706 narrowing is_database_connection_error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant