This commit is contained in:
lin98 2026-09-14 23:13:53 -07:00 committed by GitHub
commit d27385f43c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 0 deletions

View file

@ -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.",

View file

@ -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}