47f7a85510
Die Ampel-Nachruestung deckte 317/337 vorbestehende ruff-Verstoesse im ganzen Repo auf. Aufgeraeumt: - ruff.toml: intentionale Muster als Projekt-Politik ausgenommen (BLE001 blind-except, S110/S112 try-except-pass/continue, PLW1510 subprocess-best-effort, B008 FastAPI- Depends/File-Idiom, EXE001 Shebang, + wenige Stil-Regeln). __init__.py-Re-Exports geschuetzt (F401). - ruff --fix: 128 mechanische (Import-Sortierung, PEP585/604-Annotationen, tote Imports, ueberfluessige noqa) auto-behoben. - 12 echte Reste von Hand: PERF402/102, PLC3002 (Lambda->walrus), ISC004 (String-Concat geklammert), F841/RUF059 (ungenutzte Vars), PIE810 (startswith-Tuple), UP031 (f-string), UP035 (veraltete typing-Imports). Ergebnis: 'ruff check .' = 0, 'compileall' grün. Kein Verhaltenswechsel (nur Stil/Modernisierung). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
"""MC2-Zusatzmodul (20.07.2026) — Orphan-Reaper für Kanban-Worker.
|
|
|
|
Wird vom Patch-Träger (``deploy/hermes-patches/apply.py``) nach
|
|
``~/.hermes/hermes-agent/hermes_cli/`` kopiert und vom Gateway-Dispatcher
|
|
(``gateway/kanban_watchers.py``, per Patch 0003) jeden Tick aufgerufen.
|
|
|
|
Problem: Wird eine ``running``-Karte von außen blockiert/zurückgesetzt (z. B.
|
|
über das Auftragsbuch oder die No-Progress-Bremse), setzt ``block_task``
|
|
``worker_pid = NULL`` — der Worker-PROZESS läuft aber weiter. Die DB zählt ihn
|
|
nicht mehr, also unterläuft der Orphan ``kanban.max_in_progress`` (Doppel-Worker
|
|
möglich) und verbrennt Rechenzeit. Dieser Reaper beendet lebende Worker, deren
|
|
Task nicht mehr ``running`` ist.
|
|
|
|
Linux-only (/proc-Scan). Best-effort: jeder Fehler → leere Liste, nie werfen.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import signal
|
|
import sys
|
|
|
|
_TASK_RE = re.compile(rb"work kanban task (t_[0-9a-fA-F]+)")
|
|
|
|
|
|
def reap_orphaned_workers(connect_closing) -> list[tuple[int, str]]:
|
|
"""Beende lebende Kanban-Worker, deren Task nicht mehr ``running`` ist.
|
|
|
|
``connect_closing`` ist die gleichnamige Kontextmanager-Factory aus
|
|
``kanban_db`` (durchgereicht, um Import-Zyklen zu vermeiden). Gibt die Liste
|
|
der beendeten ``(pid, task_id)`` zurück.
|
|
"""
|
|
if sys.platform != "linux":
|
|
return []
|
|
candidates: dict[int, str] = {}
|
|
try:
|
|
entries = os.listdir("/proc")
|
|
except OSError:
|
|
return []
|
|
for entry in entries:
|
|
if not entry.isdigit():
|
|
continue
|
|
try:
|
|
with open(f"/proc/{entry}/cmdline", "rb") as fh:
|
|
cmd = fh.read().replace(b"\x00", b" ")
|
|
except OSError:
|
|
continue
|
|
m = _TASK_RE.search(cmd)
|
|
if m:
|
|
candidates[int(entry)] = m.group(1).decode()
|
|
if not candidates:
|
|
return []
|
|
|
|
task_ids = list(set(candidates.values()))
|
|
placeholders = ",".join("?" * len(task_ids))
|
|
running: set[str] = set()
|
|
try:
|
|
with connect_closing() as conn:
|
|
running = {
|
|
row[0]
|
|
for row in conn.execute(
|
|
f"SELECT id FROM tasks WHERE id IN ({placeholders}) "
|
|
"AND status = 'running'",
|
|
task_ids,
|
|
)
|
|
}
|
|
except Exception:
|
|
return []
|
|
|
|
killed: list[tuple[int, str]] = []
|
|
for pid, task_id in candidates.items():
|
|
if task_id in running:
|
|
continue
|
|
# Prozessgruppe zuerst (Worker starten start_new_session=True → eigene
|
|
# Gruppe; killpg räumt selbst gestartete Kinder wie uvicorn gleich mit).
|
|
try:
|
|
os.killpg(os.getpgid(pid), signal.SIGTERM)
|
|
except OSError:
|
|
try:
|
|
os.kill(pid, signal.SIGTERM)
|
|
except OSError:
|
|
continue
|
|
killed.append((pid, task_id))
|
|
return killed
|