Очередь задач на Postgres, которую не нужно обслуживать

Выносите медленную работу с пути запроса, не поднимая Redis и парк воркеров. Durable-очередь задач на Postgres со встроенными ретраями, backoff, dead-letter путём и сбором по visibility-timeout.

Очередь задач на Postgres, которую не нужно обслуживать

Почти в каждом бэкенде наступает момент, когда запрос начинает делать слишком много. HTTP-обработчик, который раньше просто писал строку в базу, теперь ещё и меняет размер картинки, ходит в сторонний API и отправляет письмо — и вдруг ваш p99 равен трём секундам, а отказ где-то ниже по потоку роняет вместе с собой и ваш эндпоинт.

Решение старое и хорошо известное: вынесите медленную работу в очередь и отвечайте на запрос сразу. Загвоздка в том, что эксплуатация очереди — это отдельный проект. Вы поднимаете Redis или SQS, пишете воркер-процесс, поддерживаете его работу, дренируете его при деплое, добавляете логику ретраев и выстраиваете dead-letter путь для задач, которые так и не выполнились. Это очень много инфраструктуры ради того, чтобы убрать один вызов функции с пути запроса.

Inquir даёт вам очередь без инфраструктуры.

Ставьте в очередь и отвечайте

Из любого обработчика — HTTP, вебхук или cron — вы передаёте работу durable-задаче в фоне и сразу же возвращаете ответ:

export async function handler(event) {
  const payload = JSON.parse(event.body);
  await global.durable.startNew('processUpload', undefined, payload);
  return { statusCode: 202, body: JSON.stringify({ accepted: true }) };
}

Запрос завершается за миллисекунды. processUpload выполняется позже — на управляемом пуле воркеров, в собственном изолированном контейнере. Нет ни строки подключения к Redis, ни воркер-сервиса, который нужно держать запущенным, ни скрипта дренирования, который нужно запускать при деплое.

Что даёт вам «durable»

Очередь работает поверх Postgres, и в этом вся суть — задачи переживают процесс, который их создал. Из этого вытекают три свойства:

  • Ретраи с backoff. Задача несёт счётчик попыток и их потолок. Сбой при оставшихся попытках не отбрасывается, а возвращается в очередь с экспоненциальным backoff. Потолок задаёте вы; ожидание и повторную доставку берёт на себя платформа.
  • Dead-letter путь. Когда задача исчерпывает свои попытки, она не растворяется в строке лога — она попадает в состояние dead-letter с записанной последней ошибкой, так что poison-сообщения можно изучить и повторить после исправления, а не потерять.
  • Сбор по visibility-timeout. Если воркер забрал задачу и упал на середине выполнения, задача не зависает в статусе «в работе» навсегда. Её аренда (lease) истекает, и задача запускается заново — так что неудачный рестарт обходится вам в один ретрай, а не в потерянную задачу.

Это ровно те три вещи, которые люди вручную прикручивают к Redis, а здесь они — поведение очереди по умолчанию.

Каждый запуск — это запись

Поскольку очередь живёт в базе данных, каждая задача — это ещё и строка, на которую можно посмотреть. Входной payload, счётчик попыток, последняя ошибка, финальный статус — всё это доступно для запросов. Когда дежурного будят вопросом «а чек по заказу 5512 вообще ушёл?», это lookup, а не grep по stdout воркера. Сравните с очередью на Redis, где обработанная задача — это ключ, срок жизни которого истёк час назад.

Честно о том, чем это не является

Это durable-очередь задач, а не высокопроизводительный брокер сообщений. Если вам нужны миллионы строго упорядоченных сообщений в секунду или гарантии доставки exactly-once — вам нужны Kafka и команда, которая будет её обслуживать. Что это заменяет — так это операционную очередь, которая на самом деле есть почти у каждого приложения: «выполняй эту работу надёжно, вне пути запроса, с ретраями и местом, куда уходят сбои».

А ретраи означают, что задача может выполниться больше одного раза, поэтому действует обычное правило: делайте обработчики идемпотентными там, где повторный запуск мог бы удвоить эффект. Очередь обещает не потерять вашу задачу; она не обещает вызвать её ровно один раз.

Форма, к которой это подталкивает

Собранный воедино, паттерн почти для любой нагрузки «сделать позже» становится компактным:

  1. Точка входа (HTTP, вебхук, cron) валидирует, ставит в очередь и затем быстро возвращает ответ.
  2. Функция-задача делает настоящую работу в изоляции, а ретраи и backoff за неё уже обеспечены.
  3. Сбои, пережившие все попытки, ждут человека в состоянии dead-letter.
  4. Всё это можно инспектировать по каждому запуску, и не нужно хостить отдельный дашборд очереди.

Если вы таскаете за собой инстанс Redis и деплой воркера только ради того, чтобы вынести отправку писем и обработку картинок из вашего API — это ровно та инфраструктура, которую всё это призвано удалить. Вы пишете задачу; очередь уже запущена.