错误处理
错误处理
钩子运行在它所观察的请求内部,所以它们的失败如何处理很重要。这一页是确切的策略。
两条策略
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。