Skip to content

graphile-worker: очередь на Postgres, замеры в комментариях и настройка, не пережившая полутора лет

v2 0 stars 0 forks 0 watchers 1 branch 0 runs Public
Ссылки на строки по историческому SHA: на main этого файла уже нет — его снёс тот самый откатv2

graphile-worker, очередь задач на Postgres. Автор и основной разработчик — Бенджи Гиллам (Benjie Gillam), тот же человек, что делает PostGraphile.

Очередь задач в базе устроена просто ровно до второго воркера. Дальше встаёт вопрос, который решают все и по-разному: как двум процессам взять из одной таблицы разные задачи и не подраться за одну.

Postgres даёт готовый ответ — FOR UPDATE SKIP LOCKED: «возьми строки, пропуская те, что уже кем-то заняты». Но есть ещё именованные очереди — когда задачи с одинаковым именем обязаны выполняться строго по одной. И вот тут одного SKIP LOCKED уже мало.

Кадр первый: 29 июня 2022

Приезжает коммит с названием, которое ничего не обещает:

Add a few strategies

Сто строк в файле выбора задачи — src/sql/getJob.ts, строки 28–52 в том самом коммите. Описания у коммита нет — вся документация внутри, комментарием над настройкой. И это лучший вид документации, потому что она не про то, как работает код, а про то, что показали замеры:

0 — we're not using named queues; skip them! …it's the absolute fastest strategy.

1 — for each matched job, go lock its job queue if you can. …what Worker traditionally used… but these days its a terrible strategy unless you're still randomly generating queue names (don't do that!). Performance is abysmal if you have a large jobs table with many higher priority but stuck jobs.

2 — lock the job queues up front, then find a job to do. …seems to be the fastest strategy for jobs that aren't in a queue…

3 — explicitly avoid locked job queues, but risk multiple jobs in same queue running at same time. Strategy 3 is probably unsafe. Don't use it.

Четыре варианта, из них один — «скорее всего небезопасен, не пользуйтесь» (строка 48). Оставлен в коде и снабжён предупреждением, потому что кому-то он всё-таки может понадобиться.

Кадр второй: пятый вариант, которого нет

Чуть ниже в том же файле лежит закомментированный кусок SQL с эпитафией — строки 98–101:

This strategy causes incredibly bad performance, presumably due to the lack of lock/skip locked

Эта стратегия даёт чудовищную производительность, предположительно из-за отсутствия lock/skip locked.

Его не удалили. Его положили рядом с надписью «пробовали, плохо» — чтобы следующий, кому эта идея покажется хорошей, сначала прочитал.

Слово presumably здесь важнее остального: автор не выдумывает объяснение задним числом, а честно помечает, что замер есть, а причина — догадка.

Кадр третий: 14 ноября 2023

Через год и четыре месяца приходит коммит e56613e9 с названием, которое просится в рамку:

Go back to the old way of doing it

Минус 267 строк, плюс 73. Файл выбора задачи худеет на 177 строк. Настраиваемость выбрасывают целиком.

Четыре стратегии прожили полтора года и свернулись обратно в одну. Замеры при этом никуда не делись — описания остались в коде. Перестал существовать сам выбор.

Почему именно так, в коммите не объяснено: он идёт в серии из семи коммитов одного дня с названиями вроде Checkout basics и More backporting — то есть в середине крупной перекладки, а не отдельным решением. Так что от домыслов воздержимся: известно, что выбор из четырёх стратегий не пережил полутора лет.

Ссылки выше ведут в коммит ad21f2a1, а не на ветку, и это здесь не формальность: на main этого файла больше нет — его и снёс тот самый откат. Ссылка на ветку показала бы «страница не найдена» ровно там, где читатель пошёл смотреть, о чём речь.

Цена решения, описанная ими самими

У этой конструкции есть цена, и найти её можно не в коде, а в документации для пользователей — в разделе про обработку ошибок:

If the worker is terminated in a way that cannot be handled (e.g. process.exit(), segfault, SIGKILL, someone pulled the power cord, etc) then the jobs that that worker was executing remain locked for at least 4 hours. Every 8-10 minutes a worker will sweep for jobs that have been locked for more than 4 hours and will make them available to be processed again automatically.

Если воркер завершился так, что обработать это нельзя — …кто-то выдернул шнур… — то задачи, которые он выполнял, остаются заблокированными не менее четырẻх часов.

Вот она, вся арифметика в одном абзаце. Задача помечается взятой (locked_at) на время работы. Воркер умер — пометка осталась. Значит нужен ещё один механизм, который ходит и разблокирует; в проекте под него отдельный файл, resetLockedAt.ts.

Выдернули шнур — пользователь ждёт четыре часа. Не потому что кто-то недодумал, а потому что это честная цена за «пометить взятой».

Что с этим делать у себя

1
Спросить, кто снимет пометку «взято» после SIGKILL

Прежде чем помечать запись взятой на время работы — найдите того, кто снимет пометку, если процесс умрёт между двумя строками.

Why: Если ответа нет — нужен второй механизм уборки, а это отдельная работа, которую надо написать, проверить и не сломать. Цена её отсутствия измерена авторами: четыре часа ожидания.
Check
  • Либо найден механизм сброса, либо показано, что помечать взятым нечего
2
Помечать попытку тем же запросом, которым выбирается записьRecommended

Альтернатива блокировке на время работы: одним UPDATE … FROM (SELECT … FOR UPDATE SKIP LOCKED) сразу поднять счётчик попыток и время следующей.

Why: Убитый процесс просто не сделает записи об успехе — запись созреет снова сама, по расписанию повторов. Сбрасывать нечего, потому что ничего не висит взятым.
Check
  • Проверено на двух параллельных воркерах: одна запись не достаётся двоим
3
Проверить, что разросшаяся таблица не замедляет опросRecommended

Авторы прямо предупреждают про «large jobs table with many higher priority but stuck jobs». Частичный индекс, исключающий доставленные и брошенные записи, снимает вопрос.

Why: Очередь растёт монотонно, а опрос идёт постоянно. Замедление приходит не сразу и потому выглядит как «что-то с базой».
Check
  • Есть частичный индекс по «созревшим» записям, а не по всей таблице
4
Не выносить выбор наружу, пока нечем выбиратьOptional

Чтобы выбрать между стратегиями 1 и 2, надо знать про свои именованные очереди, застрявшие задачи и размер таблицы. Проще выбрать за него.

Why: Четыре стратегии с настройкой прожили полтора года и свернулись в одну — у автора, который знает про свою очередь всё.
Check
  • Для каждой настройки назван, кто и по каким данным её выберет

Найти в открытом проекте настоящую историю поломки, восстановить ход мысли по написанному и закончить проверкой своего кода

v4Public 0 0
updated Sep 1, 2026

Medusa: защита от повтора отказывает там, где можно было ответить, и пропускает там, где повтор стоит денег

v1Public 0 0
updated Sep 1, 2026

Saleor: три человека за три года просят денег без копеек, потому что таких монет нет в обращении

v1Public 0 0
updated Sep 1, 2026

Better Auth: ограничение частоты считалось от последнего запроса, включая отклонённые, — и разблокировка не наступала никогда

v1Public 0 0
updated Sep 1, 2026

Medusa: кнопка «убрать скидку» выдавала максимальную, потому что ноль в JavaScript ложный

v1Public 0 0
updated Sep 1, 2026

Как Cal.com год чинил расписание, переходящее за полночь, и чем это кончилось

v2Public 0 0
updated Sep 1, 2026