- каталог задач переехал в tasks/ в корне, спринт упразднён — приоритет теперь порядок строк в BACKLOG.md, четыре задачи набора вернулись в беклог - гейт: путь docs.py переведён на av-dev-docs вместо снесённого av-dev-pm, добавлены шаги tasks.py check и openspec.py check - относительные ссылки внутри задач и ссылки из docs/ на задачи починены
91 lines
7.5 KiB
Markdown
91 lines
7.5 KiB
Markdown
# 🐞 Тормозить опрос qBittorrent бэкоффом при недоступности и эскалировать устойчивый сбой
|
||
|
||
- **Тип:** fix
|
||
- **Категория:** Инфраструктура
|
||
- **Зачем:** недоступный qBittorrent опрашивается каждые 5 с и даёт WARN на каждом тике: нужен экспоненциальный бэкофф до минутного потолка со сбросом по первому успеху и ERROR на устойчивой деградации
|
||
|
||
Остаток от задачи «классификация доменных ошибок + конвенции логирования»
|
||
(основное реализовано, см. ниже) плюс бэкофф опроса, заказанный 2026-08-06.
|
||
Речь о поведении фонового цикла, пока зависимость лежит: с какой частотой он её
|
||
дёргает и каким уровнем об этом пишет.
|
||
|
||
## Что уже сделано (не переоткрывать)
|
||
|
||
Коммит `f8fb4fa` (Tier A) + коммит этой задачи закрыли:
|
||
|
||
- **Классификация доменных ошибок:** sentinel `worker.ErrInvalidInput`→400;
|
||
обёртки `ErrConflict` в Cancel/Retry/Defer/Undo; `layout.ErrCollision`→409 в
|
||
`classifyErr` и ветка в tgbot; `logCmd` относит новые классы в DEBUG.
|
||
- **Конвенции:** `logging.md` — команды воркера = доменная граница, таблица
|
||
уровней доменных отказов (граница команды vs асинхронная стадия), правило про
|
||
`*url.Error`/секреты в URL, канон категории `state transition` (унифицированы
|
||
cancel/retry/relink/recovery). `errors.md` — таблица маппинга ошибка→статус,
|
||
развилка «транзиентный ответ vs персистентная диагностика» решена как (а):
|
||
`error_msg`/`reasons` — операторская поверхность владельца (сырой текст ок,
|
||
секреты запрещены; аудит показал, что секреты туда не текут).
|
||
- **Мелочи:** reason-коды const-блок; лог-поля `id`→`download_id`; preview
|
||
WARN; комментарий у `parseIgnored`.
|
||
|
||
## Остаток
|
||
|
||
**Шум `ext.*` ERROR решено оставить как есть (2026-08-06).** Запись
|
||
«зависимость недоступна» на каждом тике — легитимный сигнал транспортного слоя,
|
||
и гасится он уровнем сбора логов, а не кодом. Варианты с пониженным уровнем у
|
||
`logging.ExtCall` и с дедупом отклонены: первый заводит второе правило уровня
|
||
для того же класса вызовов, второй даёт транспортному логгеру память о
|
||
состоянии.
|
||
|
||
Остаются две вещи, и обе стоят на одном счётчике подряд-идущих сбоев тика.
|
||
|
||
**Бэкофф опроса (решение 2026-08-06).** Пока qBittorrent недоступен, цикл
|
||
продолжает дёргать его каждые `poll_interval` (5 с) — недоступную зависимость
|
||
незачем опрашивать с рабочей частотой. Интервал растёт экспоненциально от
|
||
`poll_interval` до потолка порядка минуты; первый успешный ответ возвращает
|
||
рабочий интервал сразу, без ступенчатого спуска. Бэкофф заодно снимает и остроту
|
||
шума: записей становится столько же на событие, но событий — единицы в минуту.
|
||
|
||
**Эскалация уровня.** Сейчас сбой тика — **всегда WARN**, сколько бы тиков
|
||
подряд он ни падал. `docs/conventions/logging.md` требует иного: устойчивый сбой
|
||
N тиков подряд — это реальная деградация, и она пишется ERROR.
|
||
|
||
## Воспроизведение
|
||
|
||
1. Остановить qBittorrent (локально, не на umbar).
|
||
2. Смотреть лог воркера в течение нескольких минут поллинга.
|
||
3. Наблюдается: запрос к qBittorrent уходит каждые 5 секунд всё время
|
||
недоступности, а доменная запись `poll failed` идёт WARN на каждом тике и
|
||
остаётся WARN бесконечно.
|
||
4. Ожидается: интервал опроса растёт до минутного потолка, а после N
|
||
подряд-идущих неудачных тиков уровень поднимается до ERROR — деградация
|
||
отличается от разового промаха.
|
||
5. Поднять qBittorrent обратно: опрос возвращается к `poll_interval` с первого
|
||
успешного ответа.
|
||
|
||
## Затрагивает
|
||
|
||
- цикл поллинга воркера (`internal/worker`) — счётчик подряд-идущих сбоев,
|
||
текущий интервал тика и его сброс по успеху;
|
||
- секция `[worker]` конфига и `config.example.toml` — потолок бэкоффа и порог
|
||
эскалации;
|
||
- `docs/database.md`, таблица «Настройки с числовым значением» — дом обоих
|
||
чисел;
|
||
- `docs/architecture.md`, «Характер потока» — там сказано, что фон непрерывный с
|
||
периодом поллинга; переменный интервал это уточняет;
|
||
- `docs/conventions/logging.md` — правило эскалации уже записано, меняться не
|
||
должно; задача приводит код к нему.
|
||
|
||
## Критерии приёмки
|
||
|
||
- При подряд-идущих сбоях интервал опроса растёт экспоненциально от
|
||
`poll_interval` и упирается в потолок из конфига, дальше не растёт (оракул:
|
||
тест цикла с подставным клиентом и управляемыми часами — проверяет
|
||
последовательность интервалов).
|
||
- Первый успешный ответ возвращает `poll_interval` немедленно (оракул: тот же
|
||
тест, сценарий «серия сбоев, успех, сбой» — после успеха интервал рабочий).
|
||
- Сбой тика ниже порога пишется WARN, начиная с N-го подряд — ERROR, а успешный
|
||
тик сбрасывает счётчик (оракул: тест, считающий уровни записей на сценарии
|
||
«сбой, сбой, успех, сбой»).
|
||
- Потолок бэкоффа и порог эскалации читаются из конфига и описаны в
|
||
`config.example.toml` с единицами и диапазоном (оракул: `task gate`, шаг
|
||
канона — сверка с `database.md`).
|