Dokumentation

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

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_load lässt das Modell im Cache, sodass das nächste load es zurückgibt, ohne on_load erneut auszulösen.
  • Ein fehlschlagender on_predict_end auf dem Erfolgspfad bedeutet, dass der Aufrufer eine Ausnahme statt eines Ergebnisses erhält, obwohl die Inferenz erfolgreich war. Verwende hooks_raise=False fü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