Skip to content

fix(proxy): self-heal Prisma read paths + harden reconnect state machine - #26756

Merged
yuneng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_prisma_reconnect_hardening
Apr 29, 2026
Merged

fix(proxy): self-heal Prisma read paths + harden reconnect state machine#26756
yuneng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_prisma_reconnect_hardening

Conversation

@yuneng-berri

Copy link
Copy Markdown
Contributor

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. 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.

This PR restores that self-heal — but instead of inlining the pattern (the auth path at auth_checks._fetch_key_object_from_db_with_reconnect and the readiness path at _health_endpoints already 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 PrismaDBExceptionHandler in litellm/proxy/db/exception_handler.py. get_generic_data now 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_cycle cleared self._engine_confirmed_dead = False before awaiting _do_heavy_reconnect(). If the heavy reconnect raised (timeout, missing DATABASE_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

  • The event-loop-blocking `disconnect()` is addressed separately by [Fix] Proxy: reconnect Prisma DB without blocking the event loop #26225 — orthogonal change, this PR does not depend on or conflict with it.
  • The auth + health inline copies of the retry pattern can be replaced with call_with_db_reconnect_retry in a follow-up PR; deferred to keep this change focused.

Test plan

  • `pytest tests/test_litellm/proxy/db/test_exception_handler_reconnect_retry.py` — 9 helper tests pass
  • `pytest tests/test_litellm/proxy/db/test_prisma_self_heal.py` — 16 tests pass (13 existing + 3 new)
  • `pytest tests/test_litellm/proxy/db/ tests/litellm/proxy/test_prisma_engine_watchdog.py` — 197 pass
  • `pytest tests/test_litellm/proxy/auth/ tests/test_litellm/proxy/db/test_exception_handler.py` — 535 pass
  • `uv run black .` — clean
  • `make lint-ruff` — clean

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-apps

greptile-apps Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR delivers two targeted fixes to the Prisma reconnect state machine: it restores the self-heal reconnect-and-retry branch on get_generic_data (lost in 1.83.x, issue #25143) by factoring a canonical call_with_db_reconnect_retry helper, and it moves the _engine_confirmed_dead = False reset to after a successful heavy reconnect so a mid-reconnect failure no longer silently demotes the next attempt to the lightweight path. The implementation is sound — error chaining, at-most-one-retry semantics, and the _coerce_timeout guard against MagicMock slots are all handled correctly, and the module-level traceback import in utils.py covers the removed inline import cleanly.

Confidence Score: 5/5

Safe 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-raise semantics are correct for Python 3's exception context rules.

No files require special attention.

Important Files Changed

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

Comment thread litellm/proxy/db/exception_handler.py Outdated
Comment on lines +235 to +241
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

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.

P2 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_exc

Greptile 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__`.
@yuneng-berri

Copy link
Copy Markdown
Contributor Author

@greptile

@yuneng-berri
yuneng-berri merged commit 602a6cf into litellm_internal_staging Apr 29, 2026
113 of 114 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_prisma_reconnect_hardening branch April 29, 2026 20:49
yugborana pushed a commit to yugborana/litellm that referenced this pull request Jun 2, 2026
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__`.
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
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__`.
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…t_hardening

fix(proxy): self-heal Prisma read paths + harden reconnect state machine
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.

2 participants