[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
---
|
||||
id: backend-job-scheduling-temporal
|
||||
title: Temporal — Workflow / Activity / Durable
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, temporal, workflow, durable, vibe-coding]
|
||||
tech_stack: { language: "TS / Temporal", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [Temporal, Cadence, durable execution, workflow, activity, signal, query]
|
||||
---
|
||||
|
||||
# Temporal
|
||||
|
||||
> 긴 / 복잡 / 신뢰 critical workflow. **코드가 곧 state machine, 자동 재시도 / 영속 / 시간**. Saga 의 정답. AWS Step Functions / Airflow 의 코드 버전.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Workflow: 결정 로직 (deterministic).
|
||||
- Activity: 외부 부수효과 (network, DB).
|
||||
- Signal: 외부에서 workflow 에 이벤트.
|
||||
- Query: workflow 상태 read.
|
||||
- Durable: 모든 step history 영속 — server crash 후 재개.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Activity (idempotent 권장)
|
||||
```ts
|
||||
// activities/payment.ts
|
||||
export async function chargeCard(orderId: string): Promise<string> {
|
||||
const id = await stripe.paymentIntents.create({
|
||||
amount: 1000, currency: 'usd',
|
||||
}, { idempotencyKey: orderId });
|
||||
return id.id;
|
||||
}
|
||||
|
||||
export async function refund(paymentId: string): Promise<void> {
|
||||
await stripe.refunds.create({ payment_intent: paymentId });
|
||||
}
|
||||
|
||||
export async function reserveInventory(orderId: string): Promise<string> {
|
||||
return await db.inventory.reserve(orderId);
|
||||
}
|
||||
|
||||
export async function releaseInventory(reservationId: string): Promise<void> {
|
||||
await db.inventory.release(reservationId);
|
||||
}
|
||||
```
|
||||
|
||||
### Workflow (deterministic)
|
||||
```ts
|
||||
// workflows/order.ts
|
||||
import { proxyActivities, log } from '@temporalio/workflow';
|
||||
import type * as activities from '../activities';
|
||||
|
||||
const { chargeCard, refund, reserveInventory, releaseInventory, ship }
|
||||
= proxyActivities<typeof activities>({
|
||||
startToCloseTimeout: '1 minute',
|
||||
retry: { maximumAttempts: 3 },
|
||||
});
|
||||
|
||||
export async function orderWorkflow(orderId: string): Promise<void> {
|
||||
let paymentId: string | undefined;
|
||||
let reservationId: string | undefined;
|
||||
|
||||
try {
|
||||
paymentId = await chargeCard(orderId);
|
||||
reservationId = await reserveInventory(orderId);
|
||||
await ship(orderId);
|
||||
} catch (e) {
|
||||
log.error('order failed', { orderId, e });
|
||||
if (reservationId) await releaseInventory(reservationId);
|
||||
if (paymentId) await refund(paymentId);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Worker (run)
|
||||
```ts
|
||||
// worker.ts
|
||||
import { Worker } from '@temporalio/worker';
|
||||
|
||||
const worker = await Worker.create({
|
||||
workflowsPath: require.resolve('./workflows'),
|
||||
activities: require('./activities'),
|
||||
taskQueue: 'orders',
|
||||
});
|
||||
|
||||
await worker.run();
|
||||
```
|
||||
|
||||
### Client (시작)
|
||||
```ts
|
||||
import { Client } from '@temporalio/client';
|
||||
|
||||
const client = new Client();
|
||||
const handle = await client.workflow.start(orderWorkflow, {
|
||||
args: ['order-42'],
|
||||
taskQueue: 'orders',
|
||||
workflowId: `order-42`, // 멱등 — 같은 ID = 같은 workflow
|
||||
workflowExecutionTimeout: '1 hour',
|
||||
});
|
||||
|
||||
// 결과 기다림
|
||||
await handle.result();
|
||||
```
|
||||
|
||||
### Signal (외부 이벤트)
|
||||
```ts
|
||||
import { defineSignal, setHandler, condition } from '@temporalio/workflow';
|
||||
|
||||
export const cancelSignal = defineSignal('cancel');
|
||||
|
||||
export async function orderWorkflow(orderId: string) {
|
||||
let cancelled = false;
|
||||
setHandler(cancelSignal, () => { cancelled = true; });
|
||||
|
||||
// payment 후 사용자가 cancel 가능
|
||||
paymentId = await chargeCard(orderId);
|
||||
|
||||
// 30초 동안 cancel 가능
|
||||
await condition(() => cancelled, '30 seconds');
|
||||
if (cancelled) {
|
||||
await refund(paymentId);
|
||||
return;
|
||||
}
|
||||
|
||||
await ship(orderId);
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// 외부에서 signal 보내기
|
||||
const handle = client.workflow.getHandle(`order-42`);
|
||||
await handle.signal(cancelSignal);
|
||||
```
|
||||
|
||||
### Query (외부에서 read)
|
||||
```ts
|
||||
import { defineQuery, setHandler } from '@temporalio/workflow';
|
||||
|
||||
export const statusQuery = defineQuery<string>('status');
|
||||
|
||||
export async function orderWorkflow(orderId: string) {
|
||||
let status = 'init';
|
||||
setHandler(statusQuery, () => status);
|
||||
|
||||
status = 'charging';
|
||||
paymentId = await chargeCard(orderId);
|
||||
status = 'shipping';
|
||||
await ship(orderId);
|
||||
status = 'done';
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
const handle = client.workflow.getHandle(`order-42`);
|
||||
const s = await handle.query(statusQuery);
|
||||
```
|
||||
|
||||
### Sleep / timer (durable)
|
||||
```ts
|
||||
import { sleep } from '@temporalio/workflow';
|
||||
|
||||
await sleep('7 days'); // 7일 후 재개. server crash 도 OK.
|
||||
await sendReminderEmail(userId);
|
||||
```
|
||||
|
||||
### Cron workflow
|
||||
```ts
|
||||
await client.workflow.start(reportWorkflow, {
|
||||
cronSchedule: '0 9 * * 1', // 월요일 9시
|
||||
workflowId: 'weekly-report',
|
||||
taskQueue: 'reports',
|
||||
});
|
||||
```
|
||||
|
||||
### Versioning (schema 변경)
|
||||
```ts
|
||||
import { patched } from '@temporalio/workflow';
|
||||
|
||||
export async function workflow() {
|
||||
if (patched('new-step')) {
|
||||
await newStep(); // 새 logic
|
||||
}
|
||||
// 옛 in-flight workflow 도 안전
|
||||
}
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| Saga / 멀티 step | Temporal |
|
||||
| 시간 의존 (3일 후 알림) | Temporal sleep |
|
||||
| 큐 / 단순 job | BullMQ / SQS |
|
||||
| Cron 만 | k8s CronJob |
|
||||
| 큰 throughput stream | Kafka |
|
||||
| AWS only | Step Functions |
|
||||
| Self-host vs Cloud | Temporal Cloud / 자체 |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Workflow 안 외부 호출 (fetch / DB)**: deterministic 깨짐. activity 로.
|
||||
- **Random / Date.now() workflow 안**: deterministic 깨짐 — workflowInfo, sleep, random 사용.
|
||||
- **Activity 가 무한 retry**: backoff + max attempts.
|
||||
- **Worker 영원 한 task queue**: scaling 어려움.
|
||||
- **Signal 무한 받음**: 메시지 누적. condition 으로 처리.
|
||||
- **Workflow 너무 큼 (월 단위)**: continueAsNew 로 reset.
|
||||
- **Activity timeout 없음**: hang.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Workflow = 순수 결정 / Activity = 부수효과.
|
||||
- workflowId = idempotency key.
|
||||
- Signal + Query 로 외부 통신.
|
||||
- Sleep 이 마법 — 7일 후도 OK.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Backend_Saga_Patterns]]
|
||||
- [[Backend_Cron_Patterns]]
|
||||
- [[Backend_Idempotency_Keys]]
|
||||
Reference in New Issue
Block a user