From 97ceb7bb69951d9603aa3dffbcff84738707887d Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Fri, 14 Aug 2026 20:38:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B7=D0=B0=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=20failure-verdict?= =?UTF-8?q?-vs-retry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/BACKLOG.md | 1 + tasks/items/failure-verdict-vs-retry.md | 68 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tasks/items/failure-verdict-vs-retry.md diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index 5564fc2..d9bdef5 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -58,6 +58,7 @@ - [🧹 Покрыть тестами шаги конвейера и захват задачи](items/pipeline-step-tests.md) — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем. - [🧹 Покрыть тестами разбор вывода ffprobe](items/metaviewer-adapter-tests.md) — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто. - [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. +- [🐞 Различать отказ, который стоит повторить, и приговор записи](items/failure-verdict-vs-retry.md) — Отказ приведения останавливает запись с первой попытки, и предел в пять отказов не работает никогда: разовый сбой ffmpeg останавливает запись приговором, хотя повтор обработал бы её успешно. - [🧹 Прервать шаг конвейера отменой контекста](items/context-cancel-in-pipeline.md) — Половина сделана 2026-08-13 — контекст доходит до внешних вызовов, а прерванный шаг оставляет задачу на повтор и не тратит попытку, — но осталось то, ради чего задача заводилась: хранилище контекста не принимает ни одним методом, и бюджет мягкой остановки не замерен. - [🐞 Убирать записанный файл, когда приём отказал на середине](items/orphan-file-on-failed-intake.md) — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками. - [🧹 Разобрать мелочи слоя хранилища](items/storage-layer-nits.md) — Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла. diff --git a/tasks/items/failure-verdict-vs-retry.md b/tasks/items/failure-verdict-vs-retry.md new file mode 100644 index 0000000..70efd1a --- /dev/null +++ b/tasks/items/failure-verdict-vs-retry.md @@ -0,0 +1,68 @@ +# 🐞 Различать отказ, который стоит повторить, и приговор записи + +- **Тип:** fix +- **Категория:** Очередь — Сроки ожидания идут вперёд: пока их нет, «медленно отвечает» от «упало» неотличимо, и часть отказов классифицировать нечем. +- **Зачем:** Отказ приведения останавливает запись с первой попытки, и предел в пять отказов не работает никогда: разовый сбой ffmpeg останавливает запись приговором, хотя повтор обработал бы её успешно. + +Спека `pipeline` заводит требование «Число отказов ограничивает повторы шага» с +пределом в пять, но какие отказы этот предел вообще расходуют, она не решает. На +деле шаг делит их сам и делит грубо: отказ конвертации, отказ операции у +провайдера и отсутствие файла останавливают запись **приговором с первой +попытки**, а до счётчика доходит только отказ хранилища. Значит нарастающая пауза +и пять попыток не работают ровно на тех отказах, которые случаются чаще всего. + +Разница наблюдаема: переполненный диск, временно недоступный `ffmpeg` и разовый +сбой у провайдера сегодня неотличимы от негодной записи, и человек получает +«попробуйте ещё раз» вместо расшифровки, которую дал бы второй заход. + +Найдено ревью задачи `record-centric-model` 2026-08-14; поведение унаследовано от +прежней модели, где отказ переводил задачу в конечное состояние. Само по себе +деление на приговор и повтор — решение, а не разведка: разбирать надо перечень +отказов, а не внешний мир. + +## Воспроизведение + +1. Подставить конвертер, отказывающий один раз, и завести запись. +2. Прогнать шаг конвейера. + +Видно: запись остановлена признаком с причиной «приговор шага», число отказов — +единица, паузы нет. Ожидалось: запись возвращается в работу с нарастающей паузой +и останавливается только на шестом отказе. + +То же на шаге отправки: отказ операции у провайдера останавливает запись, не +израсходовав ни одной попытки. + +## Затрагивает + +- `openspec/specs/pipeline/spec.md` — требование о числе отказов и раздел + `Purpose`, где вопрос назван неразобранным; +- `internal/service/transcribe.go` — `failStep`, `scheduleRetry` и ветки отказа в + шагах приведения, отправки, опроса и завершения; +- `internal/entity/audio_record.go` — перечень причин остановки, если делению + потребуется новая; +- `docs/architecture.md`, таблица внешних зависимостей — столбцы «падает» и + «отдаёт мусор» описывают сегодняшнее деление. + +## Критерии приёмки + +- Разовый отказ приведения возвращает запись в работу, а не останавливает её. + Оракул — тест: конвертер отказывает один раз, следующий прогон доводит запись + до рубежа приведения. +- Запись, отказывающая на каждой попытке, всё же останавливается по исчерпании + предела. Оракул — тест: конвертер отказывает всегда, запись останавливается на + шестом захвате с причиной «отказы исчерпаны», а не с первого. +- Отказ, о котором известно, что повтор его не устранит, останавливает запись + сразу. Оракул — тест: запись без файла источника останавливается уже на первом + заходе, и причина остановки — приговор шага. +- Спека называет, какие отказы расходуют предел, а какие выносят приговор. + Оракул — `openspec validate --strict` на изменении плюс сценарий требования, + различающий оба исхода. + +## Рамки + +Перечень отказов, которые считать приговором, ограничить теми, что сервис видит +сегодня: негодная запись, отказ операции у провайдера, отсутствие файла. +Классификацию отказов внешних сервисов по кодам не заводить — сроки ожидания +ставит `external-call-timeouts`, и до неё «медленно» от «упало» неотличимо. +Числа предела и паузы не трогать: они записаны в `docs/database.md` и решением +владельца не пересматривались.