errors: непокрытая маппингом ошибка и поведение после recover
- R25: 500 и ERROR с признаком непокрытой — без признака забытая ветвь неотличима в логах от упавшей базы - R26: HTTP-запрос завершается 500, цикл продолжается со следующего элемента, а упавший выводится из оборота — иначе poison message
This commit is contained in:
@@ -219,6 +219,25 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
<!-- local:маппинг -->
|
<!-- local:маппинг -->
|
||||||
<!-- /local -->
|
<!-- /local -->
|
||||||
|
|
||||||
|
### R25. Непокрытая маппингом ошибка — 500 и `ERROR` с признаком
|
||||||
|
|
||||||
|
**ДОЛЖЕН.** Доменная ошибка, для которой в маппинге (R15) нет ветви, отдаёт
|
||||||
|
наружу 500 и нейтральное «внутренняя ошибка», а в лог идёт `ERROR` с
|
||||||
|
признаком того, что маппинг её не знает.
|
||||||
|
|
||||||
|
**Почему.** Непокрытая ошибка — не класс отказа, а дефект: ветвь забыли
|
||||||
|
завести вопреки R16. Адресат у неё владелец в смысле «надо чинить», отсюда
|
||||||
|
`ERROR` — уровень выбирается по адресату (`lang/go/logging.md`). Статус
|
||||||
|
тоже не выбирается: известное пользовательское состояние лежало бы в
|
||||||
|
маппинге, а про неизвестное сказать пользователю нечего, поэтому 4xx
|
||||||
|
отпадает.
|
||||||
|
|
||||||
|
Признак нужен потому, что без него забытая ветвь неотличима от упавшей
|
||||||
|
базы: обе дают `ERROR` с текстом ошибки, и наткнуться на пропуск можно
|
||||||
|
только случайно. Отдельное поле или своя категория сообщения делают пропуск
|
||||||
|
находимым одним фильтром — и тогда громкость 500 и `ERROR` работает как
|
||||||
|
механизм обнаружения, а не как шум.
|
||||||
|
|
||||||
### R17. Форма текста определяется поверхностью
|
### R17. Форма текста определяется поверхностью
|
||||||
|
|
||||||
**ДОЛЖЕН.** У публичной границы две разные поверхности, и правило сырого
|
**ДОЛЖЕН.** У публичной границы две разные поверхности, и правило сырого
|
||||||
@@ -313,6 +332,42 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
|
|
||||||
## Несколько ошибок
|
## Несколько ошибок
|
||||||
|
|
||||||
|
### R26. После `recover` единица продолжает работу, исключив упавшее
|
||||||
|
|
||||||
|
**ДОЛЖЕН.** Что происходит после перехвата, зависит от того, где стоит
|
||||||
|
граница:
|
||||||
|
|
||||||
|
| № | Где перехвачена паника | Что дальше |
|
||||||
|
|---|---|---|
|
||||||
|
| R26.1 | обработчик HTTP-запроса | ответ 500, если он ещё не начат; процесс и прочие запросы не затрагиваются |
|
||||||
|
| R26.2 | итерация цикла обработки — бот, воркер | цикл продолжается со следующего элемента, упавший элемент повторно не берётся |
|
||||||
|
|
||||||
|
**Почему.** Паника внутри обработки одного элемента почти всегда говорит о
|
||||||
|
баге в работе с данными этого элемента, а не о порче общего состояния, —
|
||||||
|
останавливать всё остальное не за что. Довод «let it crash» здесь работает
|
||||||
|
не буквально: в OTP падает изолированный процесс под супервизором, а не узел
|
||||||
|
целиком, и в Go ближайшая замена такой изоляции — граница итерации, а не
|
||||||
|
граница процесса. Обратное при этом верно и делает `recover` в цикле
|
||||||
|
обязательным (R22): неперехваченная паника в любой горутине завершает весь
|
||||||
|
процесс.
|
||||||
|
|
||||||
|
Продолжать, не исключив упавший элемент, нельзя: детерминированная паника
|
||||||
|
даёт бесконечный цикл — тот же элемент, тот же стек, залитый лог и нулевой
|
||||||
|
прогресс. Это классический poison message, и лекарство берём то же, что
|
||||||
|
принято в очередях: элемент выводится из оборота, а не берётся снова. У
|
||||||
|
цикла, который и так подтверждает прогресс — сдвигает офсет, помечает
|
||||||
|
строку состоянием, — механизм для этого уже есть, заводить отдельный не
|
||||||
|
нужно.
|
||||||
|
|
||||||
|
Оговорка «если ответ ещё не начат» в R26.1 не формальность: статус
|
||||||
|
отправляется один раз, и после первой записи в тело поменять его нечем —
|
||||||
|
клиент получит обрывок с кодом 200. Отсюда же общее предпочтение собирать
|
||||||
|
ответ целиком до записи там, где это возможно.
|
||||||
|
|
||||||
|
Из R26.1 есть одно исключение: `http.ErrAbortHandler` — сигнал «прервать
|
||||||
|
обработку намеренно», и recover-обёртка пробрасывает его дальше, а не
|
||||||
|
превращает в 500. Так поступают и стандартные обёртки вроде chi.
|
||||||
|
|
||||||
### R24. Независимые ошибки собираются `errors.Join`
|
### R24. Независимые ошибки собираются `errors.Join`
|
||||||
|
|
||||||
**СЛЕДУЕТ.** Валидация конфига и подобные проверки отдают все проблемы
|
**СЛЕДУЕТ.** Валидация конфига и подобные проверки отдают все проблемы
|
||||||
|
|||||||
@@ -348,6 +348,10 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
таблицу не входит: это не доменный отказ, и логирует его recover-граница
|
таблицу не входит: это не доменный отказ, и логирует его recover-граница
|
||||||
вместе со стеком (`lang/go/errors.md`). Искать его класс здесь не нужно.
|
вместе со стеком (`lang/go/errors.md`). Искать его класс здесь не нужно.
|
||||||
|
|
||||||
|
Мимо таблицы идёт и доменная ошибка, которой нет в маппинге: класса у неё
|
||||||
|
нет, потому что её просто забыли завести. Она логируется `ERROR` с
|
||||||
|
признаком непокрытой (`lang/go/errors.md` R25).
|
||||||
|
|
||||||
### R26. Тот же отказ в асинхронной стадии — уровнем выше
|
### R26. Тот же отказ в асинхронной стадии — уровнем выше
|
||||||
|
|
||||||
**ДОЛЖЕН.** Когда пользователь не ждёт результата, отказ адресован
|
**ДОЛЖЕН.** Когда пользователь не ждёт результата, отказ адресован
|
||||||
|
|||||||
Reference in New Issue
Block a user