Документация

Обработка ошибок

Hooks выполняются внутри запроса, который они наблюдают, поэтому то, как обрабатываются их сбои, имеет значение. Эта страница описывает точную политику.

Две политики

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 на каждый вызов.

Смотрите также