文档导航

错误处理

错误处理

钩子运行在它所观察的请求内部,所以它们的失败如何处理很重要。这一页是确切的策略。

两条策略

hooks_raise 控制钩子抛异常时会发生什么。

hooks_raise 行为
True(默认) 钩子的异常从调用中向外传播。
False 钩子被跳过并给一个 RuntimeWarning,调用继续。

它按实例设置,也可以逐调用覆盖(predict_batch、system_one、Router.route、Router.predict 上的 hooks_raise=)。逐调用的 None 表示「用实例的值」。

# 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 捕获 Exception。任何不是 Exception 的东西(见 BaseException) 永远不会被吞掉,即便 hooks_raise=False 也一样。

某个东西失败时还会运行什么

predict 生命周期被包在 try / except / finally 里,所以清理钩子在失败时也会运行。

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)

失败矩阵,按事件和运行时:

事件 运行时 如果它抛异常
on_predict_start Agent / Router hooks_raise=True:on_error 和 on_predict_end 仍然运行,然后异常传播。False:警告并继续(抛出之前做的改动保留)。
inference Agent / Router 设置 ctx.error,运行 on_error,运行 on_predict_end,异常传播。
on_error Agent / Router 永远不会遮住原来的异常;以 __context__ 链接。
on_predict_end(成功路径) Agent / Router hooks_raise=True:传播(结果算出来了但调用失败)。False:警告。
on_predict_end(失败路径) Agent / Router 永远不会遮住原来的异常;以 __context__ 链接。
on_route Router 直接传播;这时还没有 predict 上下文。
on_load Router 直接传播;checkpoint 保持构建好并常驻。
on_evict Router 直接传播;checkpoint 已经被释放。

值得知道的后果:

  • 一个失败的 on_load 会让模型留在缓存里,所以下一次 load 直接返回它,不会再触发 on_load。
  • 成功路径上一个失败的 on_predict_end 意味着调用方拿到的是一个异常而不是结果,即便推理成功 了。对于纯副作用的 end 钩子,用 hooks_raise=False。

异常链

当一个钩子在另一个异常已经在传播时失败,原来的异常被重新抛出,钩子的异常作为 __context__ 附上。 根因永远不丢。

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

同样的规则适用于失败路径上一个失败的 on_predict_end。

BaseException

dispatch 捕获 Exception,不是 BaseException,所以 KeyboardInterrupt 和 SystemExit 总会 传播。它们仍然触发 predict 生命周期的 except BaseException 分支,这意味着 on_error 和 on_predict_end 在进程展开之前运行。如果你在意中断延迟,就让这些钩子快且非阻塞。

配置错误

坏的配置在任何推理之前就以 TypeError 快速失败:

情况 抛出时机 例子
类而不是实例 构造时 hooks=[MyHook]
没有生命周期方法 构造时 hooks=[object()]
事件不可调用 构造时 on_predict_start = 5
便捷钩子不可调用 构造时 on_predict_start=123
hooks= 里是普通可调用对象 构造时 hooks=[lambda ctx: None]

逐调用钩子在调用发生时校验,所以一个坏的逐调用钩子从 predict/system_one 抛 TypeError,而 不是构造时。

警告

hooks_raise=False 时,每个失败的钩子发出一个 RuntimeWarning,点名钩子类型和事件:

laya: hook Metrics.on_predict_end failed: connection reset

警告每次失败发出一次,而不是每个钩子定义一次,所以一个在高负载下时好时坏的钩子可能很吵。如果 这要紧,就在钩子内部聚合并限流。

超时

hooks_timeout 以秒为单位给每次钩子调用设上界。超过限制后仍在运行的钩子被当作钩子失败: hooks_raise=True 时是 TimeoutError,False 时是一个 RuntimeWarning。None(默认)表示 不限。

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

它可以按实例设置,也可以在 predict_batch、system_one、Router.route、Router.predict 和 ONNXAgent.system_one 上逐调用覆盖。这个值必须是正数;0 或负数在设置它的那个点抛 ValueError,而不是在一个零长度的 join 上竞争。

带超时的钩子在一个 worker 线程上运行,用的是调用方 contextvars 上下文的一份副本,所以调用方 设置的请求 id 或 tracing span 对钩子可见。

一个诚实的提醒:Python 无法中断线程,所以超时的钩子会在后台继续运行。超时给的是请求等待多久的 上界,不是钩子活多久的上界。用它来保持一个正在服务的请求响应,而不是用来回收那部分工作。对于一个 可能挂住的钩子,也给底层调用它自己的超时(socket 或 HTTP 超时)。因为线程无法回收,一个每次调用 都挂住的钩子会每次调用多长一个线程;给一个可能挂住的钩子它自己的上界,而不是依赖 hooks_timeout 来让它停下。

对于异步钩子,协程在事件循环上运行;调用方的超时到点仍然返回,而协程继续在循环上运行。

超时还会释放 hooks_concurrent=False 的锁:dispatch 只等钩子到限制为止,然后继续,而超时的钩子 在锁之外继续运行。所以这把锁串行化的是那些按时完成的钩子,不是每一个曾经启动过的钩子;一个超时的 钩子不再挡住它后面的钩子。

选一条策略

钩子种类 建议 为什么
策略 / 防护栏 / 脱敏 hooks_raise=True 一条静默失败的策略是一个安全漏洞。
审计 / 日志 测试里 hooks_raise=True,生产里常常 False 丢掉一条审计记录应该响亮,但不一定致命。
指标 / 链路追踪 hooks_raise=False 可观测性绝不能让请求失败。
缓存读/写 hooks_raise=True 一个坏掉的缓存应该暴露出来,而不是静默漏掉。

你可以混搭:用实例默认值安装一个防护栏,给一个遥测钩子它自己的 try/except,或者逐调用用单独的 hooks_raise。

另见