bfb13f44a5
Ampel / ampel (push) Successful in 30s
Zwei Commander-Befunde, beide mit derselben Wurzel: Rippy hat sich selbst
ausgebremst und dann geschwiegen.
## "Wenn der Worker installiert ist, wird dieser Bereich oft neu geladen"
Gemessen statt geraten. Die Antworten von /jobs und /capabilities waren ueber
zwanzig Sekunden byteweise identisch, alle Endpunkte antworteten unter 30 ms -
es wurde also gar nichts neu geladen. Im nginx-Log standen dagegen 97 Antworten
mit HTTP 429.
Drei Fehler griffen ineinander:
1. Das Limit war zu klein fuer Rippy selbst: 100 Anfragen/min, waehrend ein
offener Tab 111/min verursacht (Dashboard 75 + Log-Kasten 24 + Laufwerke 12)
und der Windows-Tray weitere 12/min dazulegt.
2. Der nginx gab die Client-Adresse nicht weiter. Fuer die API kam damit ALLES
von 172.19.0.6 - Browser, zweiter Tab und Tray teilten sich einen Eimer
(812 von 876 Anfragen). Das erklaert die Kopplung an den Worker: tray.py
fragt /api/jobs ueber Port 80, also durch denselben Proxy.
3. Ein abgewiesener Abruf leerte das UI. `catch(() => [])` heisst "es gibt
keine Jobs" - richtig waere "ich weiss gerade nichts Neues". Fuer einen Takt
stand "Keine Jobs", die Zaehler sprangen auf (0), vier Sekunden spaeter war
alles zurueck.
Behoben: X-Real-IP im nginx, Grenze auf 600/min mit vorgerechneter Herleitung,
jeder Fehlschlag laesst den alten Stand stehen (null statt []), axios bekommt
eine Zeitgrenze, und das Dashboard trennt schnelle Daten (Jobs/Laufwerke, 4 s)
von langsamen (Hardware/Worker/Ablagen, 12 s) - 75/min werden zu 30/min.
Ein greifendes Limit steht ab jetzt im Log, gedrosselt auf eine Meldung pro
Client und Minute.
## "Hier gibt es den Button 'neu' aber WAS wird dann gemacht?"
Immer die Komprimierung - auch bei einem Job, dessen RIP abgebrochen war. Am
26.07.2026 waeren aus 5,1 GB Bruchstueck (von rund 40 GB) brav ein Film
geworden, der bei 12 % aufhoert.
Die Phase war nach `status = "failed"` nicht mehr feststellbar, also wird sie
jetzt vermerkt (rip_fertig in den Job-Metadaten: false beim Rip-Start, true bei
der Uebergabe an die Kompression). Daraus folgt die Beschriftung: "Neu
komprimieren", "Neu rippen" - oder bei Bestandsjobs ohne Vermerk ein Dialog,
der beide Wege erklaert und die Groesse der Rohdaten als Entscheidungshilfe
nennt. Geraten wird nicht. Fuer den Rip-Fall gibt es POST
/jobs/{id}/retry-rip: neuer Job mit neuer ID (sonst laege das Bruchstueck im
Roh-Verzeichnis des neuen Rips), Titel/Ablage/Sprachwahl uebernommen, mit
ehrlicher Absage wenn keine Disc im Laufwerk liegt.
10 neue Tests (289 gruen), darunter eine Kopplungspruefung: der Name der
Phasen-Marke muss in worker/tasks.py und api/phasen.py zusammenpassen - genau
diese Sorte Auseinanderdriften hat die Zombie-Erkennung ein Release lang blind
gemacht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
"""Tests für die Phasen-Erkennung — was darf der „Neu"-Knopf anbieten?"""
|
|
|
|
import json
|
|
|
|
import phasen
|
|
|
|
|
|
def test_laufender_job_bietet_nichts():
|
|
assert phasen.retry_art({"status": "running"}) == phasen.NICHTS
|
|
assert phasen.retry_art({"status": "completed"}) == phasen.NICHTS
|
|
|
|
|
|
def test_toter_transcode_bietet_komprimieren():
|
|
job = {"status": "failed", "meta": json.dumps({"rip_fertig": True})}
|
|
assert phasen.retry_art(job) == phasen.NEU_KOMPRIMIEREN
|
|
|
|
|
|
def test_toter_rip_bietet_rippen():
|
|
"""Der Vorfall vom 26.07.2026: 5,1 GB von 40 GB. Komprimieren wäre falsch."""
|
|
job = {"status": "failed", "meta": json.dumps({"rip_fertig": False, "year": 1988})}
|
|
assert phasen.retry_art(job) == phasen.NEU_RIPPEN
|
|
|
|
|
|
def test_bestandsjob_ohne_marke_ist_unklar():
|
|
"""Jobs von VOR dieser Änderung dürfen nicht geraten werden."""
|
|
assert phasen.retry_art({"status": "failed", "meta": None}) == phasen.UNKLAR
|
|
assert phasen.retry_art({"status": "failed"}) == phasen.UNKLAR
|
|
assert phasen.retry_art(
|
|
{"status": "failed", "meta": json.dumps({"year": 1988})}
|
|
) == phasen.UNKLAR
|
|
|
|
|
|
def test_kaputtes_json_ist_unklar_statt_absturz():
|
|
"""/jobs darf an einer krummen meta-Zeile nicht scheitern."""
|
|
assert phasen.retry_art({"status": "failed", "meta": "{kein json"}) == phasen.UNKLAR
|
|
assert phasen.retry_art({"status": "failed", "meta": "[1,2]"}) == phasen.UNKLAR
|
|
assert phasen.retry_art({"status": "failed", "meta": 7}) == phasen.UNKLAR
|
|
|
|
|
|
def test_marke_wird_auch_als_dict_gelesen():
|
|
"""Der Detail-Endpunkt hat die Metadaten schon geparst."""
|
|
job = {"status": "failed", "meta": {"rip_fertig": True}}
|
|
assert phasen.retry_art(job) == phasen.NEU_KOMPRIMIEREN
|
|
|
|
|
|
def test_der_schluessel_heisst_im_worker_genauso():
|
|
"""Zwei Container, kein geteiltes Paket — die Marke muss zusammenpassen.
|
|
|
|
Ohne diese Prüfung ist ein Tippfehler in einer der beiden Dateien lautlos:
|
|
Der Worker schreibt `rip_fertig`, die API liest `ripFertig`, und JEDER Job
|
|
wäre für immer „unklar". Genau diese Sorte Auseinanderdriften hat die
|
|
Zombie-Erkennung ein Release lang blind gemacht („running" vs. „ripping").
|
|
"""
|
|
import pathlib
|
|
import re
|
|
|
|
quelle = (
|
|
pathlib.Path(__file__).resolve().parents[1] / "worker" / "tasks.py"
|
|
).read_text(encoding="utf-8")
|
|
# Der Worker schreibt die Marke über eine Konstante — deren Wert muss hier
|
|
# ankommen.
|
|
treffer = re.search(r'^RIP_FERTIG\s*=\s*"([^"]+)"', quelle, re.MULTILINE)
|
|
assert treffer, "worker/tasks.py definiert RIP_FERTIG nicht mehr"
|
|
assert treffer.group(1) == phasen.RIP_FERTIG
|