mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
* 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>
57 lines
1.6 KiB
Python
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
|