fix(proxy): recover from prisma-query-engine zombie process - #21899
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR re-introduces the prisma-query-engine zombie process recovery feature (originally #21707, reverted in #21827) with fixes for the infinite reconnect loop observed on macOS. The core issue was that the original implementation relied on The new implementation replaces all
The
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/proxy/utils.py | Adds cross-platform engine process death detection (waitpid thread, pidfd, os.kill polling) replacing /proc-dependent logic. Introduces heavy vs lightweight reconnect branching, proper zombie reaping, and clean lifecycle management. Well-structured with appropriate race condition guards. |
| tests/litellm/proxy/test_prisma_engine_watchdog.py | Comprehensive test suite covering all detection paths (waitpid thread, pidfd, polling), race conditions (already-dead engine, stale PID, double-trigger), reconnect cycle branching (heavy vs lightweight), and lifecycle start/stop. Good use of mocking patterns. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[start_db_health_watchdog_task] --> B[_start_engine_watcher]
B --> C{_get_engine_pid}
C -->|PID found| D{_try_waitpid_watch}
C -->|PID == 0| Z[No watcher - detection unavailable]
D -->|Child process - thread started| E[waitpid thread blocks on os.waitpid]
D -->|Already dead| F[Set _engine_confirmed_dead]
D -->|Not child process| G{_try_pidfd_watch}
G -->|pidfd_open success| H[Register fd with asyncio add_reader]
G -->|Unavailable/failed| I[_poll_engine_proc - os.kill polling 1s]
E -->|Engine exits| J[call_soon_threadsafe → _on_engine_death_from_thread]
H -->|pidfd readable| K[_on_pidfd_readable]
I -->|ProcessLookupError| L[Engine gone detected]
J --> M{_engine_confirmed_dead?}
K --> M
F --> N[attempt_db_reconnect force=True]
L --> N
M -->|No - first detection| N
M -->|Yes - already handled| O[Skip]
N --> P[_run_reconnect_cycle]
P --> Q{engine_is_dead?}
Q -->|Yes| R[Heavy: recreate_prisma_client + re-arm watcher]
Q -->|No| S[Lightweight: disconnect → connect → SELECT 1]
Last reviewed commit: a1a66c5
7ae1571
into
BerriAI:litellm_oss_staging_02_23_2026
* fix(proxy): recover from prisma-query-engine zombie process * fix(proxy): remove unused imports and extract helper to fix PLR0915 in utils.py
…21899) * fix(proxy): recover from prisma-query-engine zombie process * fix(proxy): remove unused imports and extract helper to fix PLR0915 in utils.py
Relevant issues
re-do of #21707 (reverted in #21827) — fixes the infinite reconnect loop on macOS
related to the
All connection attempts failedissues after upgrading to v1.81.x (#15536 #15585 #20427)Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewCI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Type
🐛 Bug Fix
Changes
re-submission of #21707 which was reverted in #21827. @ishaan-jaff caught an infinite reconnect loop when running the proxy locally on macOS, the watcher found the engine PID, then instantly concluded it disappeared, looping nonstop. this was my fault, i only tested inside docker (linux containers) and never ran it on macOS before merging
after digging into the revert logs i found two root causes. the first was that the original code depended on
/procfor everything — checking engine liveness via/proc/<pid>/stat, scanning/proc/*/cmdlinefor PID discovery, and polling/procas the fallback watcher. macOS doesn't have/proc, so all of that threwFileNotFoundErrorwhich_is_engine_alive()read as "process is gone", triggering a false-positive reconnect that restarted the watcher and looped forever. that's exactly what the revert logs showedthe second issue was that the "primary" SIGCHLD signal handler was silently dead the whole time. litellm always runs with uvloop on unix, and uvloop uses libuv internally which takes ownership of SIGCHLD and hard-blocks any user handler with a
RuntimeError(uvloop#409, uvloop#582).pidfd_open+add_reader()also gets rejected under libuv's fd restrictions. so both event-driven paths were dead code and everything fell through to the/procfallback — which was the broken path on macOS. in docker on linux/procworks fine, so the fallback happened to work and i never noticedthis version completely removes
/procandSIGCHLDlogic.the new primary detection is
os.waitpid(pid, 0)in a dedicated daemon thread that blocks until the engine exits, then notifies the asyncio loop viacall_soon_threadsafe(). this is the same pattern CPython itself uses in_ThreadedChildWatcherand this works with any event loop. it handles the race where uvloop reaps the child first by catchingChildProcessErrorpidfd_openis still there as a secondary path for linux 5.3+, though the waitpid thread already covers it. the last-resort fallback now usesos.kill(pid, 0)polling instead of/proc— the POSIX-standard signal-zero existence check, same approach psutil uses for cross-platform process liveness. works on linux, macOS, and any POSIX system_get_engine_pid()now only uses prisma internals (no/procscan),_is_engine_alive()usesos.kill(pid, 0)instead of parsing/proc/<pid>/stat, and_attempt_reconnect_with_lock_timeout()was inlined into its only caller. zombie reaping still usesos.waitpid(-1, WNOHANG)for PID 1 responsibility in containersthe rest of the watchdog is the same as #21707 — engine death sets
_engine_confirmed_dead,_run_reconnect_cycleuses the flag to pick heavy reconnect (recreate_prisma_client+ re-arm watcher) vs lightweight (disconnect → connect → SELECT 1). the existing DB health watchdog from #21706 is untouchedthis is behavior when testing locally with a stress test script:
prismalitellm.mp4