feat(worker): der externe Worker wird erwachsen - Log in Rippy, Anzeige, Slots
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:
Hitonabi
2026-07-26 14:17:14 +02:00
parent be04a9772e
commit 87484d6863
7 changed files with 587 additions and 21 deletions
+157
View File
@@ -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