Inquir Compute · async

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

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

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

Суть ответа

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

Когда подходит и когда нет

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

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

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

Четыре точки входа — четыре паттерна, которые нужно освоить

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

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

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

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

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

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

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

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

Четыре типа async-триггеров

HTTP 202 handoff

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

Входящий вебхук

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

Cron-триггер

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

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

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

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

1

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

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

2

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

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

3

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

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

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

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

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 (webhook → 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 schedule → job)
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 (job → child job)
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: периодическая фоновая работа по расписанию (минимальный интервал — 5 минут)
  • Цепочка: fan-out, последовательные шаги или условные ветки из родительского шага пайплайна

Когда лучше выбрать другое

  • Работа, которая должна вернуть синхронный результат в пределах HTTP-таймаута — оставьте в хендлере

Вопросы и ответы

Один пайплайн может запускаться из нескольких точек входа?

Да. Один и тот же хендлер пайплайна можно вызвать из HTTP 202, вебхука или cron. Используйте event.trigger для различения контекста в хендлере при необходимости.

Есть ли встроенная очередь задач?

Вызовы пайплайнов ставятся в очередь внутренне до 100 на слот параллелизма функции. Это не durable message-очередь с гарантированным порядком — это управляемый слой выполнения. Для строгого FIFO на миллионах задач в секунду используйте отдельный message broker.

Каков минимальный интервал cron?

Пять минут. Планировщик опрашивает каждые 30 секунд, поэтому реальное время срабатывания может отставать на 30 секунд от запланированного. Суб-минутное расписание не поддерживается.

Как передать данные из запускающего шага в дочернюю задачу?

Передайте payload третьим аргументом в global.durable.startNew(). Дочерний пайплайн получит его как event.payload. Выходы родительского шага доступны как event.previousOutput в рамках того же пайплайна.