- каждая запись каталога задач получила тип вместо тега kind: и префикса заголовка; секция роадмапа «Разработка» стала «Сопровождением», порядок секций канонический - поправлены протухшие факты: нереализованные маршруты Read API, MCP и `healthlog import`, словарь слоёв в инварианте, семантика гейта по покрытию диффа, периметр перестал дублировать security.md - замер слияния переведён с находки 49 на находку 54, заполнены Purpose спек storage и parsing
55 lines
5.1 KiB
Markdown
55 lines
5.1 KiB
Markdown
# 🐞 Развести бюджеты остановки и оставить следы миграции в логе
|
||
|
||
- **Тип:** 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` (архив).
|
||
|
||
Двигает строку «Завершения» цели: «Остановка сервиса называет виновный этап честно, а накат миграций виден в логе старта».
|