Слогер Создать блог
Разработка

Postgres lock timeout уронил сайт: причины и решение

Сайт отдавал HTTP 500 на каждый запрос. Виной всему — зависшая блокировка в Postgres и одна строчка с await, связавшая воркер и веб-сервер. Разбираем, что пошло не так и как защититься.

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

Теперь временный сбой базы деградирует до «задачи подождут минуту, пока воркер перезапустится» вместо «сайт лежит целиком». Это правильный размен.

По материалам: dev. Текст переработан редакцией Слогера.

← На главную

Рекламное место — Конец поста
Реклама · Слогер

Комментарии (0)

Войдите, чтобы комментировать.

Пока нет комментариев. Будьте первым.