feat(worker): Zombie-Erkennung - Jobs, an denen niemand arbeitet

Nach einem Absturz oder Rebuild blieb ein Job auf 'transcoding' stehen,
obwohl weder ein Prozess lief noch etwas in den Queues stand. Folge: keine
Anzeige, kein Download - und der Knopf "Neu komprimieren" fehlte, weil
_kann_neu_komprimieren (api/main.py) status=='failed' verlangt. Der Job war
unerreichbar, obwohl die Rohdateien vollstaendig dalagen.

zombies.py haelt beim Worker-Start die Jobs in 'ripping'/'transcoding'/
'canceling' gegen Celerys active/reserved/scheduled und setzt sie ehrlich
auf 'failed', wenn niemand daran arbeitet.

Drei Sicherungen, weil ein falsch getoeteter Job teurer ist als eine
stehengebliebene Leiche:
- nur beim Start (da ist "es lief nichts" eindeutig; ein periodischer Lauf
  koennte einen Job erwischen, der legitim in der Warteschlange wartet)
- 120 s Gnadenfrist (Celery stellt unbestaetigte Aufgaben erneut zu)
- Vollzaehligkeit: antworten weniger Knoten als laut Herzschlag online sind,
  wird NICHTS gewertet - sonst waere der laufende Job eines beschaeftigten
  Remote-Workers eine falsche Leiche

Die Job-ID wird per Textsuche ueber die Inspektions-Antwort gefunden, nicht
per Position: sie steht bei rip_disc an zweiter, bei transcode_files an
erster Stelle, und Celery liefert args je nach Version als Liste oder Text.

13 Tests ohne Postgres/Redis, darunter "laufender Job wird nicht angetastet"
und "schweigender Worker verhindert jedes Urteil".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 21:04:19 +02:00
parent 5cd1876c90
commit e0cb7b3ddc
4 changed files with 399 additions and 0 deletions
+24
View File
@@ -33,6 +33,30 @@ celery_app.conf.update(
# zuverlässig im sys.path (ModuleNotFoundError 'caps', Deploy 23.07.).
import caps # noqa: E402
import db # noqa: E402
import zombies # noqa: E402
@worker_ready.connect
def raeume_job_leichen_auf(**kwargs):
"""Nach der Gnadenfrist: Jobs, an denen niemand arbeitet, ehrlich auf
'failed' setzen (Details und Sicherungen in zombies.py).
Läuft im Hintergrund-Thread — der Worker soll sofort Aufgaben annehmen und
nicht zwei Minuten auf die Aufräumung warten.
"""
import threading
import time
def spaeter():
time.sleep(zombies.GNADENFRIST_SEKUNDEN)
try:
db.init_db()
bericht = zombies.raeume_zombies_auf(celery_app, db)
print(f"Zombie-Erkennung: {bericht}")
except Exception as e: # darf den Worker nie mitnehmen
print(f"Zombie-Erkennung fehlgeschlagen: {e}")
threading.Thread(target=spaeter, daemon=True, name="zombie-erkennung").start()
@worker_ready.connect