fix(proxy): self-heal Prisma read paths + harden reconnect state machine - #26756
Conversation
Two related fixes layered on top of the existing reconnect plumbing: 1. Restore reconnect-and-retry on `PrismaClient.get_generic_data` (issue #25143). 1.83.x lost the transport-reconnect-and-retry-once branch that 1.82.6 had on this method, so transient `httpx.ReadError` flaps now surface immediately as `db_exceptions` alerts. `_update_config_from_db` fans out four concurrent `get_generic_data` reads, so a single transport blip used to mark four alerts and a stale config window. Adds `call_with_db_reconnect_retry` to `litellm/proxy/db/exception_handler.py` — a single canonical "try DB read, on transport error reconnect once and retry once" wrapper. Mirrors the inline pattern in `auth_checks._fetch_key_object_from_db_with_reconnect` so we have one implementation rather than three drifting copies, and gives future read paths a clean opt-in. 2. Fix the `_engine_confirmed_dead` flag-reset bug in `_run_reconnect_cycle`. The flag was cleared before `_do_heavy_reconnect()` ran, so any failure inside the heavy reconnect (timeout, missing DATABASE_URL, recreate failure) left the flag False — and the next attempt could silently demote to the lightweight path even though the engine was genuinely dead. Move the reset into the success branch so the flag stays True across heavy-reconnect failures and the next attempt re-enters the heavy branch. Tests: - `tests/test_litellm/proxy/db/test_exception_handler_reconnect_retry.py` (new) — 9 tests covering the helper's contract: happy path, retry on transport error, no retry on data-layer errors, propagation when reconnect fails, propagation after second transport error, `hasattr` guard for partial mocks, fresh-coroutine-per-call invariant, explicit timeout override, default timeouts read off the prisma_client. - `tests/test_litellm/proxy/db/test_prisma_self_heal.py` — adds: - `test_get_generic_data_retries_on_transport_error_for_config_table` - `test_get_generic_data_propagates_when_reconnect_fails` - `test_engine_confirmed_dead_persists_across_failed_heavy_reconnect` (regression test for the flag-reset bug). All 16 self-heal tests + 9 helper tests + 535 auth/exception-handler tests pass locally.
Greptile SummaryThis PR delivers two targeted fixes to the Prisma reconnect state machine: it restores the self-heal reconnect-and-retry branch on Confidence Score: 5/5Safe to merge — both fixes are well-scoped, regression-tested, and introduce no breaking changes. No P0 or P1 findings. Both changes are narrow, correct, and backed by comprehensive mock-only tests covering edge cases (reconnect-raises, double-transport-error, flag persistence across failures). The helper's exception chaining and bare- No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/db/exception_handler.py | Adds call_with_db_reconnect_retry helper: well-structured, at-most-one-retry semantics, preserves original transport error when attempt_db_reconnect raises (chained as __cause__), _coerce_timeout correctly guards against MagicMock slots and booleans. |
| litellm/proxy/utils.py | Two clean fixes: get_generic_data wrapped in call_with_db_reconnect_retry (inline import traceback safely removed — module-level import on line 11 covers it), and _engine_confirmed_dead = False moved to after asyncio.wait_for succeeds so a heavy-reconnect failure no longer silently demotes the next attempt to the lightweight path. |
| tests/test_litellm/proxy/db/test_exception_handler_reconnect_retry.py | 9 new unit tests for call_with_db_reconnect_retry; all cases mocked, no real network calls, covers happy path, transport retry, data-layer pass-through, reconnect-false, double-transport-error, no-attr guard, fresh-coroutine-on-retry, explicit timeouts, and reconnect-raises-preserves-original. |
| tests/test_litellm/proxy/db/test_prisma_self_heal.py | 3 new integration-level tests: get_generic_data reconnect-and-retry for config table, propagation when reconnect fails, and regression test for _engine_confirmed_dead flag persisting across a failed heavy reconnect. All properly mocked. |
Reviews (2): Last reviewed commit: "fix(proxy): preserve original transport ..." | Re-trigger Greptile
| did_reconnect = await prisma_client.attempt_db_reconnect( | ||
| reason=reason, | ||
| timeout_seconds=resolved_timeout, | ||
| lock_timeout_seconds=resolved_lock_timeout, | ||
| ) | ||
| if not did_reconnect: | ||
| raise |
There was a problem hiding this comment.
Exception from
attempt_db_reconnect silently swallows original transport error
If attempt_db_reconnect itself raises (e.g. a timeout or lock acquisition error), the original first_exc transport error is lost and callers see the reconnect exception instead. The docstring says a "reconnect failure" corresponds to returning False, but an exception from the reconnect method bypasses the if not did_reconnect: raise branch entirely. This means the original transport error (e.g. httpx.ReadError) won't be what observers see in logs or the failure_handler.
Consider wrapping the reconnect call to preserve the original error:
try:
did_reconnect = await prisma_client.attempt_db_reconnect(
reason=reason,
timeout_seconds=resolved_timeout,
lock_timeout_seconds=resolved_lock_timeout,
)
except Exception:
raise first_exc
if not did_reconnect:
raise first_excGreptile review on #26756 (P2): if `attempt_db_reconnect` itself raises (e.g. lock cancellation, timer error, unexpected internal failure), the original `httpx.ReadError` / transport error was lost — `failure_handler` and `db_exceptions` alerts then logged the reconnect exception instead of the actual DB transport problem, masking the root cause. Wrap the reconnect call in a try/except. On reconnect failure, re-raise the *original* `first_exc` and chain the reconnect error as `__cause__` so it remains visible for debuggability without becoming the primary exception observers see. Adds `test_call_with_db_reconnect_retry_preserves_original_error_when_reconnect_raises` asserting (a) the propagated exception is the original transport error and (b) the reconnect exception is attached as `__cause__`.
602a6cf
into
litellm_internal_staging
Greptile review on BerriAI#26756 (P2): if `attempt_db_reconnect` itself raises (e.g. lock cancellation, timer error, unexpected internal failure), the original `httpx.ReadError` / transport error was lost — `failure_handler` and `db_exceptions` alerts then logged the reconnect exception instead of the actual DB transport problem, masking the root cause. Wrap the reconnect call in a try/except. On reconnect failure, re-raise the *original* `first_exc` and chain the reconnect error as `__cause__` so it remains visible for debuggability without becoming the primary exception observers see. Adds `test_call_with_db_reconnect_retry_preserves_original_error_when_reconnect_raises` asserting (a) the propagated exception is the original transport error and (b) the reconnect exception is attached as `__cause__`.
Greptile review on BerriAI#26756 (P2): if `attempt_db_reconnect` itself raises (e.g. lock cancellation, timer error, unexpected internal failure), the original `httpx.ReadError` / transport error was lost — `failure_handler` and `db_exceptions` alerts then logged the reconnect exception instead of the actual DB transport problem, masking the root cause. Wrap the reconnect call in a try/except. On reconnect failure, re-raise the *original* `first_exc` and chain the reconnect error as `__cause__` so it remains visible for debuggability without becoming the primary exception observers see. Adds `test_call_with_db_reconnect_retry_preserves_original_error_when_reconnect_raises` asserting (a) the propagated exception is the original transport error and (b) the reconnect exception is attached as `__cause__`.
…t_hardening fix(proxy): self-heal Prisma read paths + harden reconnect state machine
Summary
Two related fixes layered on top of the existing Prisma reconnect plumbing.
1. Restore reconnect-and-retry on
PrismaClient.get_generic_data(issue #25143)#25143 reports that 1.83.x lost the transport-reconnect-and-retry-once branch that 1.82.6 had on
get_generic_data. Transienthttpx.ReadErrorflaps now surface immediately asdb_exceptionsalerts._update_config_from_dbfans out four concurrentget_generic_datareads, so a single transport blip used to mark four alerts and a stale config window.This PR restores that self-heal — but instead of inlining the pattern (the auth path at
auth_checks._fetch_key_object_from_db_with_reconnectand the readiness path at_health_endpointsalready had two drifting inline copies), it factors the pattern into a single canonical helper:```python
async def call_with_db_reconnect_retry(
prisma_client, coro_factory, *, reason,
timeout_seconds=None, lock_timeout_seconds=None,
):
```
placed next to
PrismaDBExceptionHandlerinlitellm/proxy/db/exception_handler.py.get_generic_datanow wraps its query block in this helper. Future read paths can opt in by passing a zero-arg coroutine factory.2. Fix the `_engine_confirmed_dead` flag-reset bug in `_run_reconnect_cycle`
_run_reconnect_cycleclearedself._engine_confirmed_dead = Falsebefore awaiting_do_heavy_reconnect(). If the heavy reconnect raised (timeout, missingDATABASE_URL, recreate failure), the flag was left cleared and the next attempt could silently demote to the lightweight path even though the engine was genuinely dead.Move the reset into the success branch so the flag stays True across heavy-reconnect failures, and the next attempt correctly re-enters the heavy branch.
Out of scope
call_with_db_reconnect_retryin a follow-up PR; deferred to keep this change focused.Test plan