Fehlerbehandlung
Hooks laufen innerhalb der Anfrage, die sie beobachten, daher ist wichtig, wie ihre Fehler behandelt werden. Diese Seite beschreibt die genaue Richtlinie.
- Die beiden Richtlinien
- Was läuft, wenn etwas fehlschlägt
- Verkettung von Ausnahmen
- BaseException
- Konfigurationsfehler
- Warnungen
- Wahl einer Richtlinie
Die beiden Richtlinien
hooks_raise steuert, was passiert, wenn ein Hook eine Ausnahme auslöst.
hooks_raise |
Verhalten |
|---|---|
True (Standard) |
die Hook-Ausnahme wird aus dem Aufruf heraus weitergegeben. |
False |
der Hook wird mit einer RuntimeWarning übersprungen und der Aufruf läuft weiter. |
Er wird pro Instanz gesetzt und kann pro Aufruf überschrieben werden (hooks_raise= bei
predict_batch, system_one, Router.route, Router.predict). None pro Aufruf bedeutet „den
Wert der Instanz verwenden“.
# strict: a broken audit hook fails the request
laya.load("convaiinnovations/laya", on_predict_end=audit, hooks_raise=True)
# lenient: telemetry must never take down a served request
laya.load("convaiinnovations/laya", on_predict_end=metrics, hooks_raise=False)
dispatch fängt Exception. Alles, was keine Exception ist (siehe
BaseException), wird nie verschluckt, auch nicht mit hooks_raise=False.
Was läuft, wenn etwas fehlschlägt
Der predict-Lebenszyklus ist in try / except / finally gehüllt, sodass Aufräum-Hooks bei einem
Fehler laufen.
try:
on_predict_start
inference
except BaseException as exc:
ctx.error = exc
on_error (best effort; cannot mask exc)
raise
finally:
elapsed_ms, usage
on_predict_end (best effort; cannot mask exc on the failure path)
Fehlermatrix, pro Ereignis und Laufzeit:
| Ereignis | Laufzeit | wenn es eine Ausnahme auslöst |
|---|---|---|
on_predict_start |
Agent / Router | hooks_raise=True: on_error und on_predict_end laufen trotzdem, dann wird die Ausnahme weitergegeben. False: warnen und fortfahren (Mutationen, die vor dem Auslösen gemacht wurden, bleiben erhalten). |
| inference | Agent / Router | ctx.error gesetzt, on_error läuft, on_predict_end läuft, Ausnahme wird weitergegeben. |
on_error |
Agent / Router | verdeckt nie die ursprüngliche Ausnahme; wird als __context__ verkettet. |
on_predict_end (Erfolgspfad) |
Agent / Router | hooks_raise=True: wird weitergegeben (das Ergebnis ist berechnet, aber der Aufruf schlägt fehl). False: warnen. |
on_predict_end (Fehlerpfad) |
Agent / Router | verdeckt nie die ursprüngliche Ausnahme; wird als __context__ verkettet. |
on_route |
Router | wird direkt weitergegeben; es gibt noch keinen predict-Kontext. |
on_load |
Router | wird direkt weitergegeben; der Checkpoint bleibt gebaut und resident. |
on_evict |
Router | wird direkt weitergegeben; der Checkpoint ist bereits freigegeben. |
Wissenswerte Konsequenzen:
- Ein fehlschlagender
on_loadlässt das Modell im Cache, sodass das nächsteloades zurückgibt, ohneon_loaderneut auszulösen. - Ein fehlschlagender
on_predict_endauf dem Erfolgspfad bedeutet, dass der Aufrufer eine Ausnahme statt eines Ergebnisses erhält, obwohl die Inferenz erfolgreich war. Verwendehooks_raise=Falsefür End-Hooks, die reine Seiteneffekte sind.
Verkettung von Ausnahmen
Wenn ein Hook fehlschlägt, während bereits eine andere Ausnahme weitergegeben wird, wird die
ursprüngliche Ausnahme erneut ausgelöst und die Ausnahme des Hooks als __context__ angehängt. Die
Grundursache geht nie verloren.
class BadTelemetry:
def on_error(self, ctx):
raise RuntimeError("telemetry down")
try:
agent.system_one(state, questions, hooks=[BadTelemetry()])
except RuntimeError as exc:
assert exc.__context__ is not None # the telemetry failure
Dieselbe Regel gilt für einen fehlschlagenden on_predict_end auf dem Fehlerpfad.
BaseException
dispatch fängt Exception, nicht BaseException, sodass KeyboardInterrupt und SystemExit
immer weitergegeben werden. Sie lösen trotzdem den except BaseException-Zweig des
predict-Lebenszyklus aus, was bedeutet, dass on_error und on_predict_end laufen, bevor der
Prozess sich auflöst. Halte diese Hooks schnell und nicht blockierend, wenn dir die
Unterbrechungslatenz wichtig ist.
Konfigurationsfehler
Eine fehlerhafte Konfiguration schlägt sofort mit TypeError fehl, vor jeder Inferenz:
| Fall | ausgelöst bei | Beispiel |
|---|---|---|
| Klasse statt Instanz | Konstruktion | hooks=[MyHook] |
| keine Lebenszyklusmethode | Konstruktion | hooks=[object()] |
| nicht aufrufbares Ereignis | Konstruktion | on_predict_start = 5 |
| nicht aufrufbarer Komfort-Hook | Konstruktion | on_predict_start=123 |
einfaches Callable in hooks= |
Konstruktion | hooks=[lambda ctx: None] |
Hooks pro Aufruf werden validiert, wenn der Aufruf erfolgt, sodass ein schlechter Hook pro Aufruf
TypeError aus predict/system_one auslöst und nicht bei der Konstruktion.
Warnungen
Mit hooks_raise=False gibt jeder fehlschlagende Hook eine RuntimeWarning aus, die den Hook-Typ
und das Ereignis nennt:
laya: hook Metrics.on_predict_end failed: connection reset
Die Warnung wird einmal pro Fehler ausgegeben, nicht einmal pro Hook-Definition, sodass ein unzuverlässiger Hook unter Last laut werden kann. Aggregiere oder begrenze die Rate innerhalb des Hooks, wenn das wichtig ist.
Timeouts
hooks_timeout begrenzt jeden Hook-Aufruf in Sekunden. Ein Hook, der nach dem Limit noch läuft,
wird als Hook-Fehler behandelt: TimeoutError, wenn hooks_raise=True, eine RuntimeWarning, wenn
False. None (der Standard) bedeutet kein Limit.
laya.load("convaiinnovations/laya", on_predict_end=metrics, hooks_timeout=2.0)
Es kann pro Instanz gesetzt oder pro Aufruf überschrieben werden bei predict_batch, system_one,
Router.route, Router.predict und ONNXAgent.system_one. Der Wert muss positiv sein; 0 oder
eine negative Zahl löst ValueError an der Stelle aus, an der er gesetzt wird, statt bei einem
join mit Länge null in ein Rennen zu geraten.
Ein zeitlich begrenzter Hook läuft in einem Worker-Thread in einer Kopie des
contextvars-Kontexts des Aufrufers, sodass eine vom Aufrufer gesetzte Anfrage-ID oder ein
Tracing-Span für den Hook sichtbar ist.
Ein ehrlicher Vorbehalt: Python kann einen Thread nicht unterbrechen, also läuft ein Hook, dessen
Zeit abgelaufen ist, im Hintergrund weiter. Das Timeout begrenzt, wie lange die Anfrage wartet,
nicht wie lange der Hook lebt. Nutze es, um eine bediente Anfrage reaktionsfähig zu halten, nicht um
die Arbeit zurückzugewinnen. Gib einem Hook, der hängen bleiben kann, zusätzlich dem zugrunde
liegenden Aufruf ein eigenes Timeout (ein Socket- oder HTTP-Timeout). Weil der Thread nicht
zurückgewonnen werden kann, wächst bei einem Hook, der bei jedem Aufruf hängt, die Thread-Zahl um
einen pro Aufruf; gib einem Hook, der hängen bleiben kann, eine eigene Grenze, statt dich darauf zu
verlassen, dass hooks_timeout ihn stoppt.
Bei einem asynchronen Hook läuft die Koroutine auf der Ereignisschleife; ein Timeout auf der aufrufenden Seite kehrt trotzdem nach dem Limit zurück, und die Koroutine läuft auf der Schleife weiter.
Das Timeout gibt auch das Lock von hooks_concurrent=False frei: dispatch wartet nur bis zum Limit
auf den Hook und geht dann weiter, während der zeitlich begrenzte Hook außerhalb des Locks
weiterläuft. Das Lock serialisiert also die Hooks, die rechtzeitig fertig werden, nicht jeden Hook,
der je gestartet wurde; ein Hook, der überzieht, blockiert nicht mehr die dahinter.
Wahl einer Richtlinie
| Hook-Art | empfohlen | warum |
|---|---|---|
| Richtlinie / Leitplanke / Redaktion | hooks_raise=True |
eine Richtlinie, die still fehlschlägt, ist ein Sicherheitsloch. |
| Audit / Logging | hooks_raise=True in Tests, in Produktion oft False |
einen Audit-Eintrag zu verlieren, sollte laut sein, aber nicht unbedingt fatal. |
| Metriken / Tracing | hooks_raise=False |
Observability darf die Anfrage nicht scheitern lassen. |
| Cache-Lesen/-Schreiben | hooks_raise=True |
ein kaputter Cache sollte sichtbar werden, nicht still danebengreifen. |
Du kannst mischen: installiere eine Leitplanke mit dem Instanzstandard und gib einem Telemetrie-Hook
ein eigenes try/except, oder verwende ein separates hooks_raise pro Aufruf.
Siehe auch
- Lebenszyklus: die Form
try/except/finallyim Kontext. - Muster und Anti-Muster: häufige Fehler bei der Fehlerbehandlung.