feat(worker): der externe Worker wird erwachsen - Log in Rippy, Anzeige, Slots
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
Commander 26.07.2026: "der externe Encoder Worker ist ein bisschen duenn - der koennte noch viel mehr." Drei Punkte, alle am Tray. 1. LOG IN RIPPY STATT TXT-DATEI (ausdruecklich gewuenscht). Das Tray schrieb sein Log nach %LOCALAPPDATA% und oeffnete es im Editor - wer wissen wollte, warum der Worker nichts tut, musste sich an den PC setzen. Neue Bruecke (logbruecke.py) meldet die wichtigen Zeilen nach Rippy, Quelle "w:<name>", und die Logs-Seite hat jetzt Knoepfe je Quelle: "was macht mein PC" ist ein Klick. Der Filter konnte Quellen schon immer, es gab nur keinen Knopf. Durchgelassen wird WENIG und mit Grund: Die Job-Meldungen stehen laengst in Rippy (tasks.py schreibt sie selbst). Es fehlte, was DANEBEN passiert und den Worker unbrauchbar macht, ohne dass ein Job existiert - hochgefahren oder nicht, Verbindung zu Redis/Postgres, Abstuerze. Alles andere fliegt weg: Celery ist bei --loglevel=info gespraechig, die logs-Tabelle hat keine Aufraeumung, und ein zugemuelltes Log ist so unbrauchbar wie keins. Dazu eine Drossel (30 Zeilen/Minute), die MELDET, wieviel sie verschluckt hat. Zeilenformat woertlich aus dem laufenden Container abgenommen (Celery 5.4.0). Die lokale Datei bleibt - sie ist genau dann die einzige Auskunft, wenn Rippy nicht erreichbar ist. 2. DAS TRAY ZEIGT, WAS LAEUFT. Vorher stand dort "laeuft" oder "gestoppt" - auf einer Maschine, die stundenlang an einem Film rechnet, ist das keine Auskunft. Jetzt Titel, Prozent und Restzeit, geholt von Rippys /jobs. Bewusst dieselbe Quelle wie das Dashboard, damit im Tray nicht eine zweite, abweichende Schaetzung steht. Dazu: Windows schlaeft nicht mehr mitten im Encode ein (SetThreadExecutionState, ohne ES_DISPLAY_REQUIRED - der Bildschirm darf ausgehen). Die Sperre wird zurueckgenommen, sobald nichts laeuft, und auch bei einem harten Ende des Trays - sonst schlaeft der PC nie wieder ein und niemand weiss warum. 3. MEHRERE ENCODES GLEICHZEITIG. Der Worker lief fest mit --pool=solo und nahm genau EINEN Auftrag an. Der Installer fragt die Zahl jetzt (GUI: Feld neben dem Namen, mit der erkannten Kernzahl daneben), Vorbelegung ab 12 Kernen zwei, sonst einer: HandBrake nutzt schon alle Kerne, aber x265 skaliert nicht linear. Auf Windows gibt es keinen prefork-Pool (kein fork) - deshalb --pool=threads, was hier passt, weil die Arbeit ein Kind-Prozess ist und der Thread nur wartet. GUI-Layout headless gerendert und angesehen (nichts ueberlappt, 16 Kerne korrekt erkannt), beide .ps1 mit echtem PowerShell 5.1 geprueft, BOM und CRLF erhalten, .exe neu gebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""Celery-Ausgabe des Workers nach Rippy melden — statt in eine txt-Datei.
|
||||
|
||||
Commander-Wunsch 26.07.2026: *„bessere Log-Ansichten (kein txt file → direkt von
|
||||
Rippy Logs)"*. Der native Windows-Worker schrieb sein Log bisher nach
|
||||
`%LOCALAPPDATA%\\Rippy Worker\\worker.log`, und das Tray öffnete die Datei im
|
||||
Editor. Wer wissen wollte, warum der Worker nichts tut, musste sich also an den
|
||||
PC setzen — während Rippy im Browser schon eine Log-Seite hat.
|
||||
|
||||
## Was hier durchgeht — und warum so wenig
|
||||
|
||||
Die JOB-Meldungen stehen längst in Rippy: `tasks.py` schreibt „Kompression
|
||||
gestartet", Fehlschläge, Abbrüche selbst über `db.add_log`. Was fehlte, ist
|
||||
alles, was DANEBEN passiert und den Worker unbrauchbar macht, ohne dass ein Job
|
||||
existiert:
|
||||
|
||||
- Der Worker ist hochgefahren (oder eben nicht).
|
||||
- Die Verbindung zu Redis/Postgres steht nicht.
|
||||
- Er ist abgestürzt (Traceback).
|
||||
|
||||
Alles andere wird verworfen. Celery ist bei `--loglevel=info` gesprächig, die
|
||||
`logs`-Tabelle hat keine automatische Aufräumung, und ein zugemülltes Log ist
|
||||
genauso unbrauchbar wie keins.
|
||||
|
||||
## Das Format ist gemessen, nicht geraten
|
||||
|
||||
Wörtlich aus dem laufenden Worker-Container (26.07.2026, Celery 5.4.0) —
|
||||
AGENTS Regel D:
|
||||
|
||||
[2026-07-26 12:04:18,060: INFO/MainProcess] Connected to redis://redis:6379/0
|
||||
[2026-07-26 12:04:19,063: INFO/MainProcess] mingle: searching for neighbors
|
||||
[2026-07-26 12:04:19,082: INFO/MainProcess] celery@ced31798928d ready.
|
||||
[2026-07-26 12:06:19,094: WARNING/MainProcess] Zombie-Erkennung: {...}
|
||||
|
||||
Dazu der Startbanner (Zeilen ohne diese Klammer) und Warnungen aus Pythons
|
||||
warnings-Modul.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
# `[<Zeitstempel>: <LEVEL>/<Prozess>] <Text>`
|
||||
CELERY_ZEILE = re.compile(r"^\[[^\]]*?:\s*(\w+)/([^\]]+)\]\s*(.*)$")
|
||||
|
||||
# Celery-Level → Rippy-Level. DEBUG fällt bewusst weg.
|
||||
LEVEL_MAP = {
|
||||
"CRITICAL": "error",
|
||||
"ERROR": "error",
|
||||
"WARNING": "warning",
|
||||
"INFO": "info",
|
||||
}
|
||||
|
||||
# INFO-Zeilen sind nur interessant, wenn sie einen ZUSTAND melden. Diese Stücke
|
||||
# sind alle oben im echten Log nachweisbar.
|
||||
INFO_DURCHLASSEN = ("ready.", "Connected to", "Trying again", "connection")
|
||||
|
||||
# ... und diese sind reines Grundrauschen.
|
||||
INFO_VERWERFEN = ("mingle:", "task events", "searching for neighbors", "all alone")
|
||||
|
||||
# Zeilen OHNE Celery-Klammer (Startbanner, Python-warnings) kommen nur durch,
|
||||
# wenn sie nach einem echten Problem aussehen.
|
||||
ROHE_FEHLER = ("Traceback", "Error:", "error(", "Errno", "refused", "Cannot")
|
||||
|
||||
# Schutz für die logs-Tabelle: mehr als das pro Minute ist kein Log, das ist ein
|
||||
# Wasserfall.
|
||||
MAX_ZEILEN_JE_MINUTE = 30
|
||||
|
||||
|
||||
def einordnen(zeile: str):
|
||||
"""(level, text) für eine Ausgabezeile — oder None, wenn sie nichts wert ist.
|
||||
|
||||
Reine Funktion, damit das Urteil ohne laufenden Worker prüfbar ist.
|
||||
"""
|
||||
text = (zeile or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
treffer = CELERY_ZEILE.match(text)
|
||||
if not treffer:
|
||||
# Startbanner und warnings-Ausgaben: nur echte Probleme.
|
||||
if any(s in text for s in ROHE_FEHLER):
|
||||
return ("error", text[:500])
|
||||
return None
|
||||
celery_level, _prozess, inhalt = treffer.groups()
|
||||
level = LEVEL_MAP.get(celery_level.upper())
|
||||
if not level or not inhalt:
|
||||
return None
|
||||
if level == "info":
|
||||
if any(s in inhalt for s in INFO_VERWERFEN):
|
||||
return None
|
||||
if not any(s in inhalt for s in INFO_DURCHLASSEN):
|
||||
return None
|
||||
# „ready." heißt: der Worker nimmt jetzt Aufträge an. Das ist die
|
||||
# Meldung, auf die man wartet, wenn nichts passiert.
|
||||
if "ready." in inhalt:
|
||||
return ("success", inhalt[:500])
|
||||
return (level, inhalt[:500])
|
||||
|
||||
|
||||
def quelle_fuer(worker_name: str) -> str:
|
||||
"""Log-Quelle für diesen Worker.
|
||||
|
||||
Gekappt auf 32 Zeichen, weil `logs.source` genau so breit ist (db.py). Ohne
|
||||
das Kappen scheitert das Insert still bei einem langen Worker-Namen — und
|
||||
dann fehlt genau das Log, das man sucht.
|
||||
"""
|
||||
name = (worker_name or "worker").strip() or "worker"
|
||||
return f"w:{name}"[:32]
|
||||
|
||||
|
||||
class Bruecke:
|
||||
"""Nimmt Ausgabezeilen und schreibt die wichtigen nach Rippy.
|
||||
|
||||
`schreiber(level, quelle, text)` und `jetzt()` sind einspritzbar — so ist
|
||||
die Drosselung ohne Datenbank und ohne echte Uhr prüfbar.
|
||||
"""
|
||||
|
||||
def __init__(self, worker_name: str, schreiber, jetzt):
|
||||
self.quelle = quelle_fuer(worker_name)
|
||||
self.schreiber = schreiber
|
||||
self.jetzt = jetzt
|
||||
self._fenster_start = None
|
||||
self._im_fenster = 0
|
||||
self._unterdrueckt = 0
|
||||
|
||||
def zeile(self, text: str) -> bool:
|
||||
"""Eine Ausgabezeile verarbeiten. True, wenn sie nach Rippy ging."""
|
||||
eingeordnet = einordnen(text)
|
||||
if not eingeordnet:
|
||||
return False
|
||||
level, inhalt = eingeordnet
|
||||
|
||||
jetzt = self.jetzt()
|
||||
if self._fenster_start is None or jetzt - self._fenster_start >= 60:
|
||||
# Neues Fenster: erst melden, wieviel im alten verschluckt wurde.
|
||||
if self._unterdrueckt:
|
||||
self._melden(
|
||||
"warning",
|
||||
f"{self._unterdrueckt} weitere Log-Zeilen unterdrückt "
|
||||
"(mehr als 30 pro Minute) — Details im Worker-Log auf der "
|
||||
"Maschine selbst.",
|
||||
)
|
||||
self._fenster_start = jetzt
|
||||
self._im_fenster = 0
|
||||
self._unterdrueckt = 0
|
||||
|
||||
if self._im_fenster >= MAX_ZEILEN_JE_MINUTE:
|
||||
self._unterdrueckt += 1
|
||||
return False
|
||||
self._im_fenster += 1
|
||||
return self._melden(level, inhalt)
|
||||
|
||||
def _melden(self, level: str, text: str) -> bool:
|
||||
try:
|
||||
self.schreiber(level, self.quelle, text)
|
||||
return True
|
||||
except Exception:
|
||||
# Rippy nicht erreichbar → die lokale Datei bleibt der Rückfall.
|
||||
# Hier NICHTS werfen: der Worker soll weiterarbeiten.
|
||||
return False
|
||||
Reference in New Issue
Block a user