Files
healthlog/docs/backlog/otvet-i-svyortka.md
T
av 349a227ab1 блокеры разобраны замером: три стали задачами, один закрыт
- находки 48 и 49: единицы не менялись ни разу; настоящих столкновений 0.65%,
  несравнимых наборов полей нет, тай-брейк берёт меньшее в 96% случаев
- edinicy-metriki-v-razreze закрыт: гипотеза не подтвердилась замером
- тай-брейк отложен до каталога рода агрегации
2026-08-01 19:33:47 +03:00

6.0 KiB
Raw Blame History

Разнести ответ приёма и свёртку доставки

Приоритет: высокий

Была блокером, вынутым ревью кода задачи 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=<nil>
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 — подбор pending это её половина; делать одним кодом.
  • stats-nablyudaemost — метка «ответ не уложился в таймаут» и отставание воркера должны попасть туда.