Перейти к содержанию

Минимальные Workflow, Activity, Worker и Client

В первом уроке определили границу длительного согласования. Теперь соберём минимальный исполняемый каркас: Workflow вызывает reserveSlot, затем publish, а Client запускает его в очереди soglasovanie. На этой стадии редактора ещё не ждём. Такое упрощение позволяет понять соединение компонентов до добавления событий и длительных пауз.

Полный проект лежит в examples/temporal-workflows-lab. Он содержит package.json, tsconfig, Worker, Client, Activities и собственный учебный gateway. Автор не устанавливал зависимости, не выполнял TypeScript build и не запускал приложения. Все команды ниже предназначены для будущей самостоятельной локальной подготовки читателя.

Пакеты и граница модулей

В package.json пакеты @temporalio/client, worker, workflow, activity и common закреплены на 1.24.0. TypeScript закреплён на 5.9.3; соответствующий выпуск подтверждён официальным релизом. Lock-файл не создан, поскольку установка не проводилась. После самостоятельной установки читатель сохраняет его как часть собственного проверенного состояния.

В tsconfig выбран CommonJS и отдельный каталог dist. Это позволяет Worker использовать require.resolve для собранного Workflow. Выбор модуля нужен не ради обязательности CommonJS для всех проектов, а ради согласованной формы небольшого примера. При переходе на ESM пути и загрузка bundle должны быть настроены соответствующим образом.

Workflow импортирует типы Activities через import type. Реальные функции с HTTP-запросами не должны попасть в Workflow sandbox как обычный импорт исполнения. Тип описывает контракт, а вызов создаётся через proxyActivities. Такое разделение сохраняет проверку TypeScript и границу между предметной последовательностью и внешним I/O.

Ни Worker, ни Client не имеют собственного «экземпляра Workflow в памяти» как authoritative-объекта. Client отправляет команду Service, Worker исполняет полученные задачи. Если Client завершился после start, процесс согласования может продолжаться. Это особенно важно для веб-сервера: срок HTTP-запроса пользователя не должен становиться сроком всей заявки.

Первый полный Workflow

Общий gateway намеренно отделён от Temporal. Если бы Activity просто вернула строку «опубликовано», мы проверяли бы только вызов функции, а не внешний эффект. Здесь можно сопоставить результат Workflow с состоянием публикации. Даже маленькая локальная модель помогает понять, где возникает неопределённость при потере HTTP-ответа.

Каталог src содержит все необходимые определения. Submission и Result объявлены в contracts.ts, Activities зарегистрированы объектом activities, а gateway имеет собственный обработчик /effects. Выбор этих файлов не требует скачивания вымышленного шаблона. Архив курса впоследствии передаст тот же полный набор, не только выделенные для объяснения фрагменты.

Файл src/workflows/02-minimal.ts:

import { proxyActivities } from '@temporalio/workflow';
import type * as activities from '../activities';
import type { Submission, Result } from '../contracts';

const { reserveSlot, publish } = proxyActivities<typeof activities>({
  startToCloseTimeout: '10 seconds',
});

export async function approvePublication(input: Submission): Promise<Result> {
  await reserveSlot(input);
  const result = await publish(input);
  return { status: 'published', receipt: result.receipt };
}

Proxy выглядит как набор асинхронных функций, но не выполняет HTTP в sandbox. Он описывает Activity-вызовы для Temporal. await выражает зависимость: публикация начинается после подтверждённой брони. Сам reserveSlot находится в src/activities.ts и отправляет POST собственному gateway. Поэтому полный проект не требует неизвестного внешнего API.

Start-to-close timeout ограничивает одну попытку Activity. На этой стадии не задаём подробную retry-политику, поэтому применяются стандартные правила SDK/Service. В шестом уроке сделаем пределы явными и объясним, почему повтор внешнего действия должен быть безопасен. Уже сейчас нельзя трактовать proxy-вызов как «HTTP выполнится ровно один раз».

Workflow возвращает квитанцию внешней системы, а не генерирует её произвольно. Это помогает связать результат процесса с совершённым эффектом. Если ответ publish потерялся, значение появится после корректного повторного получения; придумывать receipt в catch означало бы сообщить о неподтверждённой публикации.

Подключение Worker и Client

Полный Worker выбирает стадию по WORKFLOW_STAGE из закрытого набора допустимых имён. Для этого урока значение 02-minimal. Его важная часть:

const worker = await Worker.create({
  connection,
  namespace: 'default',
  taskQueue: TASK_QUEUE,
  workflowsPath: require.resolve('./workflows/' + stage),
  activities,
  maxConcurrentActivityTaskExecutions: 4,
});

Здесь connection, TASK_QUEUE, stage и activities определены в полном worker.ts. Namespace default относится к локальному Temporal Service. Task Queue связывает старт процесса с подходящим Worker. Она не является URL gateway и не должна совпадать с именем приложения только по случайности.

Client использует client.workflow.start('approvePublication', options) и печатает Workflow ID с Run ID. Мы передаём имя типа строкой, чтобы обычный клиентский процесс не импортировал Workflow-модуль и его sandbox-зависимости. Полный Client содержит также signal, query, cancel и result для дальнейших уроков.

Start возвращает handle после принятия запуска, а не после окончания всей публикации. Для чтения результата используется handle.result(). В проекте это отдельная CLI-команда. Такое разделение подходит длительной работе: интерфейс может показать идентификатор заявки сразу, а завершение узнать позже, не удерживая исходное HTTP-соединение.

Будущая локальная подготовка

Порядок терминалов нужен для разных сроков жизни. Dev-server обычно остаётся работающим всё время лаборатории. Gateway нужен в моменты внешних операций. Worker может останавливаться и возвращаться. Client запускается кратко для отдельной команды. Такое разделение помогает наблюдать, что именно недоступно, вместо запуска одной большой функции, скрывающей все роли.

Не добавляйте обычный импорт gateway в Workflow ради удобства. Его файловые операции и HTTP-сервер относятся к Node-процессу вне sandbox. Граница модуля исполнения — часть примера: общими остаются чистые контракты и типы, а собственные внешние действия выполняются через зарегистрированные Activities.

В отдельном терминале читатель запускает dev-server с сохранением базы:

npm install
npm run build
temporal server start-dev --ip 127.0.0.1 --db-filename ./temporal.db

Затем в других терминалах:

npm run gateway
WORKFLOW_STAGE=02-minimal npm run worker
npm run client -- start article-102
npm run client -- result article-102

Команды запускают собственный gateway на loopback 7070, Worker с очередью soglasovanie и Client для article-102. Temporal Service слушает локальный 7233. Полные исходники не содержат реальной отправки письма или публикации сайта: gateway сохраняет учебное состояние в editorial-data/state.json.

Ожидаемый итог — published и квитанция, связанная с ключом заявки. Конкретный Run ID заранее неизвестен. Ошибка соединения с 7233 относится к доступу до Service, ошибка 7070 — к Activity и gateway. Если перепутать их, можно долго менять очередь, когда просто не запущен редакционный HTTP-сервис.

Dev-server удобен для локального обучения, но не является производственным развёртыванием Temporal. Параметр базы помогает пережить обычную остановку сервера; он не добавляет резервное копирование или кластерную доступность. Поддерживаемые параметры приведены в справке CLI server.

Теперь у проекта есть полный путь от Client до внешнего эффекта. Следующий урок рассматривает Task Queue, остановку Worker и повторный запуск, сохраняя тот же минимальный Workflow.

Оглавление серии