文件導航

錯誤處理

鉤子執行在它所觀察的請求內部,所以它們的失敗如何處理很重要。這一頁是確切的策略。

兩條策略

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。

另見