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

4.6 KiB

🐞 Не закрывать базу, пока живёт брошенный воркер

  • Тип: 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.goClose() и жизненный цикл обоих пулов;
  • цикл воркера internal/controller/worker — что делает шаг, у которого база закрылась под руками;
  • ключи конфига [server] shutdown_timeout и force_shutdown_timeout;
  • поведение при остановке в спеке pipeline.

Критерии приёмки

  • Жёсткая остановка не роняет процесс паникой. Оракул: тест — шаг спит дольше жёсткого таймаута, процесс останавливается, recover в тесте не срабатывает, код возврата нулевой.
  • Обращение к закрытой базе отвечает отказом, а не паникой. Оракул: тест репозитория — вызов после Close() возвращает ошибку, и она отличима от «записи нет».
  • О брошенной работе остаётся след. Оракул: тест с перехваченным журналом — жёсткая остановка при незакрытом шаге пишет строку с идентификатором записи и причиной.

Рамки

Бюджет мягкой остановки этой задачей не замеряется — это задача context-cancel-in-pipeline; здесь закрывается только путь после жёсткого таймаута. Сроки захвата и рубежи не двигаются.