- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
8.0 KiB
8.0 KiB
Why
Свёртка выполняется внутри обработчика запроса, поэтому время ответа равно
времени свёртки: 16 тысяч точек — 11 секунд. WriteTimeout в Go ставится в
readRequest, то есть до вызова обработчика, и его 30 секунд — общий бюджет на
всё: дочитать тело по мобильной сети, записать архив, вставить строку, свернуть.
Когда бюджет выходит, сервер считает, что отдал 200 (ошибки записи
обработчику не видно, ответ ушёл в буфер), клиент получает обрыв, а accessLog
пишет status_code=200 — единственный канал наблюдаемости в этом сценарии врёт.
Бьёт это по широким проходам (Today, Previous 7 Days, ручной экспорт) —
ровно по тем, ради которых заведён инвариант «дыры закрываются сами».
What Changes
- Приём отвечает
200после архивации тела и вставки строкиdelivery. Свёртка из обработчика уходит: время ответа перестаёт зависеть от ширины доставки. - Свёртку ведёт фоновый воркер — одна горутина, обработка в порядке журнала
(
received_at,id) среди доставок, видимых ему на момент выборки. - Очередью служит сама таблица, а не список идентификаторов в памяти:
доставка ждёт свёртки в статусе
pending, канал несёт только сигнал «есть работа». Отсюда три следствия: переполнять нечего (доставка и такpending, это не отказ), падение процесса очередь не теряет, а «подборpendingпри старте» перестаёт быть отдельным кодом — это обычный проход воркера. - Классификация исхода свёртки (
folded/ слой не выведен / содержимое не разобрано / прочее / частичный разбор) становится общей с пересборкой: один проигрыватель вinternal/replay, а не второй рядом. - Исход свёртки начинает отражать доставку, а не обстоятельства. Сегодня
failedпишется на любой ошибке, включая занятость базы; с воркером это означало бы, что доставка выбывает из очереди навсегда — а конкуренция за базу как раз становится штатной. Отмена и занятость статус больше не меняют, доставка остаётсяpending; всё прочее по-прежнемуfailedи возвращается только пересборкой. - Остановка сервиса формулируется инвариантом, а не обещанием досчитать: приём прекращается раньше воркера, и после остановки нет доставки, которая числится разобранной, а записана наполовину. Свёртка идёт на контексте, отвязанном от остановки.
- Наблюдаемость воркера:
WARN, когда доставка ждала свёртки дольше периода быстрого прохода, и одна строкаINFOо размере задолженности при старте. Метка считается на выборке прохода, а сам проход будит не только сигнал, но и тик — иначе «работа есть, прогресса нет» неотличимо от пустого потока. Счётчики в/stats— задачаstats-nablyudaemost, здесь только метки в логе. - Убирается второй, оставшийся источник молчаливого обрыва: общий
write_timeout(30 с) меньшеread_timeout(5 мин), а он покрывает и чтение тела — то есть медленная загрузка 64 МиБ обрывается независимо от свёртки. Длинный бюджет даётся маршруту приёма собственным дедлайном ответа; общий таймаут и конфиг не меняются, и прочие маршруты защиту не теряют. - Миграция: частичный индекс по неразобранным доставкам — воркер спрашивает их чаще, чем раз в минуту, а таблица растёт на ~300 строк в сутки.
Capabilities
New Capabilities
ingest: приём доставки как самостоятельное поведение — что делает ответ200заслуженным, когда он отдаётся, кто и в каком порядке сворачивает принятое, что происходит с несвёрнутым при остановке и рестарте.
Modified Capabilities
parsing: требование «Разбор не влияет на код ответа приёма» уточняется — разбор идёт после ответа, поэтому исход становится виден не в ответе и не сразу, а асинхронно, вparse_statusи в логе.storage: требование «Учёт частично разобранной доставки» уточняется — отказ обстоятельств (отмена, занятость базы) статуса не меняет вовсе, а прежняя формулировка «ошибка ⇒failed» этого не допускала.reindex: требование «Отчёт, оракул и исход команды» получает четвёртый класс отказа — «работа отложена по обстоятельствам»: классы у пересборки и у фоновой свёртки общие.
Impact
internal/ingest— теряет зависимость отinternal/fold:Acceptкладёт тело, учитывает доставку и будит воркер.internal/replay— общий проигрыватель (свернуть доставку, классифицировать исход) и фоновый воркер поверх него.internal/store— выборка неразобранных доставок в порядке журнала с курсором, сентинел занятости базы; миграция00006с частичным индексом.internal/fold— отмена и занятость базы больше не переводят доставку вfailed.cmd/healthlog/serve.go— жизненный цикл воркера и согласованная остановка.internal/httpapi— сборкаingest.Serviceбез свёртки; собственный дедлайн ответа на маршруте приёма.config.example.toml— комментарий кwrite_timeout.docs/architecture.md,docs/database.md— путь приёма и новый индекс.