Documentation

Gestion des erreurs

Les hooks tournent à l’intérieur de la requête qu’ils observent, donc la façon dont leurs échecs sont traités compte. Cette page est la politique exacte.

Les deux politiques

hooks_raise contrôle ce qui se passe quand un hook lève une erreur.

hooks_raise comportement
True (défaut) l’exception du hook se propage hors de l’appel.
False le hook est ignoré avec un RuntimeWarning et l’appel continue.

Il se définit par instance et peut être surchargé par appel (hooks_raise= sur predict_batch, system_one, Router.route, Router.predict). None par appel signifie « utilise la valeur de l’instance ».

# 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 attrape Exception. Tout ce qui n’est pas une Exception (voir BaseException) n’est jamais avalé, même avec hooks_raise=False.

Ce qui tourne quand quelque chose échoue

Le cycle de vie de la prédiction est enveloppé dans try / except / finally, donc les hooks de nettoyage tournent en cas d’échec.

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)

Matrice de défaillance, par événement et par runtime :

événement runtime s’il lève
on_predict_start Agent / Router hooks_raise=True : on_error et on_predict_end tournent quand même, puis l’exception se propage. False : avertir et continuer (les mutations faites avant la levée restent).
inférence Agent / Router ctx.error défini, on_error tourne, on_predict_end tourne, l’exception se propage.
on_error Agent / Router ne masque jamais l’exception d’origine ; enchaîné comme __context__.
on_predict_end (chemin du succès) Agent / Router hooks_raise=True : se propage (le résultat est calculé mais l’appel échoue). False : avertir.
on_predict_end (chemin d’échec) Agent / Router ne masque jamais l’exception d’origine ; enchaîné comme __context__.
on_route Router se propage directement ; il n’y a pas encore de contexte de prédiction.
on_load Router se propage directement ; le checkpoint reste construit et résident.
on_evict Router se propage directement ; le checkpoint est déjà libéré.

Conséquences à connaître :

  • Un on_load qui échoue laisse le modèle en cache, donc le prochain load le renvoie sans redéclencher on_load.
  • Un on_predict_end qui échoue sur le chemin du succès signifie que l’appelant reçoit une exception au lieu d’un résultat, même si l’inférence a réussi. Utilise hooks_raise=False pour les hooks de fin qui sont de purs effets de bord.

Enchaînement des exceptions

Quand un hook échoue alors qu’une autre exception est déjà en train de se propager, l’exception d’origine est relancée et celle du hook est attachée comme __context__. La cause racine n’est jamais perdue.

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

La même règle s’applique à un on_predict_end qui échoue sur le chemin d’échec.

BaseException

dispatch attrape Exception, pas BaseException, donc KeyboardInterrupt et SystemExit se propagent toujours. Ils déclenchent quand même la branche except BaseException du cycle de vie de la prédiction, ce qui signifie que on_error et on_predict_end tournent avant que le processus ne se déroule. Garde ces hooks rapides et non bloquants si la latence d’interruption t’importe.

Erreurs de configuration

Une mauvaise configuration échoue immédiatement avec TypeError, avant toute inférence :

cas levé à exemple
classe au lieu d’instance construction hooks=[MyHook]
aucune méthode de cycle de vie construction hooks=[object()]
événement non appelable construction on_predict_start = 5
hook de commodité non appelable construction on_predict_start=123
appelable simple dans hooks= construction hooks=[lambda ctx: None]

Les hooks par appel sont validés quand l’appel est fait, donc un mauvais hook par appel lève TypeError depuis predict/system_one plutôt qu’à la construction.

Avertissements

Avec hooks_raise=False, chaque hook qui échoue émet un RuntimeWarning nommant le type de hook et l’événement :

laya: hook Metrics.on_predict_end failed: connection reset

L’avertissement est émis une fois par échec, pas une fois par définition de hook, donc un hook instable sous charge peut être bruyant. Agrège ou limite le débit à l’intérieur du hook si cela compte.

Délais d’attente

hooks_timeout borne chaque appel de hook en secondes. Un hook encore en cours après la limite est traité comme un échec de hook : TimeoutError quand hooks_raise=True, un RuntimeWarning quand False. None (le défaut) signifie aucune limite.

laya.load("convaiinnovations/laya", on_predict_end=metrics, hooks_timeout=2.0)

Il peut être défini par instance ou surchargé par appel sur predict_batch, system_one, Router.route, Router.predict et ONNXAgent.system_one. La valeur doit être positive ; 0 ou un nombre négatif lève ValueError à l’endroit où il est défini, plutôt que de courir sur un join de longueur nulle.

Un hook temporisé tourne sur un thread de travail dans une copie du contexte contextvars de l’appelant, donc un id de requête ou un span de traçage défini par l’appelant est visible par le hook.

Une mise en garde honnête : Python ne peut pas interrompre un thread, donc un hook expiré continue de tourner en arrière-plan. Le timeout borne combien de temps la requête attend, pas combien de temps le hook vit. Utilise-le pour garder une requête servie réactive, pas pour récupérer le travail. Pour un hook qui peut se bloquer, donne aussi à l’appel sous-jacent son propre timeout (un timeout de socket ou HTTP). Comme le thread ne peut pas être récupéré, un hook qui se bloque à chaque appel fait croître les threads d’un par appel ; donne à un hook qui peut se bloquer sa propre borne plutôt que de compter sur hooks_timeout pour l’arrêter.

Pour un hook async, la coroutine tourne sur la boucle d’événements ; un timeout côté appelant revient quand même après la limite, et la coroutine continue de tourner sur la boucle.

Le timeout libère aussi le verrou hooks_concurrent=False : dispatch attend le hook seulement jusqu’à la limite, puis passe à la suite, tandis que le hook expiré continue de tourner hors du verrou. Donc le verrou sérialise les hooks qui finissent à temps, pas tous les hooks qui ont été démarrés ; un hook qui dépasse ne bloque plus ceux derrière lui.

Choisir une politique

type de hook recommandé pourquoi
politique / garde-fou / masquage hooks_raise=True une politique qui échoue silencieusement est un trou de sécurité.
audit / journalisation hooks_raise=True en test, souvent False en production perdre un enregistrement d’audit doit être bruyant, mais pas forcément fatal.
métriques / traçage hooks_raise=False l’observabilité ne doit pas faire échouer la requête.
lecture/écriture de cache hooks_raise=True un cache cassé doit se voir, pas manquer silencieusement.

Tu peux mélanger : installe un garde-fou avec le défaut de l’instance et donne à un hook de télémétrie son propre try/except, ou utilise un hooks_raise distinct par appel.

Voir aussi