Files
healthlog/docs/tasks/items/shutdown-and-migration-traces.md
T
av 3d24248075 docs: документация приведена к канону av-dev-pm 4
- каждая запись каталога задач получила тип вместо тега kind: и префикса
  заголовка; секция роадмапа «Разработка» стала «Сопровождением», порядок
  секций канонический
- поправлены протухшие факты: нереализованные маршруты Read API, MCP и
  `healthlog import`, словарь слоёв в инварианте, семантика гейта по покрытию
  диффа, периметр перестал дублировать security.md
- замер слияния переведён с находки 49 на находку 54, заполнены Purpose спек
  storage и parsing
2026-08-05 19:09:35 +03:00

5.1 KiB
Raw Blame History

🐞 Развести бюджеты остановки и оставить следы миграции в логе

  • Тип: fix
  • Категория: Инфра
  • Зачем: Долгий запрос чтения съедает бюджет остановки, и WARN обвиняет воркер свёртки; миграция молчит и не прерывается SIGTERM
  • Теги: goal:deploy

Две находки эксплуатационного и идиоматического проходов ревью каталога. Обе существовали и раньше, но достижимыми их сделал первый маршрут чтения: GET /api/v1/metrics — первый обработчик, способный законно работать заметное время.

Бюджет остановки один на оба этапа. shutdownCtx в runServe передаётся и в srv.Shutdown, и в ожидание фонового воркера. Shutdown ждёт, пока обработчики вернутся; контексты обработчиков он при этом не отменяет (BaseContext не задан), так что долгий запрос каталога может съесть бюджет целиком. Дальше select видит два готовых случая и выбирает равновероятно: база закрывается или нет от запуска к запуску, а в лог уходит shutdown budget exceeded stage=fold-worker — обвинение воркеру, который бюджета не превышал. Цена именно в диагнозе: этот WARN означает «доставка осталась pending, данные под вопросом», и ложное срабатывание обесценивает настоящее.

Чинится двумя движениями: собственный context.WithTimeout второму этапу вместо исчерпанного первого, и BaseContext, производный от контекста жизненного цикла, чтобы долгий запрос об остановке узнавал.

Цена этой ветки выросла (change cena-chitayushchego-marshruta): база в ней не закрывается, а значит не закрывается и закреплённое соединение версии витрины — последнего соединения к базе не наступает, SQLite не делает финального чекпойнта, и рядом с базой остаётся неразобранный -wal до 64 МиБ. Данные целы (следующее открытие проиграет журнал), но файл базы в этом состоянии нельзя переносить без его -wal. Обвинение в логе при этом стало честнее: этап называется background, а не fold-worker, потому что ждут двоих.

Миграция молчит и не прерывается штатной остановкой. store.migrate не пишет ни одной записи — ни «начал», ни «закончил», ни длительность, — а первая строка в логе появляется уже после успешного открытия базы. Если миграция идёт долго, владелец не отличит «ещё мигрирует» от «зависло» и от «упало»: тишина одинакова во всех трёх случаях. Плюс migrate работает на context.Background(), то есть SIGTERM она не видит и ждать придётся 30-секундного SIGKILL.

Порчи данных при этом нет: goose оборачивает миграцию в транзакцию, обрыв откатывает её целиком, и следующий старт повторяет с нуля. Замер на синтетической копии годового объёма (260 тысяч объектов, 483 МБ): CREATE INDEX миграции 00009 — 297 мс тёплым кешем. То есть сегодня окно тишины — доли секунды; опасность в том, что оно растёт вместе с витриной незаметно.

Готово, когда WARN о превышении бюджета называет виновный этап честно, а в логе старта видно, что миграции накатывались и сколько это заняло.

Связано: cmd/healthlog/serve.go, internal/store/store.go, change 2026-08-02-cena-chitayushchego-marshruta (архив).

Двигает строку «Завершения» цели: «Остановка сервиса называет виновный этап честно, а накат миграций виден в логе старта».