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