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

93 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Разнести ответ приёма и свёртку доставки
**Приоритет:** высокий
Была блокером, вынутым ревью кода задачи `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](reindex-iz-arhiva.md) — подбор `pending` это её половина;
делать одним кодом.
- [stats-nablyudaemost](stats-nablyudaemost.md) — метка «ответ не уложился в
таймаут» и отставание воркера должны попасть туда.