Учебное руководство
Главы руководства
На этой странице

Фоновые задачи и Hatchet

Оглавление · Далее: CMS

Обновление сайта теперь выполняется напрямую через Strapi webhook и ISR Next.js. Ниже описан отдельный учебный воркер: автоматический producer из webhook удалён. Для проверки воркера событие нужно отправить отдельно через SDK или панель Hatchet.

Событие, задача, воркер

Событие — сообщение о произошедшем изменении. Задача — определение обработчика, который должен реагировать на событие. Воркер — запущенный процесс, который регистрирует определения и исполняет выданные ему задания.

В apps/functions/src/index.ts:

const contentChanged = hatchet.task({
  name: "strapi-content-changed",
  onEvents: [strapiEventName],
  retries: 3,
  fn: async (input: unknown) => {
    const event = strapiWebhookSchema.parse(input);
    console.info("Strapi content changed", event);
    return {
      event: event.event,
      model: event.model,
      documentId: event.entry.documentId ?? event.entry.id,
    };
  },
});

После определения создаётся hatchet.worker('gheilt-functions', ...) с workflows: [contentChanged], slots: 4, затем выполняется await worker.start(). Задача, не добавленная в workflows воркера, не начнёт выполняться от одного наличия функции в исходном файле.

Hatchet Lite включает управляющие части сервиса и использует PostgreSQL для очереди. Worker SDK подключается по gRPC. Воркер не принимает входящий HTTP; для вызова HTTP нужен отдельный producer. Route handler CMS теперь только обновляет кеш сайта.

Путь обновления сайта

Исходник схемы
sequenceDiagram
  participant CMS as Strapi
  participant API as Webhook Next.js
  participant Cache as Кеш Next.js
  CMS->>API: POST с Bearer-секретом и JSON
  API->>API: Проверка секрета и контракта
  API->>Cache: revalidateTag + revalidatePath
  API-->>CMS: 200 revalidated

Что даёт DX, похожий на serverless functions

Функция объявляется через SDK, у неё есть имя, триггер и настройка повторов. Очередь и выдачу работ обслуживает готовый Hatchet. Не нужно самостоятельно писать polling PostgreSQL или протокол регистрации воркеров.

Развёртывание остаётся обычным: один долгоживущий процесс Node.js в Docker. Несколько задач одного воркера разделяют память процесса, environment и права. Нет автоматического scale-to-zero, отдельной песочницы на вызов или отдельного образа на каждую функцию. Четыре слота не равны четырём CPU-потокам.

Типы, повторы и параллельность

Вход объявлен unknown, потому что сообщение внешнее. parse() выбрасывает исключение на неверных данных, которое SDK воспринимает как ошибку выполнения. Повторная проверка защищает воркер и от сообщений, которые producer отправил в обход HTTP webhook.

retries: 3 задаёт политику повторов Hatchet, а slots: 4 ограничивает одновременно исполняемые задачи этого экземпляра. Код не задаёт собственный retry backoff, rate limit, idempotency key или настройки timeout — действуют возможности и значения SDK/сервера закреплённых версий.

Повтор может произойти после того, как внешний побочный эффект уже выполнен. Например, письмо отправилось, но процесс завершился до сохранения результата. Поэтому задача отправки писем или изменения записи должна иметь собственный способ распознать уже выполненную операцию. documentId сам по себе не уникален для всех изменений документа: для дедупликации обычно нужен ID конкретного события/версии.

Как добавить задачу

  1. В contracts опишите вход и имя нового события.
  2. Объявите hatchet.task с уникальным именем и проверкой входа.
  3. Добавьте задачу в workflows перед запуском воркера.
  4. Создайте producer, который отправляет это событие.
  5. Проверьте в логах приём, выполнение и результат, включая повтор при ошибке.

Учебный пример, который можно добавить рядом с текущей задачей:

const echo = hatchet.task({
  name: "learning-echo",
  onEvents: ["learning:echo"],
  retries: 0,
  fn: async (input: unknown) => strapiWebhookSchema.parse(input),
});
// В существующем worker: workflows: [contentChanged, echo]

Этот пример повторно использует имеющийся контракт только для упражнения. Для реального нового события создайте отдельную схему вместо случайного переиспользования webhook Strapi. Не создавайте второй worker.start() в том же файле ради ещё одной задачи.

Dev и production

pnpm dev:functions сначала собирает contracts, затем запускает node --env-file-if-exists=../../.env --watch src/index.ts. Node 24 удаляет поддерживаемые TypeScript-аннотации, но не проверяет типы и не применяет tsconfig. Изменения contracts требуют отдельной пересборки; их .ts исходники не являются экспортом пакета. Для относительных импортов вспомогательных файлов нужно учитывать оба режима запуска: Node напрямую и tsc → JavaScript. Это отдельное упражнение, а не повод бездумно переносить алиас @/ из Next.js.

В production запускается node dist/index.js. Dockerfile использует pnpm --filter @gheilt/functions deploy --prod /out/functions: это упаковка production-зависимостей, не команда удалённого деплоя на VPS.

Node 24 о выполнении TypeScript.

Где смотреть и проверять

docker compose logs --tail=100 functions

Контейнер functions не имеет healthcheck. Статус running не доказывает, что он подключён к нужному tenant и выполняет задачи. Нужна проверка реального сообщения.