отработаны находки ревью кода

Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера,
4 «сейчас», 2 развилки. Все закрыты регрессионными тестами.

- схема точки сна определяется по самой точке, а не по индексу в исходном
  массиве: одна пропущенная точка меняла эпизод и сводку местами
- доставка сворачивается одной транзакцией: частичное состояние было
  недетерминированным (восемь прогонов — семь состояний)
- граница размера на распакованном теле: 400 КиБ gzip разворачивались в
  400 МиБ мимо max_body_mb
- столкновение — расхождение канонических форм, а не байтов; WARN с
  координатами объекта; payload без HTML-экранирования
- выравнивание по местной метке: получасовые зоны уводили часовую выгрузку
  в minute
- доставка из одних суточных сводок больше не отвергается целиком
- единицы не переписываются молча; счётчик считает сохранённые точки
- все выходы Fold логируются, исход пишется на переживающем отмену контексте

Четыре развилки вынесены блокерами в беклог.
This commit is contained in:
av
2026-08-01 19:02:05 +03:00
parent 32f21044e4
commit cd7a4c1493
23 changed files with 1340 additions and 175 deletions
+80
View File
@@ -0,0 +1,80 @@
# Разнести ответ приёма и свёртку доставки
**Приоритет:** блокеры
Вынуто ревью кода задачи `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` при старте.**
Цена: средняя — воркер, очередь, подбор при старте. Бонусом закрываются ещё
две дыры: параллельные доставки одной автоматизации перестают гонять
наследование слоя (сейчас вторая может не найти слоя первой и уйти в
`failed`), и доставка, застрявшая в `pending` из-за сбоя записи, наконец
кем-то подбирается.
**(б) Поднять `write_timeout` до согласованного с `foldTimeout`.**
Цена: малая. Но худший случай (64 МиБ) всё равно минуты, и молчание
`accessLog` остаётся.
**(в) Оставить как есть, задокументировав потолок размера доставки.**
Цена: нулевая. Широкие проходы продолжают рваться.
## Рекомендация
**(а).** Единственный вариант, который решает причину, а не симптом, и попутно
снимает две смежные находки. Он же приближает `reindex`: подбор `pending` при
старте — его половина.
## Что стоит без решения
Ничего: свёртка работает, просто рискует не уложиться в таймаут на самых
широких доставках. Данные при этом не теряются — тело ложится в архив **до**
свёртки.
Связано: [reindex-iz-arhiva](reindex-iz-arhiva.md),
[stats-nablyudaemost](stats-nablyudaemost.md) — метка «ответ не уложился в
таймаут» должна попасть туда.