Обработка ошибок
Hooks выполняются внутри запроса, который они наблюдают, поэтому то, как обрабатываются их сбои, имеет значение. Эта страница описывает точную политику.
- Две политики
- Что выполняется, когда что-то падает
- Связывание исключений
- BaseException
- Ошибки конфигурации
- Предупреждения
- Выбор политики
Две политики
hooks_raise управляет тем, что происходит, когда hook возбуждает исключение.
hooks_raise |
поведение |
|---|---|
True (по умолчанию) |
исключение hook распространяется наружу из вызова. |
False |
hook пропускается с RuntimeWarning, и вызов продолжается. |
Он задаётся на уровне экземпляра и может быть переопределён на вызов (hooks_raise= в
predict_batch, system_one, Router.route, Router.predict). 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, поэтому hooks очистки выполняются при
сбое.
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 | распространяется напрямую; чекпойнт остаётся построенным и резидентным. |
on_evict |
Router | распространяется напрямую; чекпойнт уже освобождён. |
Последствия, которые стоит знать:
- Падающий
on_loadоставляет модель в кэше, поэтому следующийloadвозвращает её, не запускаяon_loadснова. - Падающий
on_predict_endна пути успеха означает, что вызывающий получает исключение вместо результата, хотя инференс прошёл успешно. Используйтеhooks_raise=Falseдля конечных hooks, которые являются чистыми побочными эффектами.
Связывание исключений
Когда hook падает, пока уже распространяется другое исключение, исходное исключение возбуждается
заново, а исключение hook присоединяется как __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 всегда распространяются. Они всё равно запускают ветку except BaseException
жизненного цикла predict, а значит on_error и on_predict_end выполняются до разворачивания
процесса. Держите эти hooks быстрыми и неблокирующими, если вам важна задержка прерывания.
Ошибки конфигурации
Плохая конфигурация падает быстро с TypeError, до любого инференса:
| случай | где возбуждается | пример |
|---|---|---|
| класс вместо экземпляра | конструирование | hooks=[MyHook] |
| нет метода жизненного цикла | конструирование | hooks=[object()] |
| невызываемое событие | конструирование | on_predict_start = 5 |
| невызываемый удобный hook | конструирование | on_predict_start=123 |
обычный вызываемый объект в hooks= |
конструирование | hooks=[lambda ctx: None] |
Hooks на вызов проверяются в момент вызова, поэтому плохой hook на вызов возбуждает TypeError из
predict/system_one, а не при конструировании.
Предупреждения
При hooks_raise=False каждый падающий hook выдаёт один RuntimeWarning с именем типа hook и
события:
laya: hook Metrics.on_predict_end failed: connection reset
Предупреждение выдаётся один раз на сбой, а не один раз на определение hook, поэтому нестабильный hook под нагрузкой может быть шумным. Если это важно, агрегируйте или ограничивайте частоту внутри hook.
Тайм-ауты
hooks_timeout ограничивает каждый вызов hook в секундах. Hook, который всё ещё работает после
предела, рассматривается как сбой hook: TimeoutError при hooks_raise=True, RuntimeWarning при
False. 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
нулевой длины.
Hook с тайм-аутом выполняется в рабочем потоке на копии контекста contextvars вызывающего,
поэтому id запроса или span трассировки, установленный вызывающим, виден hook.
Одна честная оговорка: Python не может прервать поток, поэтому hook с истёкшим тайм-аутом продолжает
работать в фоне. Тайм-аут ограничивает, сколько ждёт запрос, а не сколько живёт hook. Используйте
его, чтобы обслуживаемый запрос оставался отзывчивым, а не чтобы вернуть работу. Для hook, который
может зависнуть, дайте и нижележащему вызову собственный тайм-аут (тайм-аут сокета или HTTP).
Поскольку поток нельзя вернуть, hook, который зависает на каждом вызове, наращивает по потоку на
вызов; дайте hook, который может зависнуть, собственный предел вместо того, чтобы полагаться на
hooks_timeout в его остановке.
Для асинхронного hook корутина выполняется в цикле событий; тайм-аут на стороне вызывающего всё равно возвращает после предела, а корутина продолжает выполняться в цикле.
Тайм-аут также освобождает блокировку hooks_concurrent=False: dispatch ждёт hook только до предела,
затем идёт дальше, а hook с истёкшим тайм-аутом продолжает работать вне блокировки. Так блокировка
сериализует hooks, которые успевают вовремя, а не каждый hook, который когда-либо запускался; hook,
который вышел за предел, больше не блокирует идущие за ним.
Выбор политики
| тип hook | рекомендуется | почему |
|---|---|---|
| политика / ограничитель / редактирование | hooks_raise=True |
политика, которая молча падает, — это дыра в безопасности. |
| аудит / логирование | hooks_raise=True в тестах, часто False в продакшене |
потеря записи аудита должна быть громкой, но не обязательно фатальной. |
| метрики / трассировка | hooks_raise=False |
наблюдаемость не должна ронять запрос. |
| чтение/запись кэша | hooks_raise=True |
сломанный кэш должен всплывать, а не молча промахиваться. |
Можно смешивать: установите ограничитель со значением по умолчанию экземпляра и дайте hook
телеметрии собственный try/except, или используйте отдельный hooks_raise на каждый вызов.
Смотрите также
- Жизненный цикл: форма
try/except/finallyв контексте. - Паттерны и антипаттерны: частые ошибки при обработке ошибок.