В один не самый прекрасный день сайт начал отдавать голый HTTP 500 на каждый запрос. Не медленная страница, не один сломанный роут — всё приложение лежало. Контейнер в панели хостинга значился «онлайн», но ни одна страница не рендерилась.
Что показали логи
Ошибка повторялась каждые несколько минут:
error: canceling statement due to lock timeout
code: '55P03'
where: while inserting index tuple in relation "queue"
Код 55P03 — это lock_not_available: запрос отказался ждать блокировку. Срабатывало внутри pgboss.create_queue, то есть при старте фонового воркера. А в базе до этого было залогировано, что Postgres «не был корректно завершён» — видимо, предыдущая сессия оставила лок на таблице pgboss.queue. Воркер при старте попытался создать очередь, не дождался лока, упёрся в таймаут и бросил исключение.
Почему одна блокировка уронила весь сайт
Воркер запускался внутри веб-процесса через instrumentation hook в Next.js. В коде register() стоял await startWorker(). Воркер упал — и исключение ушло прямо из хука. Next.js считает упавший instrumentation hook фатальной ошибкой и не поднимает сервер. Так проблема фоновой задачи превратилась в полный отказ фронтенда.
У воркера и веб-сервера не было никаких причин делить зону отказа. Один await связал их намертво.
Решение: два шага
Сначала — восстановление. Нужно перезапустить Postgres, чтобы снять зависший лок, и только потом передеплоить приложение, чтобы оно стартовало на здоровой базе. Перезапуск одного приложения не поможет: лок висит в базе, пока не перезапустится сам Postgres.
| Действие | Снимает лок? |
|---|---|
| Перезапуск приложения | Нет |
| Перезапуск Postgres | Да |
| Перезапуск Postgres + деплой приложения | Да |
Второй шаг — сделать так, чтобы веб-сервер поднимался даже если воркер не смог стартовать. Вместо await startWorker() — запуск «в фоне» с обработкой ошибки: startWorker().catch(...). Тогда падение воркера лишь пишет ошибку в лог, а сайт продолжает работать.
Дальше — сделать сам воркер устойчивым. Первая версия кода выставляла флаг started = true до вызова boss.start(). Если старт падал, все повторные попытки натыкались на проверку if (started) return — и воркер оставался мёртвым навсегда. Флаг нужно ставить только после успешного старта.
Выводы
- Запуск фонового воркера не должен делить зону отказа с веб-сервером. Если воркер живёт внутри веб-процесса — ловите его ошибки и дайте сайту подняться.
- Помечайте «запущено» только после реального запуска. Оптимистичный флаг превращает сбой в вечный отказ от повторных попыток.
- Ошибка Postgres 55P03 в библиотеке очередей почти всегда значит «остался лок после грязного завершения». Перезапуск базы снимает его, перезапуск приложения — обычно нет.
Теперь временный сбой базы деградирует до «задачи подождут минуту, пока воркер перезапустится» вместо «сайт лежит целиком». Это правильный размен.
Комментарии (1)
Войдите, чтобы комментировать.