diag(api): Vorrats-Schleifen pruefbar machen - der stille except hat gekostet
Ampel / ampel (push) Successful in 28s

Live-Befund: /jobs/<id>/rohdaten findet die 74,1 GB, aber can_retry blieb ueber
80 Sekunden false - der Vorrat der Hintergrund-Schleife war leer. Direkt im
Container aufgerufen fuellt die Funktion ihn korrekt. Warum die Schleife im
Server-Prozess nichts tat, war NICHT zu sehen: Der `except Exception: pass`
verschluckte jeden Grund.

Zwei Konsequenzen, unabhaengig von der Ursache:

1. Die Schleife MELDET ihren Fehler jetzt (print in den Container-Log) statt ihn
   zu verschlucken. Ein Hintergrund-Prozess, der still scheitert, ist schlimmer
   als einer, der laut scheitert.

2. Neuer Diagnose-Endpunkt GET /health/vorraete: nennt fuer Ping- und
   Rohdaten-Vorrat, wie ALT der letzte Durchlauf ist. Bleibt so ein Vorrat leer,
   ist am Endpunkt selbst naemlich nichts zu sehen - er antwortet nur dauerhaft
   "nichts gefunden".

Die naheliegende Erklaerung (asyncio-Tasks ohne Referenz werden vom GC geholt)
ist widerlegt: Die aeltere Ping-Schleife laeuft nach demselben Muster und lebt -
ueber 72 Sekunden blieb jeder /capabilities-Aufruf unter 10 ms, waehrend ein
toter Ping-Vorrat je 30 s einen synchronen Nachping von ~1 s gekostet haette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 13:23:32 +02:00
parent 9fbaed4af8
commit 0d8426d353
+38 -4
View File
@@ -304,6 +304,34 @@ async def health_check():
return {"status": "ok", "service": "api"}
@app.get("/health/vorraete")
async def health_vorraete():
"""Laufen die Hintergrund-Schleifen wirklich? (Diagnose, kein UI-Endpunkt)
Zwei Endpunkte hängen an einem Vorrat, den eine Hintergrund-Schleife füllt:
/capabilities am Celery-Ping, /jobs an der Rohdaten-Suche. Bleibt so ein
Vorrat leer, ist am Endpunkt selbst NICHTS zu sehen — er antwortet nur
dauerhaft „nichts gefunden". Genau daran ging am 26.07.2026 eine Stunde
verloren. `alter_sekunden` sagt, wann die Schleife das letzte Mal
durchgelaufen ist; wächst der Wert über das Intervall, läuft sie nicht mehr.
"""
jetzt = time.monotonic()
return {
"ping": {
"knoten": _PING["knoten"],
"alter_sekunden": None if _PING["stand"] < 0 else round(jetzt - _PING["stand"], 1),
"intervall": PING_INTERVALL_SEKUNDEN,
},
"rohdaten": {
"jobs_im_vorrat": len(_ROHDATEN["treffer"]),
"mit_treffer": sum(1 for v in _ROHDATEN["treffer"].values() if v),
"alter_sekunden": None if _ROHDATEN["stand"] is None
else round(jetzt - _ROHDATEN["stand"], 1),
"intervall": ROHDATEN_INTERVALL_SEKUNDEN,
},
}
@app.get("/")
async def root():
return {
@@ -329,17 +357,22 @@ def _rohdaten_suchen(job_id: str, work_dir: str) -> list:
# /capabilities (v3.15): Der Endpunkt wird alle 4 Sekunden vom Dashboard
# abgefragt und darf NIE am Dateisystem hängen. Ein schlafendes NAS hätte das
# Dashboard sonst für 10 Sekunden je Aufruf eingefroren.
_ROHDATEN = {"treffer": {}}
_ROHDATEN = {"treffer": {}, "stand": None}
ROHDATEN_INTERVALL_SEKUNDEN = 30
async def _rohdaten_schleife():
"""Hält den Rohdaten-Vorrat frisch. Darf nie sterben."""
"""Hält den Rohdaten-Vorrat frisch. Darf nie sterben.
Der Fehler wird GEMELDET, nicht verschluckt: Ein `except Exception: pass`
stand hier zuerst, und als der Vorrat leer blieb, war nicht zu sehen, warum
(Sitzung 26.07.2026 — eine Stunde Rätselraten für eine Zeile Log).
"""
while True:
try:
await asyncio.to_thread(_rohdaten_vorrat_auffrischen)
except Exception: # DB/NAS weg → beim nächsten Durchlauf erneut
pass
except Exception as e: # DB/NAS weg → beim nächsten Durchlauf erneut
print(f"Rohdaten-Vorrat fehlgeschlagen: {type(e).__name__}: {e}")
await asyncio.sleep(ROHDATEN_INTERVALL_SEKUNDEN)
@@ -352,6 +385,7 @@ def _rohdaten_vorrat_auffrischen() -> None:
continue
treffer[zeile["id"]] = _rohdaten_suchen(zeile["id"], work_dir)
_ROHDATEN["treffer"] = treffer
_ROHDATEN["stand"] = time.monotonic()
def _kann_neu_komprimieren(job: dict, work_dir: str) -> bool: