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