Files
healthlog/docs/backlog/otvet-i-svyortka.md
T
av cd7a4c1493 отработаны находки ревью кода
Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера,
4 «сейчас», 2 развилки. Все закрыты регрессионными тестами.

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

Четыре развилки вынесены блокерами в беклог.
2026-08-01 19:02:05 +03:00

4.7 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 при старте. Цена: средняя — воркер, очередь, подбор при старте. Бонусом закрываются ещё две дыры: параллельные доставки одной автоматизации перестают гонять наследование слоя (сейчас вторая может не найти слоя первой и уйти в failed), и доставка, застрявшая в pending из-за сбоя записи, наконец кем-то подбирается.

(б) Поднять write_timeout до согласованного с foldTimeout. Цена: малая. Но худший случай (64 МиБ) всё равно минуты, и молчание accessLog остаётся.

(в) Оставить как есть, задокументировав потолок размера доставки. Цена: нулевая. Широкие проходы продолжают рваться.

Рекомендация

(а). Единственный вариант, который решает причину, а не симптом, и попутно снимает две смежные находки. Он же приближает reindex: подбор pending при старте — его половина.

Что стоит без решения

Ничего: свёртка работает, просто рискует не уложиться в таймаут на самых широких доставках. Данные при этом не теряются — тело ложится в архив до свёртки.

Связано: reindex-iz-arhiva, stats-nablyudaemost — метка «ответ не уложился в таймаут» должна попасть туда.