Inquir Compute · async

Serverless async-задачи: четыре способа поставить работу в очередь

Асинхронная работа попадает в систему через четыре точки входа: HTTP-запрос с ответом 202, входящий вебхук от провайдера, cron-расписание или шаг пайплайна, запускающий другой пайплайн. У каждой точки входа свой паттерн. Эта страница собирает все четыре, чтобы вы выбрали подходящий под свой сценарий и не изобретали одну и ту же передачу в фон для каждого типа триггера.

Обновлено: 2026-06-28

Суть ответа

Serverless async-задачи: четыре способа поставить работу в очередь. В Inquir все четыре точки входа сходятся к одному примитиву: вызов пайплайна с изоляцией «один контейнер на функцию», политикой ретраев на шаг и записью прогона в истории выполнения (трассы хранятся 30 дней). Тип триггера — метаданные; код обработчика остаётся тем же.

Когда подходит

  • HTTP 202: инициированная пользователем работа, которая не должна блокировать ответ (обработка файлов, генерация отчётов)
  • Вебхук: события от провайдеров (Stripe, GitHub, Slack), где повторные доставки требуют идемпотентности
  • Cron: периодическая фоновая работа по расписанию (минимальный интервал — 1 минута)

На что обратить внимание

  • Обрабатывать все четыре точки входа одинаково — одна общая очередь, один общий воркер — значит, что семантика ретраев для cron-задачи совпадает с вебхуком Stripe, хотя требования к идемпотентности у них совершенно разные.
  • Воркер, читающий общую очередь, не может естественно отличить «это пришло из вебхука, который повторяет доставку 72 часа» от «это пришло из HTTP, и вызывающий ждёт». Разным триггерам нужна разная обработка, а не один цикл вычитки очереди.

Четыре точки входа — четыре паттерна

Большинство гайдов по async показывают один триггер — обычно HTTP-обработчик, который вызывает очередь. Но в реальных приложениях работа приходит с четырёх направлений: от пользователя (HTTP), от сторонних провайдеров (вебхуки), по расписанию (cron) и из внутренних шагов пайплайна (цепочка задач). У каждого — свои гарантии, свои ограничения по задержке и свой режим сбоя.

Ошибиться с паттерном точки входа дорого. HTTP-обработчик, синхронно ждущий медленную работу, упадёт по таймауту. Обработчик вебхука, делающий тяжёлую работу до ответа 200, спровоцирует повторные доставки провайдера и дубли побочных эффектов. Cron-триггер без защиты идемпотентности обработает данные дважды при рестарте планировщика.

Почему универсальный async-паттерн не работает

Обрабатывать все четыре точки входа одинаково — одна общая очередь, один общий воркер — значит, что семантика ретраев для cron-задачи совпадает с вебхуком Stripe, хотя требования к идемпотентности у них совершенно разные.

Воркер, читающий общую очередь, не может естественно отличить «это пришло из вебхука, который повторяет доставку 72 часа» от «это пришло из HTTP, и вызывающий ждёт». Разным триггерам нужна разная обработка, а не один цикл вычитки очереди.

Одна платформа, четыре типа триггеров, единое выполнение

В Inquir все четыре точки входа сходятся к одному примитиву: вызов пайплайна с изоляцией «один контейнер на функцию», политикой ретраев на шаг и записью прогона в истории выполнения (трассы хранятся 30 дней). Тип триггера — метаданные; код обработчика остаётся тем же.

У каждого триггера правильный контракт: HTTP-триггеры сразу возвращают 202; триггеры вебхуков подтверждают до таймаута провайдера; cron-триггеры проверяют выражение при сохранении (минимум 1 минута); цепочка задач передаёт выход родительского шага на вход дочернему. Async-вызовы ограничены 120 в минуту на тенант.

Четыре точки входа async-триггеров

Передача через HTTP 202

Валидируйте вход, вызовите global.durable.startNew(), верните 202 со ссылкой на задачу. Вызывающий получает ответ сразу; пайплайн работает вне окна запроса.

Триггер входящего вебхука

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

Триггер по cron-расписанию

Узел cronTrigger запускает пайплайн по cron-выражению (минимальный интервал — 1 минута). Выражения проверяются при сохранении. Каждый прогон создаёт запись в истории выполнения.

Цепочка задача → задача

Шаг пайплайна вызывает global.durable.startNew(), чтобы породить дочерний пайплайн: fan-out, последовательные цепочки или условные ветки по выходу родительского шага.

Как выбрать нужный async-триггер

1

Определить точку входа

Работа инициирована пользователем (HTTP), провайдером (вебхук), временем (cron) или самим пайплайном (цепочка задач)? Каждому варианту соответствует свой контракт триггера.

2

Применить подходящий паттерн

HTTP: вернуть 202 + startNew. Вебхук: проверить + подтвердить + startNew. Cron: cronTrigger + идемпотентный обработчик. Цепочка: startNew из шага с выходом родителя как payload.

3

Наблюдать все четыре в одной истории

История выполнения показывает каждый прогон независимо от источника триггера. Фильтруйте по функции, типу триггера или состоянию сбоя, не переключая дашборды.

Все четыре точки входа

Каждый сниппет показывает минимальный паттерн точки входа. Сам обработчик задачи (обработка event.payload) одинаков для всех четырёх — различаются только вызов постановки в очередь и его контекст.

triggers/http-handoff.mjs (HTTP → async-задача)
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 }) };
}
triggers/webhook-handoff.mjs (вебхук → async-задача)
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' };
}
triggers/cron-handler.mjs (cron-расписание → задача)
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 };
}
triggers/job-chain.mjs (задача → дочерняя задача)
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 внутри того же пайплайна.