mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
Merge b747b65386 into af6dc1db08
This commit is contained in:
commit
d27385f43c
2 changed files with 95 additions and 0 deletions
|
|
@ -5386,6 +5386,14 @@ class PrismaClient:
|
|||
def _is_engine_alive(self) -> bool:
|
||||
if self._engine_pid <= 0:
|
||||
return True
|
||||
# Windows 上 os.kill(pid, 0) 不是 Unix 的「signal-0 存活探測」,而是直接
|
||||
# 呼叫 TerminateProcess(handle, 0) 把 engine 殺掉:CPython 在 Windows 只
|
||||
# 把 CTRL_C_EVENT / CTRL_BREAK_EVENT 當特例,其餘 sig 值(含 0)一律
|
||||
# TerminateProcess。Windows 沒有安全的 signal-0 探測方式,故一律回報存活,
|
||||
# 真正的 engine 故障交由查詢層 call_with_db_reconnect_retry 在
|
||||
# ReadError / ConnectError 時惰性處理。
|
||||
if sys.platform == "win32":
|
||||
return True
|
||||
try:
|
||||
os.kill(self._engine_pid, 0)
|
||||
return True
|
||||
|
|
@ -5610,6 +5618,11 @@ class PrismaClient:
|
|||
Only used when BOTH waitpid thread and pidfd are unavailable
|
||||
(e.g., PID is not our child process and pidfd_open fails)
|
||||
"""
|
||||
# 保險絲:Windows 上 os.kill(pid, 0) 會 TerminateProcess 殺掉 engine,
|
||||
# 這個 poller 絕不能在 Windows 執行。_start_engine_watcher 已在 win32
|
||||
# 不會排這個 task,此處再擋一道以防未來有其他呼叫點。
|
||||
if sys.platform == "win32":
|
||||
return
|
||||
while self._watching_engine and self._engine_pid > 0:
|
||||
try:
|
||||
os.kill(self._engine_pid, 0)
|
||||
|
|
@ -5692,6 +5705,18 @@ class PrismaClient:
|
|||
"Watching engine PID %s via pidfd.",
|
||||
pid,
|
||||
)
|
||||
elif sys.platform == "win32":
|
||||
# Windows 沒有 waitpid/pidfd,唯一的 fallback 是 os.kill 輪詢,但
|
||||
# os.kill(pid, 0) 在 Windows 會 TerminateProcess 把 engine 殺掉
|
||||
# (連上後約 1 秒就被自己的存活檢查殺死,之後所有查詢 ConnectError)。
|
||||
# 因此 Windows 不做主動 engine-death 偵測——回到 1.82 之前 Windows 的
|
||||
# 既有行為,engine 故障由查詢層的 reconnect retry 惰性處理。
|
||||
verbose_proxy_logger.info(
|
||||
"Engine PID %s: proactive engine-death detection is unavailable on "
|
||||
"Windows (waitpid/pidfd unsupported; os.kill polling would terminate "
|
||||
"the engine). Relying on query-level reconnect retry instead.",
|
||||
pid,
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.info(
|
||||
"Watching engine PID %s via os.kill polling.",
|
||||
|
|
|
|||
|
|
@ -712,3 +712,73 @@ async def test_poll_engine_proc_planned_death_skips_reconnect(
|
|||
"cleanup_called": 1,
|
||||
"confirmed_dead": False,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Windows: os.kill(pid, 0) is NOT a liveness probe -- CPython maps every sig
|
||||
# other than CTRL_C_EVENT/CTRL_BREAK_EVENT (including 0) to TerminateProcess,
|
||||
# so the Unix-style liveness checks would terminate the query engine (~1s after
|
||||
# connect, then every subsequent query fails with ConnectError). The engine
|
||||
# watcher therefore must never signal the engine on Windows. These tests run on
|
||||
# the Linux CI runner and simulate Windows by patching ``sys.platform`` -- the
|
||||
# module-level ``skipif(win32)`` above keeps them from running on real Windows
|
||||
# where ``os.kill`` would actually kill the test's own processes.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_is_engine_alive_never_calls_os_kill_on_windows(
|
||||
prisma_client: PrismaClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
prisma_client._engine_pid = 12345
|
||||
fake_kill = MagicMock()
|
||||
monkeypatch.setattr("os.kill", fake_kill)
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
pinned = {
|
||||
"result": prisma_client._is_engine_alive(),
|
||||
"os_kill_calls": fake_kill.call_count,
|
||||
}
|
||||
assert pinned == {"result": True, "os_kill_calls": 0}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_poll_engine_proc_is_noop_on_windows(
|
||||
prisma_client: PrismaClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
prisma_client._engine_pid = 555
|
||||
prisma_client._watching_engine = True
|
||||
prisma_client.attempt_db_reconnect = AsyncMock()
|
||||
fake_kill = MagicMock()
|
||||
monkeypatch.setattr("os.kill", fake_kill)
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
|
||||
# Must return immediately without ever signalling the engine.
|
||||
await asyncio.wait_for(prisma_client._poll_engine_proc(), timeout=1)
|
||||
pinned = {
|
||||
"os_kill_calls": fake_kill.call_count,
|
||||
"reconnect_calls": prisma_client.attempt_db_reconnect.await_count,
|
||||
}
|
||||
assert pinned == {"os_kill_calls": 0, "reconnect_calls": 0}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_engine_watcher_does_not_poll_on_windows(
|
||||
prisma_client: PrismaClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""On Windows both waitpid and pidfd are unavailable; the watcher must NOT
|
||||
fall back to os.kill polling (which would terminate the engine). It records
|
||||
the PID but starts no poller, deferring to query-level reconnect retry."""
|
||||
monkeypatch.setattr(prisma_client, "_get_engine_pid", MagicMock(return_value=4242))
|
||||
monkeypatch.setattr(prisma_client, "_try_waitpid_watch", MagicMock(return_value=False))
|
||||
monkeypatch.setattr(prisma_client, "_try_pidfd_watch", MagicMock(return_value=False))
|
||||
poll = AsyncMock()
|
||||
monkeypatch.setattr(prisma_client, "_poll_engine_proc", poll)
|
||||
fake_kill = MagicMock()
|
||||
monkeypatch.setattr("os.kill", fake_kill)
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
|
||||
await prisma_client._start_engine_watcher()
|
||||
await asyncio.sleep(0)
|
||||
pinned = {
|
||||
"watching": prisma_client._watching_engine,
|
||||
"poll_started": poll.await_count,
|
||||
"os_kill_calls": fake_kill.call_count,
|
||||
}
|
||||
assert pinned == {"watching": False, "poll_started": 0, "os_kill_calls": 0}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue