Асинхронные задачи: четыре способа поставить работу в очередь
Асинхронная работа попадает в систему через четыре точки входа: HTTP-запрос с ответом 202, входящий вебхук от провайдера, cron-расписание или шаг пайплайна, запускающий другой пайплайн. У каждой точки входа свой паттерн. Эта страница собирает все четыре, чтобы вы выбрали подходящий под свой сценарий и не изобретали одну и ту же передачу в фон для каждого типа триггера.
Обновлено: 2026-10-26
Четыре точки входа — четыре паттерна
Большинство гайдов по async показывают один триггер — обычно HTTP-обработчик, который вызывает очередь. Но в реальных приложениях работа приходит с четырёх направлений: от пользователя (HTTP), от сторонних провайдеров (вебхуки), по расписанию (cron) и из внутренних шагов пайплайна (цепочка задач). У каждого — свои гарантии, свои ограничения по задержке и свой режим сбоя.
Ошибиться с паттерном точки входа дорого. HTTP-обработчик, синхронно ждущий медленную работу, упадёт по таймауту. Обработчик вебхука, делающий тяжёлую работу до ответа 200, спровоцирует повторные доставки провайдера и дубли побочных эффектов. Cron-триггер без защиты идемпотентности обработает данные дважды при рестарте планировщика.
Почему универсальный async-паттерн не работает
Обрабатывать все четыре точки входа одинаково — одна общая очередь, один общий фоновый обработчик — значит, что семантика повторов для cron-задачи совпадает с вебхуком Stripe, хотя требования к идемпотентности у них совершенно разные.
Фоновый обработчик, читающий общую очередь, не может естественно отличить «это пришло из вебхука, который повторяет доставку 72 часа» от «это пришло из HTTP, и вызывающий ждёт». Разным триггерам нужна разная обработка, а не один цикл вычитки очереди.
Одна платформа, четыре типа триггеров, единое выполнение
В Inquir все четыре точки входа сходятся к одному примитиву: вызов пайплайна с изоляцией «один контейнер на функцию», политикой повторов на шаг и записью прогона в истории выполнения (трассировки хранятся 30 дней). Тип триггера — метаданные; код обработчика остаётся тем же.
У каждого триггера правильный контракт: HTTP-триггеры сразу возвращают 202; триггеры вебхуков подтверждают до таймаута провайдера; cron-триггеры проверяют выражение при сохранении (минимум 1 минута); цепочка задач передаёт выход родительского шага на вход дочернему. Async-вызовы ограничены 120 в минуту на тенант.
Сравнение эксплуатации очередей
Что вы разворачиваете и эксплуатируете при каждом подходе — сравнение не возможностей, а эксплуатации.
| Зона ответственности | BullMQ + Redis | AWS SQS + Lambda | Пайплайны Inquir |
|---|---|---|---|
| Инфраструктура очереди | Кластер Redis (разворачиваете вы) | Очередь SQS (управляет AWS) | Нет — встроена в пайплайн |
| Процесс-обработчик | Обработчики BullMQ (запускаете вы) | Lambda-консьюмер (настраиваете вы) | Шаги пайплайна (запускает платформа) |
| Настройка повторов | На задачу в коде обработчика | Redrive policy + DLQ | На шаг в конфигурации пайплайна |
| История прогонов / наблюдаемость | Ключи Redis (короткий TTL) | CloudWatch + разбор DLQ | Записи выполнения (хранятся 30 дней) |
| Модель секретов | Отдельно от HTTP-маршрутов | IAM + Secrets Manager | Общие секреты рабочего пространства |
| Гарантия доставки | At-least-once через Redis | At-least-once через SQS | At-least-once (без гарантии порядка) |
Четыре точки входа async-триггеров
Передача через HTTP 202
Валидируйте вход, вызовите global.durable.startNew(), верните 202 со ссылкой на задачу. Вызывающий получает ответ сразу; пайплайн работает вне окна запроса.
Триггер входящего вебхука
Проверьте подпись провайдера по сырому телу, запишите ключ идемпотентности, быстро верните 200, затем поставьте тяжёлую работу в очередь. Повторные доставки провайдера упираются в проверку идемпотентности без повторной обработки.
Триггер по cron-расписанию
Узел cronTrigger запускает пайплайн по cron-выражению (минимальный интервал — 1 минута). Выражения проверяются при сохранении. Каждый прогон создаёт запись в истории выполнения.
Цепочка задача → задача
Шаг пайплайна вызывает global.durable.startNew(), чтобы породить дочерний пайплайн: fan-out, последовательные цепочки или условные ветки по выходу родительского шага.
Как выбрать нужный async-триггер
Определить точку входа
Работа инициирована пользователем (HTTP), провайдером (вебхук), временем (cron) или самим пайплайном (цепочка задач)? Каждому варианту соответствует свой контракт триггера.
Применить подходящий паттерн
HTTP: вернуть 202 + startNew. Вебхук: проверить + подтвердить + startNew. Cron: cronTrigger + идемпотентный обработчик. Цепочка: startNew из шага с выходом родителя как payload.
Наблюдать все четыре в одной истории
История выполнения показывает каждый прогон независимо от источника триггера. Фильтруйте по функции, типу триггера или состоянию сбоя, не переключаясь между панелями.
Все четыре точки входа
Каждый сниппет показывает минимальный паттерн точки входа. Сам обработчик задачи (обработка event.payload) одинаков для всех четырёх — различаются только вызов постановки в очередь и его контекст.
export async function handler(event) { const { reportId } = JSON.parse(event.body || '{}'); if (!reportId) return { statusCode: 400, body: JSON.stringify({ error: 'reportId required' }) }; // Return immediately — job runs outside HTTP window const { instanceId: jobId } = await global.durable.startNew('generate-report', undefined, { reportId }); return { statusCode: 202, body: JSON.stringify({ jobId }) }; }
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const sig = event.headers['x-webhook-signature'] ?? ''; const expected = createHmac('sha256', process.env.WEBHOOK_SECRET).update(body).digest('hex'); if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return { statusCode: 401, body: 'invalid signature' }; const payload = JSON.parse(body); const isNew = await db.upsertEvent(payload.id, payload.type); // idempotency key if (!isNew) return { statusCode: 200, body: 'duplicate' }; await global.durable.startNew('process-event', undefined, { eventId: payload.id, type: payload.type }); return { statusCode: 200, body: 'accepted' }; }
export async function handler(event) { // event.trigger.type === 'schedule' when fired by a cronTrigger node // Cron minimum interval: 1 minute; fires within ~30s of scheduled time const since = process.env.LAST_CURSOR ?? new Date(Date.now() - 86_400_000).toISOString(); const records = await source.fetchUpdatedSince(since); if (records.length === 0) return { synced: 0 }; await destination.upsertBatch(records); // idempotent by record ID return { synced: records.length, cursor: records.at(-1)?.updatedAt }; }
export async function handler(event) { // Parent step output available as event.previousOutput const { batchId, items } = event.payload ?? {}; // Fan-out: spawn one child pipeline per item const children = await Promise.all( items.map((item) => global.durable.startNew('process-item', undefined, { batchId, itemId: item.id }) ) ); return { batchId, spawned: children.length }; }
Выбирайте триггер по типу точки входа
Когда это уместно
- HTTP 202: инициированная пользователем работа, которая не должна блокировать ответ (обработка файлов, генерация отчётов)
- Вебхук: события от провайдеров (Stripe, GitHub, Slack), где повторные доставки требуют идемпотентности
- Cron: периодическая фоновая работа по расписанию (минимальный интервал — 1 минута)
- Цепочка задач: fan-out, последовательные шаги или условные ветки из родительского шага пайплайна
Когда лучше выбрать другое
- Работа, которая должна вернуть синхронный результат вызывающему в пределах HTTP-таймаута, — оставьте её в обработчике
Частые вопросы
Один пайплайн может запускаться из нескольких точек входа?
Да. Один и тот же обработчик пайплайна можно вызвать из HTTP-пути с 202, обработчика вебхука или cron-триггера. При необходимости различайте контекст в обработчике через event.trigger.
Под капотом есть встроенная очередь задач?
Вызовы пайплайнов ставятся во внутреннюю очередь — до 100 на слот параллелизма функции. Это не надёжная очередь сообщений с гарантированным порядком, а управляемый слой выполнения. Для строгого FIFO на миллионах задач в секунду используйте выделенный брокер сообщений.
Каков минимальный интервал cron?
Одна минута. Планировщик опрашивает каждые 30 секунд, поэтому реальное срабатывание может отстать от запланированной минуты до 30 секунд. Расписания чаще раза в минуту не поддерживаются.
Как передать данные из запускающего шага в дочернюю задачу?
Передайте payload третьим аргументом в global.durable.startNew(). Дочерний пайплайн получит его как event.payload. Выходы родительского шага доступны как event.previousOutput внутри того же пайплайна.
Гарантирует ли Inquir доставку exactly-once?
Нет. Вызовы пайплайнов выполняются at-least-once. Делайте обработчики идемпотентными: проверяйте ключ дедупликации (например, exportId или jobId) до записи побочных эффектов, чтобы повторы были безопасны.
Можно ставить задачи из обработчиков на Python или Go?
global.durable.startNew() — метод Node.js SDK. Из Python или Go отправьте POST на URL триггера пайплайна с тем же payload — API триггера работает по обычному HTTP из любой среды выполнения.