fix(api): os.path.isdir hing im Kernel und toetete die Schleife - harte Zeitgrenze
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
Ursache gefunden, nicht geraten. Der neue Diagnose-Endpunkt sagte `rohdaten.alter_sekunden: null` - die Funktion war NIE EINMAL fertig geworden, und kein Fehler war gemeldet. Der Blick auf die Threads des API-Prozesses zeigte zwei im Zustand **D** (uninterruptible sleep, im Kernel blockiert): tid=1600034 name=uvicorn state=D tid=1600037 name=uvicorn state=D Der Mechanismus: Die Schleife startete ihren ersten Durchlauf, waehrend Rippy beim Container-Start die CIFS-Freigabe neu einhaengte. Ihr `os.path.isdir` blieb im Kernel stecken, `asyncio.to_thread` kam nie zurueck, die Schleife erreichte ihr `sleep` nie - und war damit fuer immer tot. Sichtbar war nur, dass can_retry dauerhaft false blieb. Ein Timeout um den Aufruf haette nichts geholfen: Ein im Kernel haengender Thread laesst sich aus Python nicht abbrechen, jeder Versuch haette einen weiteren Thread verbrannt, bis der Pool leer ist. Ein Kind-PROZESS laesst sich abbrechen. Geprueft wird jetzt mit `timeout 4 ls -d <pfad>` - dasselbe Werkzeug, das mounts.ist_erreichbar seit dem 24.07.2026 fuer genau dieses Problem benutzt (dort fuer den toten NAS-Mount). Laeuft es in die Zeitgrenze, gilt das Verzeichnis als "nicht da": Ein Ort, den man nicht in Sekunden ansehen kann, ist fuer einen Rip ohnehin unbrauchbar. os.listdir bleibt fuer /app/media selbst - das ist ein lokales Verzeichnis, die Freigaben sind Unterordner davon. Und os.path.isdir bleibt fuer den Rueckfall auf /app/temp/raw: ein Docker-Volume, dort kann nichts haengen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+20
-8
@@ -6,6 +6,7 @@ from typing import List, Optional, Dict
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import time
|
||||
import uuid
|
||||
@@ -344,13 +345,16 @@ async def root():
|
||||
def _rohdaten_suchen(job_id: str, work_dir: str) -> list:
|
||||
"""Wo liegen die Roh-MKVs dieses Jobs? (Details in rohdaten.py)
|
||||
|
||||
Kann LANGSAM sein: Ein schlafendes NAS lässt `os.path.isdir` bis zum
|
||||
CIFS-Timeout hängen — auf dieser VM gemessene 10 Sekunden, während der Mount
|
||||
nach einem Container-Neustart hochkam. Deshalb nur dort direkt aufrufen, wo
|
||||
ein Mensch auf genau diese Antwort wartet (/rohdaten, retry-transcode).
|
||||
Für die Job-Liste gibt es den Vorrat unten.
|
||||
Geprüft wird mit `rohdaten.verzeichnis_da` und NICHT mit os.path.isdir:
|
||||
Letzteres hing am 26.07.2026 unbegrenzt im Kernel, während Rippy die
|
||||
CIFS-Freigabe neu einhängte — und nahm die Hintergrund-Schleife dauerhaft
|
||||
mit. Ausführliche Begründung dort.
|
||||
|
||||
Bleibt trotzdem der langsame Weg (mehrere Kind-Prozesse, im schlechtesten
|
||||
Fall je vier Sekunden). Für die Job-Liste, die das Dashboard alle vier
|
||||
Sekunden abfragt, gibt es deshalb den Vorrat unten.
|
||||
"""
|
||||
return rohdaten.suche(job_id, work_dir, os.listdir, os.path.isdir)
|
||||
return rohdaten.suche(job_id, work_dir, os.listdir, rohdaten.verzeichnis_da)
|
||||
|
||||
|
||||
# Vorrat für die Job-Liste. Dasselbe Muster wie beim Celery-Ping in
|
||||
@@ -410,7 +414,9 @@ def _kann_neu_komprimieren(job: dict, work_dir: str) -> bool:
|
||||
vorrat = _ROHDATEN["treffer"]
|
||||
if job["id"] in vorrat:
|
||||
return bool(vorrat[job["id"]])
|
||||
return os.path.isdir(os.path.join(rohdaten.RAW_STANDARD, job["id"]))
|
||||
# Nur der lokale Ort: /app/temp ist ein Docker-Volume, os.path.isdir kann
|
||||
# dort nicht hängen (im Gegensatz zu allem unter /app/media).
|
||||
return os.path.isdir(posixpath.join(rohdaten.RAW_STANDARD, job["id"]))
|
||||
|
||||
|
||||
@app.get("/jobs", response_model=List[Job])
|
||||
@@ -443,7 +449,13 @@ async def get_jobs():
|
||||
|
||||
|
||||
def _rohdaten_groesse(pfade: list) -> tuple:
|
||||
"""(Bytes, Dateizahl) der Roh-MKVs (Details in rohdaten.py)."""
|
||||
"""(Bytes, Dateizahl) der Roh-MKVs (Details in rohdaten.py).
|
||||
|
||||
Hier bleibt os.* stehen: Diese Funktion wird nur aufgerufen, NACHDEM
|
||||
`verzeichnis_da` den Pfad innerhalb von Sekunden bestätigt hat — der Mount
|
||||
antwortet also. Und sie läuft nur, wenn ein Mensch auf die Zahl wartet
|
||||
(/rohdaten, Löschen), nie in der Hintergrund-Schleife.
|
||||
"""
|
||||
return rohdaten.groesse(pfade, os.listdir, os.path.isfile, os.path.getsize)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user