Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера, 4 «сейчас», 2 развилки. Все закрыты регрессионными тестами. - схема точки сна определяется по самой точке, а не по индексу в исходном массиве: одна пропущенная точка меняла эпизод и сводку местами - доставка сворачивается одной транзакцией: частичное состояние было недетерминированным (восемь прогонов — семь состояний) - граница размера на распакованном теле: 400 КиБ gzip разворачивались в 400 МиБ мимо max_body_mb - столкновение — расхождение канонических форм, а не байтов; WARN с координатами объекта; payload без HTML-экранирования - выравнивание по местной метке: получасовые зоны уводили часовую выгрузку в minute - доставка из одних суточных сводок больше не отвергается целиком - единицы не переписываются молча; счётчик считает сохранённые точки - все выходы Fold логируются, исход пишется на переживающем отмену контексте Четыре развилки вынесены блокерами в беклог.
4.7 KiB
Разнести ответ приёма и свёртку доставки
Приоритет: блокеры
Вынуто ревью кода задачи 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 — метка «ответ не уложился в таймаут» должна попасть туда.