# Разнести ответ приёма и свёртку доставки **Приоритет:** высокий Была блокером, вынутым ревью кода задачи `razbor-metrik-v-obekty` (профиль `deep`, находка №4 триажа, severity major). **Решение принято** — ниже задача. ## Что не так сегодня Свёртка выполняется **синхронно внутри обработчика запроса**, поэтому время ответа равно времени свёртки. `WriteTimeout` в Go ставится в `readRequest` — **до** чтения тела и до вызова обработчика (`net/http/server.go:993-997`, прочитано в исходниках). Значит 30 секунд по умолчанию это бюджет на всё сразу: дочитать до 64 МиБ по мобильной сети, сделать `fsync` архива, вставить доставку и свернуть. Воспроизведено минимальной программой: сервер с `WriteTimeout=200ms`, обработчик спит 500 мс. ``` handler: WriteHeader(200), body Write err= client: elapsed=501ms err=EOF ``` Сервер считает, что отдал `200` — ошибки записи не видно, ответ ушёл в буфер и сбрасывается позже. Клиент получил обрыв. Код обработчика этого не видит, а `accessLog` честно запишет `status_code=200`: единственный сегодняшний канал наблюдаемости в этом сценарии врёт. Стоимость свёртки измерена **до** перехода на одну транзакцию на доставку: | тело | объектов | свёртка | |---|---|---| | 80 КиБ | 1001 | 815 мс | | 323 КиБ | 4001 | 3.07 с | | 1302 КиБ | 16001 | 11.07 с | Одна транзакция на доставку убрала около 0.7 мс на объект (прогон живого архива ускорился с 64 до 52 секунд), но порядок величины остался: широкая доставка по-прежнему измеряется секундами. Бьёт это по **широким проходам** — `Today`, `Previous 7 Days`, ручной экспорт, — то есть ровно по тем, ради которых заведён инвариант «дыры закрываются сами». ## Что решено Вариант (а): **отвечать `200` сразу после архивации и учёта; свёртка — воркером в порядке журнала, с подбором `pending` при старте.** Почему он, а не альтернативы: - Поднять `write_timeout` до согласованного с `foldTimeout` — дёшево, но худший случай (64 МиБ) всё равно минуты, и молчание `accessLog` остаётся. Это лечит симптом. - Оставить как есть — широкие проходы продолжают рваться. Вариант (а) решает причину и попутно снимает две смежные дыры: параллельные доставки одной автоматизации перестают гонять наследование слоя (сейчас вторая может не найти слоя первой и уйти в `failed`), и доставка, застрявшая в `pending` из-за сбоя записи, наконец кем-то подбирается. ## Что делать 1. Воркер свёртки: одна горутина, очередь идентификаторов доставок, обработка **строго в порядке журнала** (`received_at`, `id`) — от этого зависит наследование слоя и воспроизводимость. 2. Приём отвечает `200` после архивации и вставки доставки; свёртку ставит в очередь. Очередь переполнена — доставка остаётся `pending`, это не отказ. 3. Подбор `pending` при старте, тем же путём. Это половина `reindex`, поэтому код должен быть общим с ним, а не соседним. 4. Остановка сервиса дожидается текущей доставки: свёртка — одна транзакция, рвать её нечем, но очередь надо дренировать осознанно. 5. Метка «доставка ждала свёртки дольше N» — в наблюдаемость, чтобы отставание воркера было видно до того, как оно станет отставанием на сутки. 6. Тесты: порядок журнала соблюдается при конкурентных доставках; `pending` подбирается при старте; отмена контекста не оставляет половинчатого состояния; `task verify:archive` даёт то же состояние. ## Что стоит без решения Ничего: свёртка работает, просто рискует не уложиться в таймаут на самых широких доставках. Данные при этом не теряются — тело ложится в архив **до** свёртки. ## Связано - [reindex-iz-arhiva](reindex-iz-arhiva.md) — подбор `pending` это её половина; делать одним кодом. - [stats-nablyudaemost](stats-nablyudaemost.md) — метка «ответ не уложился в таймаут» и отставание воркера должны попасть туда.