Приём отвечает 200 до свёртки, свёртку ведёт фоновый воркер
- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
## 1. Опоры в хранилище
|
||||
|
||||
- [x] 1.1 Миграция `00006`: частичный индекс
|
||||
`delivery (received_at, id) WHERE parse_status = 'pending'`. Комментарий
|
||||
объясняет, почему частичный, а не по `parse_status`: в установившемся
|
||||
режиме в нём ноль–одна строка, полный хранил бы всю таблицу ради выборки
|
||||
из одной.
|
||||
- [x] 1.2 `store.PendingDeliveries(ctx, after, limit)` — неразобранные доставки
|
||||
в порядке `(received_at, id)`, строго после курсора; отдаёт идентификатор
|
||||
и `received_at`. Сравнение курсора — **row-value** `(received_at, id) >
|
||||
(?, ?)`: развёрнутая форма через `OR` даёт `SCAN` вместо `SEARCH`
|
||||
(проверено `EXPLAIN QUERY PLAN`). Нулевой курсор — нулевое время и пустой
|
||||
идентификатор, без ветки «первая страница».
|
||||
- [x] 1.3 `store.CountPendingDeliveries(ctx)` — размер задолженности для
|
||||
строки `INFO` при старте.
|
||||
- [x] 1.4 `store.ErrBusy` — доменный сентинел занятости базы; `inTx` оборачивает
|
||||
им исчерпание повторов, чтобы вызывающий не разбирал коды драйвера.
|
||||
- [x] 1.5 Обновить `docs/database.md`: новый индекс в перечне индексов
|
||||
`delivery`.
|
||||
|
||||
## 2. Исход свёртки отражает доставку, а не обстоятельства
|
||||
|
||||
- [x] 2.1 `fold.fail`: отмена контекста и `store.ErrBusy` статус **не меняют** —
|
||||
доставка остаётся `pending`; всё прочее по-прежнему `failed`. Уровень лога
|
||||
по адресату: занятость и отмена — `WARN` (пройдёт само), остальное как
|
||||
сейчас.
|
||||
- [x] 2.2 Тест: свёртка на занятой базе оставляет доставку `pending`; свёртка
|
||||
непонятого содержимого оставляет `failed`.
|
||||
|
||||
## 3. Общий проигрыватель — `internal/replay`
|
||||
|
||||
- [x] 3.1 `replay.Player.Play(ctx, deliveryID) (Outcome, error)` — свернуть одну
|
||||
доставку и вернуть её исход **значением**: ровно один классовый счётчик
|
||||
равен единице, плюс `partial`/`incomparable` у успешной свёртки.
|
||||
Классифицируется **только ошибка**; на контекст `Player` не смотрит —
|
||||
решение «нас остановили» принимает цикл.
|
||||
- [x] 3.2 `replay.Outcome` + `(*Outcome).Add(other)`; `replay.Report` встраивает
|
||||
`Outcome`, чтобы имена исходов не раздвоились. Существующие вызывающие
|
||||
(`cmd/healthlog/reindex*.go`, тесты) читают поля по-прежнему.
|
||||
- [x] 3.3 `replay.Run` переводится на `Player`, свою ветку отмены оставляет
|
||||
себе. Поведение и отчёт не меняются — проверяется существующими тестами
|
||||
пакета.
|
||||
- [x] 3.4 Табличный тест классификатора: ошибка → ожидаемый `Outcome`.
|
||||
|
||||
## 4. Воркер свёртки
|
||||
|
||||
- [x] 4.1 `replay.Worker` с синхронным швом: `Pass(ctx) (Outcome, error)` — один
|
||||
проход, без каналов; `Run(ctx)` — тонкий `select` поверх него по сигналу,
|
||||
тику и отмене; `Notify()` — неблокирующая отправка в канал ёмкостью 1;
|
||||
`done` закрывается в `defer` внутри `Run`.
|
||||
- [x] 4.2 `Pass`: выбирать `pending` порциями по курсору, сворачивать через
|
||||
`Player`, курсор строго возрастает; отмена проверяется **между**
|
||||
доставками; проход конечен даже когда доставка осталась `pending`.
|
||||
- [x] 4.3 Свёртка внутри прохода идёт на `context.WithoutCancel` от контекста
|
||||
прохода плюс собственный дедлайн (`foldTimeout`, переезжает из
|
||||
`internal/ingest`).
|
||||
- [x] 4.4 Отказ выборки — `ERROR` и выход из `Pass`, но не из `Run`: воркер
|
||||
переживает временный отказ базы.
|
||||
- [x] 4.5 Наблюдаемость: `INFO` с размером задолженности перед первым проходом;
|
||||
`WARN` «доставка ждала свёртки дольше пяти минут» — считается на выборке
|
||||
прохода, включается после того, как первый проход завершился. Ни значений
|
||||
точек, ни имён устройств.
|
||||
|
||||
## 5. Приём без свёртки
|
||||
|
||||
- [x] 5.1 `ingest.Service` теряет зависимость от `fold`: `Accept` пишет тело,
|
||||
учитывает доставку, логирует принятие и будит воркер. `foldTimeout` и
|
||||
вызов свёртки уходят.
|
||||
- [x] 5.2 Учёт доставки — на `context.WithoutCancel` с коротким дедлайном:
|
||||
обрыв соединения после записи тела не должен оставлять тело без строки в
|
||||
журнале. Проверка формы тела остаётся на исходном контексте.
|
||||
- [x] 5.3 Сигнал воркеру — параметр конструктора функцией; `nil` приводится к
|
||||
пустой функции **один раз в конструкторе**, как это уже делают `fold.New`
|
||||
и `replay.Run` со своими нулевыми значениями. Проверок на `nil` в местах
|
||||
вызова быть не должно.
|
||||
- [x] 5.4 `internal/httpapi` собирается без `fold`; транспорт по-прежнему не
|
||||
логирует исход и переводит только ошибки приёма.
|
||||
|
||||
## 6. Жизненный цикл в `serve.go`
|
||||
|
||||
- [x] 6.1 Собрать воркер, запустить `Run` в горутине, передать его `Notify` в
|
||||
`ingest`, разбудить при старте — этим и делается подбор `pending`.
|
||||
- [x] 6.2 Остановка: `srv.Shutdown` → отмена контекста воркера → ожидание
|
||||
`done` в остатке того же бюджета `shutdownTimeout` (30 с, как
|
||||
`stop_grace_period`).
|
||||
- [x] 6.3 Контекстная ошибка `Shutdown` — `WARN`, а не отказ команды: stdlib
|
||||
возвращает `DeadlineExceeded` штатно, а `main` печатает на любой ошибке
|
||||
`fatal startup` и выходит с кодом 1.
|
||||
- [x] 6.4 База не закрывается, пока воркер не вышел: `Close` под живой
|
||||
транзакцией свёртки дал бы `ERROR` по доставке, с которой всё в порядке.
|
||||
Не уложились — оставляем закрытие процессу и называем это `WARN`.
|
||||
|
||||
## 7. Бюджет ответа маршрута приёма
|
||||
|
||||
- [x] 7.1 `handleIngest` перед чтением тела ставит дедлайн записи ответа через
|
||||
`http.NewResponseController(w).SetWriteDeadline` на `read_timeout +
|
||||
write_timeout`. `http.ErrNotSupported` — `DEBUG` и продолжение, а не
|
||||
отказ приёма.
|
||||
- [x] 7.2 Общий `write_timeout` и его умолчание не меняются; комментарий в
|
||||
`config.example.toml` объясняет, что он покрывает и чтение тела и потому
|
||||
приём держит собственный бюджет.
|
||||
|
||||
## 8. Проверки
|
||||
|
||||
Приёмочные критерии — рубрика ревью дизайна, перенесена сюда целиком.
|
||||
|
||||
- [x] 8.1 **Завершимость прохода.** Доставка, оставшаяся `pending`, не
|
||||
выбирается проходом повторно; `Pass` возвращает управление.
|
||||
- [x] 8.2 **Атомарность единицы работы.** Прерванная свёртка оставляет доставку
|
||||
`pending` и не оставляет частично записанных объектов.
|
||||
- [x] 8.3 **Идемпотентность повтора.** Двойная свёртка той же доставки даёт тот
|
||||
же отпечаток витрины.
|
||||
- [x] 8.4 **Сигнал не теряет работу.** Доставка, чей сигнал потерян, всё равно
|
||||
подбирается — тиком или следующим проходом.
|
||||
- [x] 8.5 **Тотальный порядок.** Доставки с одинаковым `received_at`
|
||||
сворачиваются в порядке `id` при любом размере порции; порядок проверяется
|
||||
наблюдаемым следствием — наследованием слоя.
|
||||
- [x] 8.6 **Остановка.** Приём прекращается раньше воркера; после остановки нет
|
||||
доставки, числящейся разобранной и записанной наполовину; исчерпание
|
||||
бюджета не даёт ненулевого кода возврата.
|
||||
- [x] 8.7 **Транзиентный отказ ≠ отказ доставки.** Занятость базы оставляет
|
||||
`pending`, содержимое даёт `failed` (задача 2.2).
|
||||
- [x] 8.8 **Отставание наблюдаемо и при отсутствии прогресса.** Метка не
|
||||
срабатывает на задолженности первого прохода и срабатывает после него;
|
||||
считается на выборке, а не по факту свёртки.
|
||||
- [x] 8.9 **Одна классификация на оба входа.** Табличный тест `Player` (задача
|
||||
3.4) плюс зелёные существующие тесты `replay`.
|
||||
- [x] 8.10 **Тестируемость без сна.** Ни один тест воркера не ждёт по часам:
|
||||
проверки идут через `Pass`, тест на `Run` — один, «отмена завершает цикл».
|
||||
- [x] 8.11 **Приём не платит за воркер.** После `Accept` доставка числится
|
||||
`pending`, обработчик не ждёт; приём с неработающим воркером отвечает
|
||||
`200`.
|
||||
- [x] 8.12 **Данные о здоровье не в логе.** Записи воркера несут только
|
||||
идентификатор доставки, счётчики и длительности.
|
||||
- [x] 8.13 `task gate` зелёный; `task verify:archive` даёт то же состояние.
|
||||
|
||||
## 9. Документация
|
||||
|
||||
- [x] 9.1 `docs/architecture.md`, раздел «Приём»: ответ отдаётся после архивации
|
||||
и учёта — это **контракт**, а не деталь реализации; очередь — таблица, а
|
||||
не память; порядок журнала и остановка; классификация «отказ доставки»
|
||||
против «отказ обстоятельств»; бюджет ответа маршрута приёма; отвергнутые
|
||||
варианты с причинами.
|
||||
- [x] 9.2 Строка в задачу беклога `stats-nablyudaemost`: длина `pending`,
|
||||
возраст самой старой неразобранной доставки и то, что `/healthz` их не
|
||||
отражает.
|
||||
|
||||
## 10. Правки по ревью кода (профиль `deep`)
|
||||
|
||||
- [x] 10.1 `store.CreateDelivery` — через `inTx` с повторами: одиночная вставка
|
||||
пересиживала только `busy_timeout`, и приём отвечал `500` по доставке,
|
||||
тело которой уже на диске (измерено: окно занятости 6.55 с при широкой
|
||||
свёртке против пяти секунд ожидания).
|
||||
- [x] 10.2 Паника свёртки перехватывается в `fold.Fold` — у той же границы, что
|
||||
пишет исход разбора: в фоновой горутине она валила процесс, а
|
||||
`restart: unless-stopped` превращал дефект одной доставки в цикл
|
||||
перезапуска. Писатель `parse_status` остался единственным.
|
||||
- [x] 10.3 Флаг «первый проход завершён» снимается только у прохода, дошедшего
|
||||
до пустой выборки: взведённый на отказе базы, он включал метку задержки
|
||||
после прохода, который ничего не свернул.
|
||||
- [x] 10.4 Метка отставания — одна запись на проход (число задержанных и худшее
|
||||
ожидание), а не запись на доставку: задолженность в сотню тел давала бы
|
||||
сотню одинаковых `WARN` каждую минуту.
|
||||
- [x] 10.5 Отмена не пишется `ERROR`-ом в цикле воркера; ветка отказа `Serve` в
|
||||
`serve.go` останавливает воркер прежде, чем закрыть базу.
|
||||
- [x] 10.6 Значения заголовков обрезаются перед логом: `automation-name` длиной
|
||||
600 КБ выдавливал из ротации всю недавнюю историю (измерено).
|
||||
- [x] 10.7 Запасной `store.Now()` для метки приёма убран: он заводил второй
|
||||
источник времени вопреки соседнему комментарию и был недостижим.
|
||||
- [x] 10.8 `classify` неэкспортируема: вторая публичная дверь возвращала
|
||||
`Outcome` без `Partial`, то есть молча занижала счётчик, по которому
|
||||
решается судьба тела.
|
||||
- [x] 10.9 Оракул правила «занятость — обстоятельство»: `task verify:busy` —
|
||||
свёртка под удерживаемой блокировкой. В гейт не входит (25 секунд), но
|
||||
мутацию «убрать ветку `Transient`» убивает.
|
||||
- [x] 10.10 Дельты `storage` и `reindex`: «отказ ⇒ `failed`» сужено до отказов
|
||||
разбора, добавлен класс «отложено».
|
||||
- [x] 10.11 Остаточный предел порядка при конкурентных приёмах назван в спеке и
|
||||
в `docs/architecture.md`, вынут блокером
|
||||
(`docs/backlog/poryadok-zhurnala-na-priyome.md`).
|
||||
Reference in New Issue
Block a user