Ampel / ampel (push) Failing after 21s
Suche aus Merkliste (deploy/radar-watchlist.json) und Hugging-Face-Entdeckung; nur Kandidaten mit Bild-Projektor, die neben das Warm-Set passen (<= 115 GB inkl. KV-Cache). Nachtlauf mc2-radar.timer 00:30, Tests nur bis 02:30 (um 03:00 kommt NerdQuiz), hoechstens ein neuer Kandidat pro Woche, Notbremse 02:35, RuntimeMaxSec als letzte Sicherung. Pruefstand als Modul (deploy/bench/pruefstand.py): Tempo, Werkzeuge, Deutsch/JSON bzw. Programmieraufgaben und Bild-Probe gegen das heutige Modell der Rolle. Durchgefallene werden geloescht, Bestandene gemeldet und erst nach "Uebernehmen" getauscht. Beim Uebernehmen wandern die Zweitrollen mit (fast beim Hirn, heavy beim Coder), und das Warm-Set des Stewards wird selbst umgestellt statt als Handgriff zu bleiben. Modell-Code im Pruefstand darf keine Prozesse starten (RLIMIT_NPROC=0). Radar steht im Flugplan und unter Waechter-Aufsicht; deploy.sh spielt seine Units ein. Oberflaeche: Vergleichswerte je Kandidat, Rueckfrage vor Uebernehmen und Verwerfen, Status auf Deutsch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""
|
||
Nächtlicher Lauf des Modell-Radars (Box-Wart, 09/2026) — gestartet von mc2-radar.timer um 00:30.
|
||
|
||
Warum ein eigener Einstieg: Der Test lädt ein bis zu ~85 GB großes Modell neben Lucys Hirn. Das
|
||
darf nur nachts passieren und muss um 02:30 sicher vorbei sein, weil um 03:00 der NerdQuiz-
|
||
Nachtlauf das Hirn braucht. Ablauf:
|
||
|
||
1. Suchen — höchstens einmal am Tag (Merkliste + Entdeckung).
|
||
2. Offene Urteile nachholen („getestet“: damals fehlte die Vergleichsmessung).
|
||
3. Testen — nur im Fenster 00:30–02:30 und höchstens ein neuer Kandidat pro Woche.
|
||
|
||
Die Messungen prüfen die Frist selbst. Hängt trotzdem etwas, zieht fünf Minuten nach der Frist die
|
||
Notbremse: Kandidaten-Server und Download werden gestoppt, der Prozess endet hart. systemd
|
||
(RuntimeMaxSec) ist die letzte Sicherung. Ist nichts fällig, endet der Lauf sofort mit Code 0.
|
||
"""
|
||
|
||
import logging
|
||
import os
|
||
import signal
|
||
import subprocess
|
||
import sys
|
||
import threading
|
||
import time
|
||
from pathlib import Path
|
||
|
||
from services import radar
|
||
|
||
logging.basicConfig(
|
||
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
|
||
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||
)
|
||
log = logging.getLogger("radar_lauf")
|
||
|
||
NOTBREMSE_S = int(os.environ.get("MC_RADAR_NOTBREMSE_S", "300"))
|
||
|
||
|
||
def _beim_beenden(signum, _frame) -> None:
|
||
"""SIGTERM (systemd stop, RuntimeMaxSec) → normal auslaufen, damit der Kandidaten-Server gestoppt wird."""
|
||
raise SystemExit(f"Signal {signum}")
|
||
|
||
|
||
def _notbremse(frist: float) -> threading.Timer:
|
||
def ziehen() -> None:
|
||
log.error("Radar: Notbremse – %s s nach der Frist läuft noch etwas, der Lauf wird hart beendet.", NOTBREMSE_S)
|
||
try:
|
||
radar.notstopp()
|
||
finally:
|
||
os._exit(3)
|
||
|
||
bremse = threading.Timer(max(frist - time.time(), 0) + NOTBREMSE_S, ziehen)
|
||
bremse.daemon = True
|
||
bremse.start()
|
||
return bremse
|
||
|
||
|
||
NOTIFY = Path(__file__).resolve().parents[1] / "deploy" / "notify.sh"
|
||
|
||
|
||
def melde_bestanden(test: dict) -> None:
|
||
"""Ein bestandener Kandidat wartet auf eine Entscheidung — einmal Bescheid geben, sonst liegt er
|
||
unbemerkt herum (Lehre der Pins vom September). notify.sh sammelt Nachtmeldungen für den Morgen."""
|
||
k = radar.lade_stand()["kandidaten"].get(test.get("kandidat")) or {}
|
||
rolle = "das Hirn" if test.get("rolle") == "hirn" else "den Coder"
|
||
text = (f"{k.get('name') or test.get('kandidat')} hat den Nachttest für {rolle} bestanden. "
|
||
f"{str(test.get('zusammenfassung') or '')[:300]} "
|
||
"In MC2 unter Modelle kannst du ihn übernehmen oder verwerfen.")
|
||
try:
|
||
subprocess.run(["bash", str(NOTIFY), "-s", "[Modell-Radar]", text], timeout=120, check=False,
|
||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
except (OSError, subprocess.SubprocessError):
|
||
log.warning("Radar: Meldung über den bestandenen Kandidaten ging nicht raus.", exc_info=True)
|
||
|
||
|
||
def main() -> int:
|
||
beginn = radar.jetzt()
|
||
if radar.suche_faellig(beginn):
|
||
try:
|
||
ergebnis = radar.suche()
|
||
log.info("Radar: Suche – %s neu, %s warten", len(ergebnis.get("neu") or []), ergebnis.get("wartend"))
|
||
except Exception:
|
||
log.warning("Radar: Suche fehlgeschlagen", exc_info=True)
|
||
else:
|
||
log.info("Radar: heute schon gesucht.")
|
||
|
||
if not radar.im_fenster(radar.jetzt()):
|
||
log.info("Radar: außerhalb des Test-Fensters %s–%s, nichts zu testen.", radar.FENSTER_START,
|
||
radar.FENSTER_ENDE)
|
||
return 0
|
||
|
||
frist = radar.fenster_ende(radar.jetzt()).timestamp()
|
||
signal.signal(signal.SIGTERM, _beim_beenden)
|
||
bremse = _notbremse(frist)
|
||
try:
|
||
if (nachgeholt := radar.nachurteilen(frist)):
|
||
log.info("Radar: %s offene Urteile nachgeholt.", nachgeholt)
|
||
plan = radar.plane_test(radar.lade_stand(), radar.jetzt())
|
||
if not plan.get("kandidat"):
|
||
log.info("Radar: nichts fällig (%s).", plan.get("grund"))
|
||
return 0
|
||
log.info("Radar: teste %s bis spätestens %s.", plan.get("grund"), radar.FENSTER_ENDE)
|
||
test = radar.teste(plan["kandidat"], frist)
|
||
if test:
|
||
log.info("Radar: %s – %s", test.get("ergebnis"), test.get("zusammenfassung"))
|
||
if test.get("ergebnis") == "bestanden":
|
||
melde_bestanden(test)
|
||
return 0
|
||
finally:
|
||
bremse.cancel()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|