litellm/tests/_process_helpers.py
devin-ai-integration[bot] 0d6ee3dc5a
test: count a zombie grandchild as gone in the migrate deploy timeout test (#42570)
* test: count a zombie grandchild as gone in the migrate deploy timeout test

A SIGKILLed grandchild whose parent died in the same killpg reparents to
PID 1 or the nearest subreaper and stays a zombie until reaped, and signal 0
still succeeds on a zombie, so the timeout test read it as alive wherever
PID 1 is slow to reap or never does. The sibling test in
tests/test_litellm/proxy/db already handled that; both now share one
process_is_gone helper that reads the /proc state and reaps its own
children, with unit tests for the live, reaped, unreaped, and foreign
zombie shapes.

* test: move the pre-commit interrupt test onto the shared zombie-aware liveness helper

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-09-22 14:59:26 -07:00

57 lines
1.6 KiB
Python

"""Whether a killed process is really gone, for tests that kill whole process trees.
A SIGKILLed grandchild whose parent died in the same ``killpg`` reparents to the
nearest subreaper or PID 1, and until that ancestor reaps it the pid is a zombie
that ``os.kill(pid, 0)`` still accepts. Reading its ``/proc`` state, and reaping
it when it landed on this process, keeps a runner that is slow to reap, or never
does, from turning a dead process into a failed assertion. The reap comes after
the liveness read so a child seen dying between the two is still collected on
the next poll instead of staying this process's own zombie.
"""
import os
import time
from pathlib import Path
from typing import Final
POLL_INTERVAL_S: Final = 0.05
def _exists(pid: int) -> bool:
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
return True
def _is_zombie(pid: int) -> bool:
try:
stat: Final = Path(f"/proc/{pid}/stat").read_text()
except OSError:
return False
return stat.rpartition(")")[2].split()[0] == "Z"
def _reap_if_ours(pid: int) -> None:
if os.name == "nt":
return
try:
os.waitpid(pid, os.WNOHANG)
except ChildProcessError:
pass
def _gone_now(pid: int) -> bool:
dead: Final = not _exists(pid) or _is_zombie(pid)
_reap_if_ours(pid)
return dead
def process_is_gone(pid: int, within_seconds: float) -> bool:
deadline: Final = time.monotonic() + within_seconds
while time.monotonic() < deadline:
if _gone_now(pid):
return True
time.sleep(POLL_INTERVAL_S)
return False