Очередь задач на Postgres, которую не нужно обслуживать
Выносите медленную работу с пути запроса, не поднимая Redis и парк воркеров. Durable-очередь задач на Postgres со встроенными ретраями, backoff, dead-letter путём и сбором по visibility-timeout.
Почти в каждом бэкенде наступает момент, когда запрос начинает делать слишком много. 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 и команда, которая будет её обслуживать. Что это заменяет — так это операционную очередь, которая на самом деле есть почти у каждого приложения: «выполняй эту работу надёжно, вне пути запроса, с ретраями и местом, куда уходят сбои».
А ретраи означают, что задача может выполниться больше одного раза, поэтому действует обычное правило: делайте обработчики идемпотентными там, где повторный запуск мог бы удвоить эффект. Очередь обещает не потерять вашу задачу; она не обещает вызвать её ровно один раз.
Форма, к которой это подталкивает
Собранный воедино, паттерн почти для любой нагрузки «сделать позже» становится компактным:
- Точка входа (HTTP, вебхук, cron) валидирует, ставит в очередь и затем быстро возвращает ответ.
- Функция-задача делает настоящую работу в изоляции, а ретраи и backoff за неё уже обеспечены.
- Сбои, пережившие все попытки, ждут человека в состоянии dead-letter.
- Всё это можно инспектировать по каждому запуску, и не нужно хостить отдельный дашборд очереди.
Если вы таскаете за собой инстанс Redis и деплой воркера только ради того, чтобы вынести отправку писем и обработку картинок из вашего API — это ровно та инфраструктура, которую всё это призвано удалить. Вы пишете задачу; очередь уже запущена.