В один не самый прекрасный день сайт начал отдавать голый 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 в библиотеке очередей почти всегда значит «остался лок после грязного завершения». Перезапуск базы снимает его, перезапуск приложения — обычно нет.
Теперь временный сбой базы деградирует до «задачи подождут минуту, пока воркер перезапустится» вместо «сайт лежит целиком». Это правильный размен.
Комментарии (0)
Войдите, чтобы комментировать.
Пока нет комментариев. Будьте первым.