Workflow
Workflow
Заголовок раздела «Workflow»Workflow — это устойчивые функции, оркеструющие activity и сохраняющие состояние при сбоях и перезапусках. Определяются с помощью типа записи workflow.lua.
Определение
Заголовок раздела «Определение»- name: order_workflow kind: workflow.lua source: file://order_workflow.lua method: main modules: - funcs - time - workflow meta: temporal: workflow: worker: app:workerПоля метаданных
Заголовок раздела «Поля метаданных»| Поле | Обязательно | Описание |
|---|---|---|
worker | Да | Ссылка на запись temporal.worker |
name | Нет | Пользовательское имя типа workflow (по умолчанию ID записи) |
Базовая реализация
Заголовок раздела «Базовая реализация»local funcs = require("funcs")local time = require("time")
local function main(order) local payment, err = funcs.call("app:charge_payment", { amount = order.total, customer = order.customer_id }) if err then return {status = "failed", error = tostring(err)} end
time.sleep("1h")
local shipment, err = funcs.call("app:ship_order", { order_id = order.id, address = order.shipping_address }) if err then funcs.call("app:refund_payment", payment.id) return {status = "failed", error = tostring(err)} end
return { status = "completed", payment_id = payment.id, tracking = shipment.tracking_number }end
return { main = main }Модуль workflow
Заголовок раздела «Модуль workflow»Модуль workflow предоставляет специфичные для workflow операции.
workflow.info()
Заголовок раздела «workflow.info()»Получение информации о текущем выполнении workflow:
local workflow = require("workflow")
local info = workflow.info()print(info.workflow_id) -- Workflow execution IDprint(info.run_id) -- Current run IDprint(info.workflow_type) -- Workflow type nameprint(info.task_queue) -- Task queue nameprint(info.namespace) -- Temporal namespaceprint(info.attempt) -- Current attempt numberprint(info.history_length) -- Number of history eventsprint(info.history_size) -- History size in bytesworkflow.exec()
Заголовок раздела «workflow.exec()»Синхронный запуск дочернего workflow с ожиданием результата:
local result, err = workflow.exec("app:child_workflow", input_data)if err then return nil, errendПростейший способ запуска дочерних workflow, когда нужно дождаться результата в текущем потоке.
workflow.version()
Заголовок раздела «workflow.version()»Обработка изменений кода с детерминированным версионированием:
local version = workflow.version("payment-v2", 1, 2)
if version == 1 then result = funcs.call("app:old_payment", input)else result = funcs.call("app:new_payment", input)endПараметры:
change_id— уникальный идентификатор измененияmin_supported— минимальная поддерживаемая версияmax_supported— максимальная (текущая) версия
Номер версии детерминирован для каждого выполнения workflow. Уже запущенные workflow продолжают использовать записанную версию, а новые используют max_supported.
workflow.attrs()
Заголовок раздела «workflow.attrs()»Обновление поисковых атрибутов и memo:
workflow.attrs({ search = { status = "processing", customer_id = order.customer_id, order_total = order.total }, memo = { notes = "Priority customer", source = "web" }})Поисковые атрибуты индексируются и доступны для запросов через Temporal visibility API. Memo — произвольные неиндексированные данные, прикреплённые к workflow.
workflow.history_length() / workflow.history_size()
Заголовок раздела «workflow.history_length() / workflow.history_size()»Мониторинг роста истории workflow:
local length = workflow.history_length()local size = workflow.history_size()
if length > 10000 then -- Consider continue-as-new to reset historyendЗапуск workflow
Заголовок раздела «Запуск workflow»Базовый spawn
Заголовок раздела «Базовый spawn»Запуск workflow из любого кода с помощью process.spawn():
local pid, err = process.spawn( "app:order_workflow", -- workflow entry "app:worker", -- temporal worker {order_id = "123"} -- input)Второй параметр указывает на temporal worker (не на обычный хост процессов). Workflow выполняется устойчиво в инфраструктуре Temporal.
Spawn с мониторингом
Заголовок раздела «Spawn с мониторингом»Мониторинг workflow для получения событий EXIT при завершении:
local pid, err = process.spawn_monitored( "app:order_workflow", "app:worker", {order_id = "123"})
local events = process.events()local event = events:receive()
if event.kind == process.event.EXIT then local result = event.result.value local error = event.result.errorendSpawn с именем
Заголовок раздела «Spawn с именем»Назначение имени workflow для идемпотентных запусков:
local spawner = process .with_options({}) :with_name("order-" .. order.id)
local pid, err = spawner:spawn_monitored( "app:order_workflow", "app:worker", {order_id = order.id})Если указано имя, Temporal использует его для дедупликации запусков. Запуск с тем же именем при работающем workflow по умолчанию возвращает PID существующего workflow.
Spawn с явным workflow ID
Заголовок раздела «Spawn с явным workflow ID»Установка конкретного Temporal workflow ID:
local spawner = process .with_options({ ["temporal.workflow.id"] = "order-" .. order.id, })
local pid, err = spawner:spawn_monitored( "app:order_workflow", "app:worker", order)Политики конфликтов ID
Заголовок раздела «Политики конфликтов ID»Управление поведением при запуске workflow с уже существующим ID:
-- Fail if workflow already existslocal spawner = process .with_options({ ["temporal.workflow.id"] = "order-123", ["temporal.workflow.id_conflict_policy"] = "fail", })
local pid, err = spawner:spawn("app:order_workflow", "app:worker", order)if err then -- Workflow already running with this IDend-- Error when already started (alternative approach)local spawner = process .with_options({ ["temporal.workflow.id"] = "order-123", ["temporal.workflow.execution_error_when_already_started"] = true, })
local pid, err = spawner:spawn("app:order_workflow", "app:worker", order)-- Reuse existing (default behavior with explicit ID)local spawner = process .with_options({ ["temporal.workflow.id"] = "order-123", })
local pid, err = spawner:spawn("app:order_workflow", "app:worker", order)-- Returns existing workflow PID if already running| Политика | Поведение |
|---|---|
"use_existing" | Вернуть PID существующего workflow (по умолчанию при явном ID) |
"fail" | Вернуть ошибку, если workflow существует |
"terminate_existing" | Завершить существующий и запустить новый |
Параметры запуска workflow
Заголовок раздела «Параметры запуска workflow»Передача параметров Temporal workflow через with_options():
local spawner = process.with_options({ ["temporal.workflow.id"] = "order-123", ["temporal.workflow.execution_timeout"] = "24h", ["temporal.workflow.run_timeout"] = "1h", ["temporal.workflow.task_timeout"] = "30s", ["temporal.workflow.id_conflict_policy"] = "fail", ["temporal.workflow.retry_policy"] = { initial_interval = 1000, backoff_coefficient = 2.0, maximum_interval = 300000, maximum_attempts = 3, }, ["temporal.workflow.cron_schedule"] = "0 */6 * * *", ["temporal.workflow.search_attributes"] = { customer_id = "cust-123" }, ["temporal.workflow.memo"] = { source = "api" }, ["temporal.workflow.start_delay"] = "5m", ["temporal.workflow.parent_close_policy"] = "terminate",})Полный справочник параметров
Заголовок раздела «Полный справочник параметров»| Параметр | Тип | Описание |
|---|---|---|
temporal.workflow.id | string | Явный ID выполнения workflow |
temporal.workflow.task_queue | string | Переопределение очереди задач |
temporal.workflow.execution_timeout | duration | Общий таймаут выполнения workflow |
temporal.workflow.run_timeout | duration | Таймаут одного запуска |
temporal.workflow.task_timeout | duration | Таймаут обработки задачи workflow |
temporal.workflow.id_conflict_policy | string | use_existing, fail, terminate_existing |
temporal.workflow.id_reuse_policy | string | allow_duplicate, allow_duplicate_failed_only, reject_duplicate |
temporal.workflow.execution_error_when_already_started | boolean | Ошибка, если workflow уже запущен |
temporal.workflow.retry_policy | table | Политика повторных попыток (см. ниже) |
temporal.workflow.cron_schedule | string | Cron-выражение для периодических workflow |
temporal.workflow.memo | table | Неиндексированные метаданные workflow |
temporal.workflow.search_attributes | table | Индексированные атрибуты для поиска |
temporal.workflow.enable_eager_start | boolean | Немедленный запуск выполнения |
temporal.workflow.start_delay | duration | Задержка перед началом workflow |
temporal.workflow.parent_close_policy | string | Поведение дочернего при закрытии родителя |
temporal.workflow.wait_for_cancellation | boolean | Ожидание завершения отмены |
temporal.workflow.namespace | string | Переопределение пространства имён Temporal |
Значения длительности принимают строки ("5s", "10m", "1h") или числа в миллисекундах.
Политика закрытия родителя
Заголовок раздела «Политика закрытия родителя»Определяет поведение дочерних workflow при закрытии родителя:
| Политика | Поведение |
|---|---|
"terminate" | Завершить дочерний workflow |
"abandon" | Позволить дочернему продолжить независимо |
"request_cancel" | Отправить запрос на отмену дочернему |
Сообщения при запуске
Заголовок раздела «Сообщения при запуске»Добавление сигналов в очередь для отправки в workflow сразу после запуска. Сообщения доставляются до любых внешних сигналов:
local spawner = process .with_options({}) :with_name("counter-workflow") :with_message("increment", {amount = 2}) :with_message("increment", {amount = 1}) :with_message("increment", {amount = 4})
local pid, err = spawner:spawn_monitored( "app:counter_workflow", "app:worker", {initial = 0})Этот подход особенно полезен с политикой конфликтов use_existing. Когда второй spawn разрешается в существующий workflow, начальные сообщения всё равно доставляются:
-- First spawn starts the workflow with initial messageslocal first = process .with_options({}) :with_name("my-counter") :with_message("increment", {amount = 3})
local pid, err = first:spawn("app:counter_workflow", "app:worker", {initial = 0})
-- Second spawn reuses existing workflow and delivers new messageslocal second = process .with_options({}) :with_name("my-counter") :with_message("increment", {amount = 2})
local pid2, err = second:spawn("app:counter_workflow", "app:worker", {initial = 999})-- pid2 == pid (same workflow), input {initial = 999} is ignored-- But the increment message with amount=2 is deliveredПередача контекста
Заголовок раздела «Передача контекста»Передача значений контекста, доступных внутри workflow и его activity:
local spawner = process.with_context({ user_id = "user-1", tenant = "tenant-1", request_id = "req-abc",})
local pid, err = spawner:spawn_monitored( "app:order_workflow", "app:worker", order)Внутри workflow (или любой вызванной activity) чтение контекста через модуль ctx:
local ctx = require("ctx")
local user_id = ctx.get("user_id") -- "user-1"local tenant = ctx.get("tenant") -- "tenant-1"local all = ctx.all() -- {user_id="user-1", tenant="tenant-1", request_id="req-abc"}Из HTTP-обработчиков
Заголовок раздела «Из HTTP-обработчиков»local function handler() local req = http.request() local order = json.decode(req:body())
local spawner = process .with_context({request_id = req:header("X-Request-ID")}) :with_options({ ["temporal.workflow.id"] = "order-" .. order.id, ["temporal.workflow.id_conflict_policy"] = "fail", })
local pid, err = spawner:spawn( "app:order_workflow", "app:worker", order )
local res = http.response() if err then res:set_status(409) return res:write_json({error = tostring(err)}) end
res:set_status(202) return res:write_json({ workflow_id = tostring(pid), status = "started" })endСигналы
Заголовок раздела «Сигналы»Workflow получают сигналы через систему сообщений. Сигналы устойчивы — они переживают replay workflow.
Паттерн inbox
Заголовок раздела «Паттерн inbox»Получение всех сообщений через inbox процесса:
local function main(order) local inbox = process.inbox()
while true do local msg = inbox:receive() local topic = msg:topic() local data = msg:payload():data()
if topic == "approve" then break elseif topic == "cancel" then return {status = "cancelled", reason = data.reason} end end
return process_order(order)endПодписка по топикам
Заголовок раздела «Подписка по топикам»Подписка на конкретные топики с помощью process.listen():
local function main(input) local results = {} local job_ch = process.listen("add_job") local exit_ch = process.listen("exit")
while true do local result = channel.select{ job_ch:case_receive(), exit_ch:case_receive() }
if result.channel == exit_ch then break elseif result.channel == job_ch then local job_data = result.value local activity_result, err = funcs.call( "app:echo_activity", {job_id = job_data.id, data = job_data} ) table.insert(results, { job_id = job_data.id, result = activity_result }) end end
return {total_jobs = #results, results = results}endПо умолчанию process.listen() возвращает необработанные данные payload. Используйте {message = true} для получения объектов Message с информацией об отправителе:
local ch = process.listen("request", {message = true})local msg = ch:receive()local sender = msg:from()local data = msg:payload():data()Множественные обработчики сигналов
Заголовок раздела «Множественные обработчики сигналов»Используйте coroutine.spawn() для параллельной обработки различных типов сигналов:
local function main(input) local counter = input.initial or 0 local done = false
coroutine.spawn(function() local ch = process.listen("increment", {message = true}) while not done do local msg, ok = ch:receive() if not ok then break end
local data = msg:payload():data() local reply_to = msg:from()
if type(data) ~= "table" or type(data.amount) ~= "number" then process.send(reply_to, "nak", "amount must be a number") else process.send(reply_to, "ack") counter = counter + data.amount process.send(reply_to, "ok", {value = counter}) end end end)
coroutine.spawn(function() local ch = process.listen("decrement", {message = true}) while not done do local msg, ok = ch:receive() if not ok then break end
local data = msg:payload():data() local reply_to = msg:from()
if counter - data.amount < 0 then process.send(reply_to, "nak", "would result in negative value") else process.send(reply_to, "ack") counter = counter - data.amount process.send(reply_to, "ok", {value = counter}) end end end)
-- Main coroutine waits for finish signal local finish_ch = process.listen("finish", {message = true}) local msg = finish_ch:receive() process.send(msg:from(), "ack") process.send(msg:from(), "ok", {message = "finishing"}) done = true
return {final_counter = counter}endПодтверждение сигналов
Заголовок раздела «Подтверждение сигналов»Реализация паттерна запрос-ответ через отправку ответов обратно отправителю:
-- Workflow sidelocal ch = process.listen("get_status", {message = true})local msg = ch:receive()process.send(msg:from(), "status_response", {status = "processing", progress = 75})-- Caller sidelocal response_ch = process.listen("status_response")process.send(workflow_pid, "get_status", {})
local timeout = time.after("5s")local result = channel.select{ response_ch:case_receive(), timeout:case_receive()}
if result.channel == response_ch then local status = result.valueendПередача сигналов между workflow
Заголовок раздела «Передача сигналов между workflow»Workflow могут отправлять сигналы другим workflow по PID:
-- Sender workflowlocal function main(input) local target_pid = input.target local ok, err = process.send(target_pid, "cross_host_ping", {data = "hello"}) if err then return {ok = false, error = tostring(err)} end
local response_ch = process.listen("cross_host_pong") local response = response_ch:receive() return {ok = true, received = response}endДочерние workflow
Заголовок раздела «Дочерние workflow»Синхронный дочерний (workflow.exec)
Заголовок раздела «Синхронный дочерний (workflow.exec)»Запуск дочернего workflow с ожиданием результата:
local result, err = workflow.exec("app:child_workflow", input_data)if err then return nil, errendАсинхронный дочерний (process.spawn)
Заголовок раздела «Асинхронный дочерний (process.spawn)»Запуск дочернего workflow без блокировки с последующим ожиданием завершения через события:
local events_ch = process.events()
local child_pid, err = process.spawn( "app:child_workflow", "app:worker", {message = "hello from parent"})if err then return {status = "spawn_failed", error = tostring(err)}end
-- Wait for child EXIT eventlocal event = events_ch:receive()
if event.kind == process.event.EXIT then local child_result = event.result.value local child_error = event.result.errorendПередача ошибок от дочерних workflow
Заголовок раздела «Передача ошибок от дочерних workflow»Если дочерний workflow возвращает ошибку, она появляется в событии EXIT:
local events_ch = process.events()local child_pid, err = process.spawn( "app:error_child_workflow", "app:worker")
local event = events_ch:receive()if event.result.error then local child_err = event.result.error -- Error objects have kind(), retryable(), message() methods print(child_err:kind()) -- e.g. "NOT_FOUND" print(child_err:retryable()) -- false print(child_err:message()) -- error message textendСинхронное выполнение workflow (process.exec)
Заголовок раздела «Синхронное выполнение workflow (process.exec)»Запуск workflow и ожидание результата одним вызовом:
local result, err = process.exec( "app:hello_workflow", "app:worker", {name = "world"})if err then return nil, errend-- result contains the workflow return valueМониторинг и связывание
Заголовок раздела «Мониторинг и связывание»Мониторинг после запуска
Заголовок раздела «Мониторинг после запуска»Мониторинг workflow после его запуска:
local pid, err = process.spawn( "app:long_workflow", "app:worker", {iterations = 100})
-- Monitor laterlocal ok, err = process.monitor(pid)
local events_ch = process.events()local event = events_ch:receive() -- EXIT when workflow completesСвязывание после запуска
Заголовок раздела «Связывание после запуска»Связывание с работающим workflow для получения LINK_DOWN при аварийном завершении:
local ok, err = process.set_options({trap_links = true})
local pid, err = process.spawn( "app:long_workflow", "app:worker", {iterations = 100})
-- Link after workflow has startedtime.sleep("200ms")local ok, err = process.link(pid)
-- If workflow is terminated, receive LINK_DOWNprocess.terminate(pid)
local events_ch = process.events()local event = events_ch:receive()-- event.kind == process.event.LINK_DOWNСобытия LINK_DOWN требуют trap_links = true в параметрах процесса. Без этого завершение связанного процесса приводит к распространению ошибки.
Отмена мониторинга / связывания
Заголовок раздела «Отмена мониторинга / связывания»Снятие мониторинга или связывания:
process.unmonitor(pid) -- stop receiving EXIT eventsprocess.unlink(pid) -- remove bidirectional linkПосле снятия мониторинга или связывания события для этого процесса больше не доставляются.
Завершение и отмена
Заголовок раздела «Завершение и отмена»Принудительное завершение
Заголовок раздела «Принудительное завершение»Принудительное завершение работающего workflow:
local ok, err = process.terminate(workflow_pid)Мониторящие вызывающие стороны получают событие EXIT с ошибкой.
Запрос корректной отмены с опциональной причиной:
local ok, err = process.cancel(workflow_pid, "cancelled by operator")Параллельная работа
Заголовок раздела «Параллельная работа»Используйте coroutine.spawn() и каналы для параллельной работы внутри workflow:
local function main(input) local worker_count = input.workers or 3 local job_count = input.jobs or 6
local work_queue = channel.new(10) local results = channel.new(10)
for w = 1, worker_count do coroutine.spawn(function() while true do local job, ok = work_queue:receive() if not ok then break end time.sleep(10 * time.MILLISECOND) results:send({worker = w, job = job, result = job * 2}) end end) end
for j = 1, job_count do work_queue:send(j) end work_queue:close()
local total = 0 local processed = {} for _ = 1, job_count do local r = results:receive() total = total + r.result table.insert(processed, r) end
return {total = total, processed = processed}endВсе операции с каналами и sleep внутри корутин безопасны для replay.
Таймеры
Заголовок раздела «Таймеры»Устойчивые таймеры переживают перезапуски:
local time = require("time")
time.sleep("24h")time.sleep("5m")time.sleep("30s")time.sleep(100 * time.MILLISECOND)Отслеживание прошедшего времени:
local start = time.now()time.sleep("1s")local elapsed = time.now():sub(start):milliseconds()Детерминизм
Заголовок раздела «Детерминизм»Код workflow должен быть детерминированным. Одинаковые входные данные должны порождать одинаковую последовательность команд.
Безопасные для replay операции
Заголовок раздела «Безопасные для replay операции»Эти операции автоматически перехватываются и их результаты записываются. При replay возвращаются записанные значения:
-- Activity callslocal data = funcs.call("app:fetch_data", id)
-- Durable sleeptime.sleep("1h")
-- Current timelocal now = time.now()
-- UUID generationlocal id = uuid.v4()
-- Crypto operationslocal bytes = crypto.random_bytes(32)
-- Child workflowslocal result = workflow.exec("app:child", input)
-- Versioninglocal v = workflow.version("change-1", 1, 2)Недетерминированные операции (избегать)
Заголовок раздела «Недетерминированные операции (избегать)»-- Don't use wall clock timelocal now = os.time() -- non-deterministic
-- Don't use random directlylocal r = math.random() -- non-deterministic
-- Don't do I/O in workflow codelocal file = io.open("data.txt") -- non-deterministic
-- Don't use global mutable statecounter = counter + 1 -- non-deterministic across replaysОбработка ошибок
Заголовок раздела «Обработка ошибок»Ошибки activity
Заголовок раздела «Ошибки activity»Ошибки activity содержат структурированные метаданные:
local result, err = funcs.call("app:risky_activity", order)if err then print(err:kind()) -- error classification (e.g. "NOT_FOUND", "INTERNAL") print(err:retryable()) -- whether the error is retryable print(err:message()) -- human-readable error messageendРежимы сбоя activity
Заголовок раздела «Режимы сбоя activity»Настройка поведения повторных попыток для вызовов activity:
local executor = funcs.new():with_options({ ["activity.retry_policy"] = { maximum_attempts = 1, }})
local result, err = executor:call("app:unreliable_activity", input)if err then local kind = err:kind() -- "INTERNAL" for runtime errors local retryable = err:retryable()endОшибки дочерних workflow
Заголовок раздела «Ошибки дочерних workflow»Ошибки дочерних workflow (через process.exec или события EXIT) содержат те же метаданные:
local result, err = process.exec("app:error_workflow", "app:worker")if err then print(err:kind()) -- e.g. "NOT_FOUND" print(err:retryable()) -- false print(err:message()) -- error detailsendПаттерн компенсации (Saga)
Заголовок раздела «Паттерн компенсации (Saga)»local function run_compensations(compensations) for _, comp in ipairs(compensations) do funcs.call(comp.action, comp.args) endend
local function main(order) local compensations = {}
local reservation, err = funcs.call("app:reserve_inventory", order.items) if err then return {status = "failed", step = "inventory", error = tostring(err)} end table.insert(compensations, 1, { action = "app:release_inventory", args = reservation.id })
local payment, err = funcs.call("app:charge_payment", order.payment) if err then run_compensations(compensations) return {status = "failed", step = "payment", error = tostring(err)} end table.insert(compensations, 1, { action = "app:refund_payment", args = payment.id })
local shipment, err = funcs.call("app:ship_order", order.shipping) if err then run_compensations(compensations) return {status = "failed", step = "shipping", error = tostring(err)} end
return {status = "completed", tracking = shipment.tracking}end