Files
transcriber/tasks/items/db-closed-under-live-worker.md
T
av 75c6f0168a учёт: закрыт переезд хранилища, заведён урожай его ревью
- storage-without-pocketbase закрыта как реализованная: приёмка сошлась по всем
  пяти критериям записи и по двенадцати приёмочным свойствам рубрики ревью
  дизайна, работа лежит коммитом c9b7765
- урожай триажа ревью развёрнут в одиннадцать записей с тегом партии
  review-2026-08-23 и расставлен по зависимости, а не в конец списка
- находка про признак живости воркера слита в stalled-pipeline-metric: у неё та
  же причина — вставший конвейер неотличим от простоя
2026-08-23 08:41:49 +03:00

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`; здесь закрывается только путь после жёсткого
таймаута. Сроки захвата и рубежи не двигаются.