액티비티
Temporal 액티비티는 비결정론적 작업을 실행합니다. function.lua 또는 process.lua 엔트리의 메타데이터를 통해 액티비티로 등록하세요.
이 페이지의 코드 조각은 API 사용법을 보여 주는 예시입니다. 결제 예제를 실제로 사용하려면 애플리케이션이 소유하는 환경 엔트리, 자격 증명에 대한 env.get 권한, 결제 공급자 URL에 대한 http_client.request 권한, 그리고 결제 공급자 계약이 필요합니다.
액티비티 등록
함수를 액티비티로 등록하려면 meta.temporal.activity를 추가하세요:
- name: charge_payment
kind: function.lua
source: file://payment.lua
method: charge
modules:
- env
- errors
- http_client
- json
meta:
temporal:
activity:
worker: app:worker
메타데이터 필드
| 필드 | 필수 | 설명 |
|---|---|---|
worker |
예 | temporal.worker 엔트리 참조 |
local |
아니오 | 로컬 액티비티로 실행 (기본값: false) |
name |
아니오 | 커스텀 액티비티 타입 이름 (기본값: 엔트리 ID) |
구현
액티비티는 일반 Lua 함수입니다. Temporal은 워크플로우 입력을 워크플로우 이력에 저장하므로 자격 증명을 입력에 포함하지 마세요. 이 예제는 액티비티 안에서 환경 레지스트리의 결제 키를 읽습니다. 예시 공급자는 JSON 결제 요청을 받고 JSON 응답을 반환합니다. 상태 매핑은 애플리케이션이 소유하는 정책입니다. URL, 요청 필드, 응답 필드, 실패 매핑을 실제 공급자 계약에 맞게 바꾸세요.
-- payment.lua
local http = require("http_client")
local json = require("json")
local env = require("env")
local errors = require("errors")
local function payment_error(status)
if status == 408 then
return errors.new({kind = errors.TIMEOUT, message = "payment provider timed out", retryable = true})
elseif status == 429 then
return errors.new({kind = errors.RATE_LIMITED, message = "payment provider rate limited the request", retryable = true})
elseif status >= 500 then
return errors.new({kind = errors.UNAVAILABLE, message = "payment provider is unavailable", retryable = true})
end
return errors.new({kind = errors.INVALID, message = "payment request was rejected", retryable = false})
end
local function charge(input)
local api_key, env_err = env.get("PAYMENTS_API_KEY")
if env_err then return nil, env_err end
local body, encode_err = json.encode({
amount = input.amount,
currency = input.currency,
payment_token = input.payment_token
})
if encode_err then
return nil, encode_err
end
local response, err = http.post("https://payments.example.com/v1/charges", {
headers = {
["Authorization"] = "Bearer " .. api_key,
["Content-Type"] = "application/json"
},
body = body
})
if err then
return nil, err
end
if response.status_code >= 400 then
return nil, payment_error(response.status_code)
end
return json.decode(response.body)
end
return { charge = charge }
액티비티 호출
워크플로우에서 funcs 모듈을 사용합니다:
local funcs = require("funcs")
local result, err = funcs.call("app:charge_payment", {
amount = 5000,
currency = "usd",
payment_token = "payment-token-123"
})
if err then
return nil, err
end
액티비티 옵션
executor 빌더를 사용하여 타임아웃, 재시도 동작, 기타 실행 파라미터를 설정합니다:
local funcs = require("funcs")
local executor = funcs.new():with_options({
["activity.start_to_close_timeout"] = "30s",
["activity.schedule_to_close_timeout"] = "5m",
["activity.heartbeat_timeout"] = "10s",
["activity.retry_policy"] = {
maximum_attempts = 3,
initial_interval = 1000,
backoff_coefficient = 2.0,
maximum_interval = 60000,
}
})
local result, err = executor:call("app:charge_payment", input)
executor는 불변이며 재사용 가능합니다. 한 번 빌드하고 여러 호출에 사용하세요:
local reliable = funcs.new():with_options({
["activity.start_to_close_timeout"] = "60s",
["activity.retry_policy"] = {
maximum_attempts = 5,
initial_interval = 2000,
backoff_coefficient = 2.0,
maximum_interval = 120000,
}
})
local a, err = reliable:call("app:step_one", input)
if err then
return nil, err
end
local b, err = reliable:call("app:step_two", a)
if err then
return nil, err
end
옵션 레퍼런스
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
activity.start_to_close_timeout |
duration | 10m | 액티비티 실행 최대 시간 |
activity.schedule_to_close_timeout |
duration | - | 스케줄링부터 완료까지 최대 시간 |
activity.schedule_to_start_timeout |
duration | - | 액티비티 시작 전 최대 대기 시간 |
activity.heartbeat_timeout |
duration | - | heartbeat 사이 최대 시간 |
activity.id |
string | - | 커스텀 액티비티 실행 ID |
activity.task_queue |
string | - | 이 호출의 태스크 큐 오버라이드 |
activity.wait_for_cancellation |
boolean | false | 액티비티 취소 대기 |
activity.disable_eager_execution |
boolean | false | 즉시 실행 비활성화 |
activity.retry_policy |
table | - | 재시도 설정 (아래 참조) |
activity.name |
string | - | 레지스트리 ID와 다를 때 호출할 액티비티 타입 이름 |
activity.summary |
string | - | Temporal UI에 표시되는 사람이 읽을 수 있는 요약 |
activity.priority |
table | - | 태스크 우선순위: priority_key (number), fairness_key (string), fairness_weight (number) |
activity.versioning_intent |
string | - | compatible (빌드 ID 상속) 또는 default (할당 규칙 사용) |
duration 값은 문자열 ("5s", "10m", "1h") 또는 밀리초 숫자를 허용합니다.
새 코드에는 표준 activity.* 이름을 사용하세요. 기존 temporal.activity.* 별칭도 호환성을 위해 계속 지원됩니다.
local executor = funcs.new():with_options({
["activity.summary"] = "Charge the order payment",
["activity.priority"] = {
priority_key = 10,
fairness_key = "customer-123",
fairness_weight = 1.0,
},
["activity.name"] = "charge-payment",
["activity.versioning_intent"] = "use_assignment_rules",
})
재시도 정책
실패한 액티비티의 자동 재시도 동작을 설정합니다:
["activity.retry_policy"] = {
initial_interval = 1000, -- ms before first retry
backoff_coefficient = 2.0, -- multiplier for each retry
maximum_interval = 300000, -- max interval between retries (ms)
maximum_attempts = 10, -- max retry attempts (0 = unlimited)
non_retryable_error_types = { -- errors that skip retries
"Invalid",
"PermissionDenied"
}
}
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
initial_interval |
number | 1000 | 첫 번째 재시도 전 밀리초 |
backoff_coefficient |
number | 2.0 | 재시도마다 간격에 적용되는 승수 |
maximum_interval |
number | - | 재시도 간격 상한 (ms) |
maximum_attempts |
number | 0 | 최대 시도 횟수 (0 = 무제한) |
non_retryable_error_types |
array | - | 재시도를 건너뛰는 에러 종류 |
타임아웃 관계
|--- schedule_to_close_timeout --------------------------------|
|--- schedule_to_start_timeout ---|--- start_to_close_timeout -|
(waiting in queue) (executing)
start_to_close_timeout: 액티비티 자체가 실행될 수 있는 시간. 가장 많이 사용되는 타임아웃입니다.schedule_to_close_timeout: 액티비티가 스케줄링된 시점부터 완료까지의 총 시간으로, 큐 대기 시간과 재시도를 포함합니다.schedule_to_start_timeout: 워커가 액티비티를 선택하기 전 태스크 큐에서 대기할 수 있는 최대 시간입니다.heartbeat_timeout: 장기 실행 액티비티에서 heartbeat 보고 사이의 최대 시간입니다.
로컬 액티비티
액티비티는 local 필드를 허용합니다:
- name: validate_input
kind: function.lua
source: file://validate.lua
method: validate
modules:
- json
meta:
temporal:
activity:
worker: app:worker
local: true
현재 local: true는 파싱되지만 일반 액티비티와 동일하게 동작합니다: 표준 액티비티 경로를 통해 등록되고 실행됩니다. 아직 별도의 로컬 액티비티 실행 경로가 없으므로 지연 시간, 태스크 큐 동작, heartbeat에 영향을 주지 않습니다.
액티비티 명명
액티비티는 전체 엔트리 ID를 이름으로 등록됩니다:
namespace: app
entries:
- name: charge_payment
kind: function.lua
# ...
액티비티 이름: app:charge_payment
컨텍스트 전파
워크플로우 스폰 시 설정된 컨텍스트 값은 액티비티 내에서 사용할 수 있습니다:
-- Spawner sets context
local spawner = process.with_context({
user_id = "user-1",
tenant = "tenant-1",
})
local pid, err = spawner:spawn("app:order_workflow", "app:worker", order)
if err then
return nil, err
end
-- Activity reads context
local ctx = require("ctx")
local function process_order(input)
local user_id, user_err = ctx.get("user_id") -- "user-1"
if user_err then return nil, user_err end
local tenant, tenant_err = ctx.get("tenant") -- "tenant-1"
if tenant_err then return nil, tenant_err end
-- use context for authorization, logging, etc.
end
워크플로우 내에서 funcs.new():with_context()로 호출된 액티비티에도 컨텍스트가 전파됩니다:
-- Inside workflow
local executor = funcs.new():with_context({trace_id = "abc-123"})
local result, err = executor:call("app:charge_payment", input)
보안 컨텍스트
보안 컨텍스트 하에서 스케줄된 액티비티는 액티비티 ID를 대상(audience)으로 하는 서명된 wippy-security 헤더를 받습니다. 워커는 서명과 대상을 검증한 뒤, 액티비티 함수가 실행되기 전에 전파된 ctx 값과 보안 페이로드를 새 프레임에 병합합니다.
이 병합은 전부 아니면 전무이며 실패하면 액티비티에 치명적입니다: 액티비티는 코드가 실행되기 전에 에러를 반환하므로, 부분적인 컨텍스트나 검증되지 않은 액터로는 절대 실행되지 않습니다. 병합은 서명이나 대상이 검증되지 않을 때, 엔벨로프가 일관되지 않을 때(스코프 없는 액터, 또는 액터 없는 정책), 또는 엔벨로프에 지명된 정책이 로컬 보안 레지스트리에서 해석되지 않을 때 실패합니다 — 마지막이 운영상 흔한 원인입니다: 워커의 배포에 호출자가 가진 정책 엔트리가 없는 경우입니다.
워커는 자신이 참조하는 temporal.client 엔트리에서 서명 및 검증 키를 가져옵니다. 보안 컨텍스트 전파를 참조하세요.
에러 처리
표준 Lua 패턴으로 에러를 반환합니다:
local errors = require("errors")
-- Replace this mapping with the payment provider's documented error contract.
local function payment_error(status)
if status == 408 then
return errors.new({kind = errors.TIMEOUT, message = "payment provider timed out", retryable = true})
elseif status == 429 then
return errors.new({kind = errors.RATE_LIMITED, message = "payment provider rate limited the request", retryable = true})
elseif status >= 500 then
return errors.new({kind = errors.UNAVAILABLE, message = "payment provider is unavailable", retryable = true})
end
return errors.new({kind = errors.INVALID, message = "payment request was rejected", retryable = false})
end
local function charge(input)
if not input.amount or input.amount <= 0 then
return nil, errors.new({ kind = errors.INVALID, message = "amount must be positive" })
end
local response, err = http.post(url, options)
if err then
return nil, errors.wrap(err, "payment API failed")
end
if response:status() >= 400 then
return nil, errors.new({ kind = errors.INVALID, message = "payment declined" })
end
return json.decode(response.body)
end
에러 객체
워크플로우에 전파된 액티비티 에러는 구조화된 메타데이터를 포함합니다:
local result, err = funcs.call("app:charge_payment", input)
if err then
err:kind() -- error classification string
err:retryable() -- boolean, whether retry makes sense
err:message() -- human-readable error message
end
실패 모드
| 실패 | 에러 종류 | 재시도 가능 | 설명 |
|---|---|---|---|
| 애플리케이션 에러 | 액티비티가 반환한 것 | 반환된 에러에서 상속됨 | return nil, err로 액티비티 코드가 반환한 에러 |
| 런타임 크래시 | Internal |
false | 액티비티의 처리되지 않은 Lua 에러 |
| 누락된 액티비티 | NotFound |
false | 워커에 등록되지 않은 액티비티 |
| 타임아웃 | Timeout |
false | 설정된 타임아웃을 초과한 액티비티 |
| 보안 검증 | Internal |
true | 전파된 보안 헤더의 서명, 대상 또는 엔벨로프 검사 실패 |
| 보안 정책 누락 | Internal |
true | 보안 엔벨로프에 지명된 정책이 이 워커에서 해석되지 않음 |
두 보안 실패 모두 액티비티 함수가 실행되기 전 컨텍스트 병합 중에 발생합니다. 재시도 불가로 표시되지 않으므로 액티비티 재시도 정책이 계속 재시도하지만, 잘못된 서명이나 누락된 정책 엔트리는 시도마다 달라지지 않으므로 재시도는 도움이 되지 않습니다. 빠르게 실패시키려는 액티비티에는 maximum_attempts에 상한을 두고, 액티비티 로그 출력 없이 반복되는 Internal 실패는 액티비티 자체의 결함이 아니라 컨텍스트 병합 실패로 읽으세요.
local executor = funcs.new():with_options({
["activity.retry_policy"] = {maximum_attempts = 1}
})
local result, err = executor:call("app:missing_activity", input)
if err then
print(err:kind()) -- "NotFound"
print(err:retryable()) -- false
end
프로세스 액티비티
process.lua 엔트리도 장기 실행 작업을 위해 액티비티로 등록할 수 있습니다:
- name: long_task
kind: process.lua
source: file://long_task.lua
method: main
modules:
- http_client
meta:
temporal:
activity:
worker: app:worker