errors: непокрытая маппингом ошибка и поведение после recover
- R25: 500 и ERROR с признаком непокрытой — без признака забытая ветвь неотличима в логах от упавшей базы - R26: HTTP-запрос завершается 500, цикл продолжается со следующего элемента, а упавший выводится из оборота — иначе poison message
This commit is contained in:
@@ -219,6 +219,25 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
<!-- local:маппинг -->
|
||||
<!-- /local -->
|
||||
|
||||
### R25. Непокрытая маппингом ошибка — 500 и `ERROR` с признаком
|
||||
|
||||
**ДОЛЖЕН.** Доменная ошибка, для которой в маппинге (R15) нет ветви, отдаёт
|
||||
наружу 500 и нейтральное «внутренняя ошибка», а в лог идёт `ERROR` с
|
||||
признаком того, что маппинг её не знает.
|
||||
|
||||
**Почему.** Непокрытая ошибка — не класс отказа, а дефект: ветвь забыли
|
||||
завести вопреки R16. Адресат у неё владелец в смысле «надо чинить», отсюда
|
||||
`ERROR` — уровень выбирается по адресату (`lang/go/logging.md`). Статус
|
||||
тоже не выбирается: известное пользовательское состояние лежало бы в
|
||||
маппинге, а про неизвестное сказать пользователю нечего, поэтому 4xx
|
||||
отпадает.
|
||||
|
||||
Признак нужен потому, что без него забытая ветвь неотличима от упавшей
|
||||
базы: обе дают `ERROR` с текстом ошибки, и наткнуться на пропуск можно
|
||||
только случайно. Отдельное поле или своя категория сообщения делают пропуск
|
||||
находимым одним фильтром — и тогда громкость 500 и `ERROR` работает как
|
||||
механизм обнаружения, а не как шум.
|
||||
|
||||
### 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`
|
||||
|
||||
**СЛЕДУЕТ.** Валидация конфига и подобные проверки отдают все проблемы
|
||||
|
||||
@@ -348,6 +348,10 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
таблицу не входит: это не доменный отказ, и логирует его recover-граница
|
||||
вместе со стеком (`lang/go/errors.md`). Искать его класс здесь не нужно.
|
||||
|
||||
Мимо таблицы идёт и доменная ошибка, которой нет в маппинге: класса у неё
|
||||
нет, потому что её просто забыли завести. Она логируется `ERROR` с
|
||||
признаком непокрытой (`lang/go/errors.md` R25).
|
||||
|
||||
### R26. Тот же отказ в асинхронной стадии — уровнем выше
|
||||
|
||||
**ДОЛЖЕН.** Когда пользователь не ждёт результата, отказ адресован
|
||||
|
||||
Reference in New Issue
Block a user