Беклог: классификация доменных ошибок (500→409/400) и конвенции логирования
Отложенные Tier B/C по итогам ревью Fable (продолжение f8fb4fa): sentinel-
обёртки ErrConflict/валидации, classifyErr для layout.ErrCollision, единая
категория переходов, правила уровней и *url.Error/секретов в docs/conventions,
решение по сырому err.Error() в reasons/error_msg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ Tududi (проект `jellybit`) больше **не** держит беклог
|
||||
- [Обучение на правках человека (few-shot из прошлых ревью)](obuchenie-na-pravkah.md) — Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать…
|
||||
- [Confidence-гейт авто-раскладки: узаконить в спеке + сделать выключаемым (дефолт 0.7)](gate-confidence-spec-vs-code.md) — Решено (B): гейт оставляем как доп. проверку на ревью — выключаемый порог, дефолт 0.85→0.7, записать в спеку
|
||||
- [Внешние субтитры: пары VobSub и языковой суффикс](vneshnie-subtitry.md) — Привязка субтитр→серия уже работает; остались пары VobSub .idx+.sub и потеря Lang/Flags
|
||||
- [Классификация доменных ошибок (500→409/400) и конвенции логирования](oshibki-klassifikaciya-i-konvencii-logirovaniya.md) — Продолжение коммита f8fb4fa (Tier A сделан): sentinel-обёртки ErrConflict/валидации, classifyErr для ErrCollision, правки docs/conventions _(ревью Fable)_
|
||||
- [Defer из catched → лимбо → необратимый deleted (MAJOR-6)](review-major6-defer-catched.md) — Defer из ещё-не-добавленного catched уводит задачу в необратимый deleted _(ревью 2026-07-08)_
|
||||
- [processCatched: promote-without-add если торрент уже в qBittorrent (F2)](review-f2-promote-without-add.md) — торрент уже в qBittorrent → processCatched зациклен на Add вместо promote _(ревью 2026-07-08)_
|
||||
- [Cancel во время add оставляет неуправляемый торрент в qBittorrent (F3/NIT-13)](review-f3-cancel-during-add.md) — Cancel во время add оставляет неуправляемый торрент в qBittorrent _(ревью 2026-07-08)_
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Классификация доменных ошибок (500→409/400) и конвенции логирования
|
||||
|
||||
**Приоритет:** средний · **Теги:** review-fable, errors, logging
|
||||
|
||||
Продолжение коммита `f8fb4fa` (логирование ошибок: доменная граница + защита
|
||||
секретов). Тот коммит закрыл «ядро + секреты» (Tier A) по итогам ревью двумя
|
||||
сабагентами Fable (инфраструктурные и доменные ошибки). Здесь — отложенные
|
||||
Tier B (классификация) и Tier C (конвенции + политика), не вошедшие в scope.
|
||||
|
||||
## Tier B — классификация ошибок (сейчас штатные конфликты/валидация → 500)
|
||||
|
||||
Проблема: часть доменных отказов возвращается голым `fmt.Errorf` без sentinel,
|
||||
поэтому `classifyErr` (httpapi) отправляет их в `default` → **500 «внутренняя
|
||||
ошибка»** вместо 409/400. Побочно: новый `logCmd` (worker) логирует такие
|
||||
отказы как **ERROR**, хотя это норма (адресат — пользователь), должно быть
|
||||
DEBUG.
|
||||
|
||||
- **Обернуть `ErrConflict`** в командах, где конфликт состояния не обёрнут (в
|
||||
отличие от `requireReviewable`/`Apply`/`Undo`/`Delete`, где уже сделано):
|
||||
- `worker.go:789` — `cancel: ... is already terminal`;
|
||||
- `worker.go:809` — `retry: ... only failed/stuck are retriable`;
|
||||
- `review.go:515` — `defer: ... is terminal`;
|
||||
- `review.go:551` — `undo: nothing to revert` (скорее конфликт).
|
||||
- **Sentinel для валидации ввода** (`ErrInvalidInput` → 400) или дооборачивание,
|
||||
чтобы промах пользователя не выглядел сбоем ни в статусе, ни в уровне лога:
|
||||
- `review.go` — `refine: empty hint`, `set type: invalid type`,
|
||||
`ignore: empty path`, `add source: invalid provider`/`empty id`,
|
||||
`set provider: invalid provider`/`empty id`,
|
||||
`choose candidate: candidate ... does not belong`.
|
||||
- **`classifyErr` не знает `layout.ErrCollision`**: ручной Apply с коллизией
|
||||
цели штатно уводит задачу в review с причиной (`review.go` linkPlan), но
|
||||
пользователь в вебе получает 500, а tgbot — «Не удалось выполнить действие».
|
||||
Добавить кейс (409 или сценарий «ушло в ревью: коллизия цели») и исключение
|
||||
в tgbot по аналогии с `ErrNotReady`.
|
||||
|
||||
После sentinel-обёрток обновить `logCmd` (worker.go) — он уже относит
|
||||
`ErrConflict/ErrNotReady/ErrNotFound` в DEBUG; добавить туда новый
|
||||
`ErrInvalidInput`.
|
||||
|
||||
## Tier C — конвенции и политика (docs/conventions)
|
||||
|
||||
Оба ревьюера предложили закрепить в `logging.md`/`errors.md` (чтобы дыры и
|
||||
дубли не возникали снова — уже дважды выстрелило: ingest, затем команды):
|
||||
|
||||
- **Команды воркера = доменная граница.** Явно дописать в `logging.md`, раздел
|
||||
«Ошибки»: публичные методы воркера (`Apply`/`Refine`/`Cancel`/…) — граница
|
||||
домена, логируют исход ровно один раз (реализовано в `logCmd`); транспорты не
|
||||
логируют возвращённую ошибку. Сейчас формулировка «стадии воркера» двусмысленна.
|
||||
- **Таблица уровней доменных отказов:** `ErrConflict`/`ErrNotReady`/валидация →
|
||||
DEBUG (адресат — пользователь); нарушенный инвариант хранилища → INFO/WARN;
|
||||
прочее (БД/ФС/зависимости) → ERROR. Правило: у каждой доменной ошибки ровно
|
||||
один логирующий, уровень — по адресату, а не по месту.
|
||||
- **Правило про `*url.Error`/секреты в URL** (раздел «Безопасность»): ошибки
|
||||
HTTP-транспорта встраивают URL, который может нести секрет — санитизировать на
|
||||
границе клиента до лога и обёртки. Частично реализовано (`logging.SanitizeErr`,
|
||||
применён в `ExtCall`/tgbot/metadata) — осталось задокументировать + правило
|
||||
«секрет не кладём в URL, если у API есть заголовок».
|
||||
- **Уровень повторяющихся сбоев фоновых циклов:** `poll/sweep/list failed` =
|
||||
WARN, а та же ошибка БД в `ingest.Ingest` = ERROR. Договориться о едином
|
||||
правиле (транзиентный сбой тика → WARN; эскалация в ERROR при устойчивом сбое
|
||||
N тиков) — сейчас уровень зависит от места. Смежно: ERROR-шторм при
|
||||
недоступном qBittorrent (`qbt` логирует Failure на каждом тике поллинга).
|
||||
- **Единая категория переходов состояния:** свести к одному msg `state
|
||||
transition` (from/to/code); `download cancelled`/`download retried`/`relink
|
||||
re-recognizing` не теряются из-под `jq 'select(.msg=="state transition")'`.
|
||||
|
||||
## Развилка — сырой `err.Error()` на публичных поверхностях
|
||||
|
||||
`reasons` (`review.go` recognizeOne) и `error_msg` переходов (`linkPlan`)
|
||||
показываются в review-экране и Telegram-карточке и могут нести детали
|
||||
реализации (тело LLM/qBit, абсолютные пути). Решить: (а) узаконить в `errors.md`
|
||||
как «операторская поверхность владельца» с явным запретом секретов; либо (б)
|
||||
держать там reason-код + нейтральный текст, полную ошибку — в лог по
|
||||
`download_id`.
|
||||
|
||||
## Мелочь (по желанию, из тех же ревью)
|
||||
|
||||
- `httpapi/review.go` — поле `"id"` вместо словарного `"download_id"`; msg
|
||||
«review data» → категория «review data failed».
|
||||
- `worker/review.go` `parseIgnored` — `_ = json.Unmarshal` без обязательного
|
||||
комментария «почему» (битый override молча обнуляет игнор).
|
||||
- Reason-коды переходов (`"resolve"`/`"build"`/`"persist"`/`"collision"`/…) —
|
||||
свести в const-блок рядом с `errCode*` (сейчас только `reasonTitleFolderDesync`).
|
||||
- Превью в `ReviewData` при сбое — DEBUG, хотя это видимая деградация (пропадает
|
||||
кнопка «Применить»); уместнее WARN.
|
||||
|
||||
Вердикт: change (Tier B — правки кода + миграция статусов ошибок; Tier C —
|
||||
правки конвенций + одно продуктовое решение по `err.Error()`).
|
||||
Reference in New Issue
Block a user