diag(api): Vorrats-Schleifen pruefbar machen - der stille except hat gekostet
Ampel / ampel (push) Successful in 28s
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:
+38
-4
@@ -304,6 +304,34 @@ async def health_check():
|
|||||||
return {"status": "ok", "service": "api"}
|
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("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {
|
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
|
# /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
|
# abgefragt und darf NIE am Dateisystem hängen. Ein schlafendes NAS hätte das
|
||||||
# Dashboard sonst für 10 Sekunden je Aufruf eingefroren.
|
# Dashboard sonst für 10 Sekunden je Aufruf eingefroren.
|
||||||
_ROHDATEN = {"treffer": {}}
|
_ROHDATEN = {"treffer": {}, "stand": None}
|
||||||
ROHDATEN_INTERVALL_SEKUNDEN = 30
|
ROHDATEN_INTERVALL_SEKUNDEN = 30
|
||||||
|
|
||||||
|
|
||||||
async def _rohdaten_schleife():
|
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:
|
while True:
|
||||||
try:
|
try:
|
||||||
await asyncio.to_thread(_rohdaten_vorrat_auffrischen)
|
await asyncio.to_thread(_rohdaten_vorrat_auffrischen)
|
||||||
except Exception: # DB/NAS weg → beim nächsten Durchlauf erneut
|
except Exception as e: # DB/NAS weg → beim nächsten Durchlauf erneut
|
||||||
pass
|
print(f"Rohdaten-Vorrat fehlgeschlagen: {type(e).__name__}: {e}")
|
||||||
await asyncio.sleep(ROHDATEN_INTERVALL_SEKUNDEN)
|
await asyncio.sleep(ROHDATEN_INTERVALL_SEKUNDEN)
|
||||||
|
|
||||||
|
|
||||||
@@ -352,6 +385,7 @@ def _rohdaten_vorrat_auffrischen() -> None:
|
|||||||
continue
|
continue
|
||||||
treffer[zeile["id"]] = _rohdaten_suchen(zeile["id"], work_dir)
|
treffer[zeile["id"]] = _rohdaten_suchen(zeile["id"], work_dir)
|
||||||
_ROHDATEN["treffer"] = treffer
|
_ROHDATEN["treffer"] = treffer
|
||||||
|
_ROHDATEN["stand"] = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
def _kann_neu_komprimieren(job: dict, work_dir: str) -> bool:
|
def _kann_neu_komprimieren(job: dict, work_dir: str) -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user