79 lines
No EOL
3.4 KiB
Python
79 lines
No EOL
3.4 KiB
Python
"""Регресс e-261003004801: таймаут LLM-тика не должен валить зачистку.
|
||
|
||
Было: subprocess.run + TimeoutExpired без .pid → AttributeError в
|
||
изолированном runner'е ДО добивания процесс-группы. LLM-потомки воркера
|
||
выживали и писали в стор после смерти тика (race, burn роутера).
|
||
Теперь: Popen(start_new_session=True) → killpg(proc.pid); исключение —
|
||
честный TimeoutExpired, группа (с потомками) мертва до re-raise.
|
||
"""
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
import unittest
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from harness import isolated_runner
|
||
|
||
|
||
class TimeoutKillTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.tmp = tempfile.mkdtemp(prefix="test_timeout_kill_")
|
||
self.worker = os.path.join(self.tmp, "fake_worker.py")
|
||
# фейковый воркер: спавнит потомка (имитация LLM-сабпроцесса) и спит
|
||
with open(self.worker, "w") as f:
|
||
f.write(
|
||
"import subprocess, time\n"
|
||
"child = subprocess.Popen(['sleep', '287'])\n"
|
||
"print(child.pid, flush=True)\n"
|
||
"time.sleep(300)\n"
|
||
)
|
||
self._orig = (isolated_runner._VENV_PY, isolated_runner._WORKER)
|
||
isolated_runner._VENV_PY = sys.executable
|
||
isolated_runner._WORKER = self.worker
|
||
|
||
def tearDown(self):
|
||
isolated_runner._VENV_PY, isolated_runner._WORKER = self._orig
|
||
|
||
def _cfg(self):
|
||
class Cfg:
|
||
name = "test"
|
||
model = "x"
|
||
effort = "low"
|
||
mandate = ""
|
||
can_shell = False
|
||
ctx_tokens = 1000
|
||
every = 300
|
||
return Cfg()
|
||
|
||
def test_timeout_kills_group_without_pid_crash(self):
|
||
with self.assertRaises(subprocess.TimeoutExpired):
|
||
isolated_runner.run_llm_tick(self._cfg(), "ping", timeout=3)
|
||
# группа воркера убита: потомок-«LLM» не выжил
|
||
time.sleep(0.5)
|
||
r = subprocess.run(["pgrep", "-c", "-f", "sleep 287"],
|
||
capture_output=True)
|
||
# pgrep: exit 1 = совпадений нет → потомок-«LLM» мёртв
|
||
self.assertEqual(r.returncode, 1,
|
||
f"LLM-потомок пережил таймаут добивания: {r.stdout!r}")
|
||
|
||
def test_normal_run_unaffected(self):
|
||
# короткий воркер: пишет результат в result_path как настоящий
|
||
worker = os.path.join(self.tmp, "ok_worker.py")
|
||
with open(worker, "w") as f:
|
||
f.write(
|
||
"import json, os, sys\n"
|
||
"payload = json.loads(sys.stdin.read())\n"
|
||
"json.dump({'ok': True, 'text': 'done',\n"
|
||
" 'usage': {}, 'trace_id': 't'}, open(payload['result_path'], 'w'))\n"
|
||
)
|
||
isolated_runner._WORKER = worker
|
||
text, usage, trace = isolated_runner.run_llm_tick(self._cfg(),
|
||
"ping", timeout=15)
|
||
self.assertEqual(text, "done")
|
||
self.assertEqual(trace, "t")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main(verbosity=2) |