- storage-without-pocketbase закрыта как реализованная: приёмка сошлась по всем
пяти критериям записи и по двенадцати приёмочным свойствам рубрики ревью
дизайна, работа лежит коммитом c9b7765
- урожай триажа ревью развёрнут в одиннадцать записей с тегом партии
review-2026-08-23 и расставлен по зависимости, а не в конец списка
- находка про признак живости воркера слита в stalled-pipeline-metric: у неё та
же причина — вставший конвейер неотличим от простоя
59 lines
4.6 KiB
Markdown
59 lines
4.6 KiB
Markdown
# 🐞 Не закрывать базу, пока живёт брошенный воркер
|
|
|
|
- **Тип:** fix
|
|
- **Категория:** Очередь — Остановка чинится раньше бюджета остановки: пока база закрывается под живым воркером, замерять мягкий бюджет нечем — процесс падает паникой, а захват стоит до восьми часов
|
|
- **Зачем:** По истечении жёсткого таймаута остановки процесс возвращается из run(), отложенный db.Close() обнуляет пулы, брошенный воркер разыменует nil и роняет процесс паникой; захват записи после этого стоит до восьми часов, и следа не остаётся.
|
|
- **Теги:** review-2026-08-23
|
|
|
|
Остановка сервиса не дожидается брошенных горутин. По истечении
|
|
`ForceShutdownTimeout` `run()` возвращается, отложенный `db.Close()` обнуляет
|
|
пулы соединений, а воркер, который не успел закончить шаг, разыменует `nil` и
|
|
роняет процесс паникой.
|
|
|
|
Замер прогона триажа: `Close()` вернулся за 2.8 мкс, пока другая горутина
|
|
держала пишущую транзакцию, и та зафиксировала её уже после закрытия.
|
|
|
|
Цена не в самой панике, а в следе: захваченная запись остаётся захваченной до
|
|
истечения срока захвата — до восьми часов, — и о причине в журнале нет ничего.
|
|
Инвариант «принятая запись не теряется молча» держится тем, что отказ виден
|
|
владельцу записи; здесь не виден никому.
|
|
|
|
Нашли проходы `review-code` (C2) и `review-ops` (O3) ревью change
|
|
`2026-08-23-storage-without-pocketbase`.
|
|
|
|
## Воспроизведение
|
|
|
|
1. Занять шаг конвейера работой дольше `[server] force_shutdown_timeout`.
|
|
2. Послать процессу `SIGTERM`.
|
|
3. По истечении жёсткого таймаута процесс возвращается из `run()` и закрывает
|
|
базу; брошенный воркер обращается к закрытому пулу и роняет процесс паникой.
|
|
4. Запись остаётся с признаком захвата, а в журнале об этом ни строки.
|
|
|
|
## Затрагивает
|
|
|
|
- порядок остановки в `cmd/transcriber/main.go` — возврат из `run()` и
|
|
отложенное закрытие базы;
|
|
- `internal/adapter/repo/sqlite/db.go` — `Close()` и жизненный цикл обоих пулов;
|
|
- цикл воркера `internal/controller/worker` — что делает шаг, у которого база
|
|
закрылась под руками;
|
|
- ключи конфига `[server] shutdown_timeout` и `force_shutdown_timeout`;
|
|
- поведение при остановке в спеке `pipeline`.
|
|
|
|
## Критерии приёмки
|
|
|
|
- Жёсткая остановка не роняет процесс паникой. **Оракул:** тест — шаг спит
|
|
дольше жёсткого таймаута, процесс останавливается, `recover` в тесте не
|
|
срабатывает, код возврата нулевой.
|
|
- Обращение к закрытой базе отвечает отказом, а не паникой. **Оракул:** тест
|
|
репозитория — вызов после `Close()` возвращает ошибку, и она отличима от
|
|
«записи нет».
|
|
- О брошенной работе остаётся след. **Оракул:** тест с перехваченным журналом —
|
|
жёсткая остановка при незакрытом шаге пишет строку с идентификатором записи и
|
|
причиной.
|
|
|
|
## Рамки
|
|
|
|
Бюджет мягкой остановки этой задачей не замеряется — это задача
|
|
`context-cancel-in-pipeline`; здесь закрывается только путь после жёсткого
|
|
таймаута. Сроки захвата и рубежи не двигаются.
|