리플레이 모델: AWS Lambda Durable Functions가 실제로 작동하는 방식
Source: Dev.to
핵심 원칙
핸들러 함수는 매 호출 시 처음부터 다시 실행되지만, 완료된 작업은 체크포인트에서 캐시된 결과를 반환하므로 다시 실행되지 않습니다.
async function processOrder(event: any, ctx: DurableContext) {
const order = await ctx.step('create-order', async () => {
console.log('Creating order...');
return { orderId: '123', total: 50 };
});
const payment = await ctx.step('process-payment', async () => {
console.log('Processing payment...');
return { transactionId: 'txn-456', status: 'success' };
});
await ctx.wait({ seconds: 300 }); // Wait 5 minutes
const notification = await ctx.step('send-notification', async () => {
console.log('Sending notification...');
return { sent: true };
});
return { order, payment, notification };
}
호출 흐름
Invocation 1 (t = 0 s)
Creating order...
Processing payment...
[Checkpoint: create-order completed]
[Checkpoint: process-payment completed]
[Function terminates – waiting 5 minutes]
Invocation 2 (t = 300 s, after wait completes)
[REPLAY MODE: Skipping create-order – returning cached result]
[REPLAY MODE: Skipping process-payment – returning cached result]
[EXECUTION MODE: Running send-notification]
Sending notification...
[Checkpoint: send-notification completed]
[Function completes]
함수는 매번 처음부터 시작하지만, 이전에 완료된 단계는 건너뛰므로 로그는 한 번만 나타납니다.
실행 모드: 비밀 소스
SDK는 두 가지 자동 모드로 동작합니다:
- ExecutionMode – 작업을 처음 실행할 때; 결과가 체크포인트에 저장되고, 로그가 출력되며, 부수 효과가 발생합니다.
- ReplayMode – 이전에 완료된 작업을 재생할 때; 캐시된 결과가 즉시 반환되고, 로그는 억제되며, 부수 효과가 발생하지 않습니다.
SDK는 아직 완료되지 않은 작업에 도달하면 ReplayMode에서 ExecutionMode로 전환합니다.
체크포인트 작동 방식
각 작업은 메타데이터와 결과를 저장하는 체크포인트를 생성합니다:
{
"operationId": "2",
"operationType": "STEP",
"operationName": "process-payment",
"status": "SUCCEEDED",
"result": {
"transactionId": "txn-456",
"status": "success"
}
}
함수가 재시작될 때 SDK는 모든 체크포인트를 로드하고, 작업 ID별로 인덱싱한 뒤, 완료된 작업에 대해 캐시된 결과를 반환하고 새로운 작업은 정상적으로 실행합니다.
결정론성 요구 사항
Replay를 사용하려면 코드가 결정론적이어야 합니다 – 매 호출마다 동일한 순서로 동일한 작업 흐름이 발생해야 합니다.
결정론성을 깨뜨리는 경우
// ❌ 랜덤 제어 흐름
if (Math.random() > 0.5) {
await ctx.step('optional-step', async () => doSomething());
}
// ❌ 시간 기반 분기
const isWeekend = new Date().getDay() >= 5;
if (isWeekend) {
await ctx.step('weekend-task', async () => doWeekendWork());
}
// ❌ 외부 가변 상태
let counter = 0;
await ctx.step('step1', async () => {
counter++; // Replay 중에는 증가하지 않음!
return counter;
});
이러한 패턴은 실행 간 작업 순서가 일치하지 않게 만들어 Replay 일관성 위반을 초래합니다.
결정론적인 코드 작성 방법
비결정론적 값은 모두 단계 내부에서 캡처합니다:
// ✅ 단계 안에서 랜덤 값 캡처
const randomId = await ctx.step('generate-id', async () => {
return crypto.randomUUID(); // 한 번만 실행되고 Replay에서 캐시됨
});
// ✅ 단계 안에서 타임스탬프 캡처
const timestamp = await ctx.step('get-timestamp', async () => {
return Date.now(); // 모든 Replay에서 동일한 타임스탬프
});
// ✅ 이벤트 데이터를 이용한 제어 흐름 (결정론적)
if (event.shouldProcess) {
await ctx.step('process', async () => doWork());
}
// ✅ 시간 기반 결정도 단계 안에서 캡처
const isWeekend = await ctx.step('check-day', async () => {
return new Date().getDay() >= 5;
});
if (isWeekend) {
await ctx.step('weekend-task', async () => doWeekendWork());
}
Replay 일관성 검증
SDK는 각 호출이 동일한 작업 순서를 따르는지 검증합니다:
- 작업 유형 (STEP, WAIT, INVOKE)
- 작업 이름 (사용자 지정 식별자)
- 작업 위치 (연속 순서)
예시 검증 오류:
Replay consistency violation: Expected operation 'process-payment'
of type STEP at position 2, but found operation 'send-email' of type STEP
이러한 조기 감지는 비결정론적 버그를 빠르게 찾아낼 수 있게 도와줍니다.
전체 예시: Replay가 적용된 주문 처리
async function processOrder(event: any, ctx: DurableContext) {
ctx.logger.info('Order processing started', { orderId: event.orderId });
// Step 1: 재고 검증
const inventory = await ctx.step('check-inventory', async () => {
ctx.logger.info('Checking inventory');
const response = await fetch(`https://api.inventory.com/check`, {
method: 'POST',
body: JSON.stringify({ items: event.items })
});
return response.json();
});
if (!inventory.available) {
ctx.logger.warn('Out of stock', { missing: inventory.missing });
return { status: 'out-of-stock' };
}
// Step 2: 결제 처리
const payment = await ctx.step('process-payment', async () => {
ctx.logger.info('Processing payment', { amount: inventory.total });
const response = await fetch(`https://api.payments.com/charge`, {
method: 'POST',
body: JSON.stringify({
customerId: event.customerId,
amount: inventory.total
})
});
return response.json();
});
// Step 3: 창고 확인 대기 (5분 제한)
ctx.logger.info('Waiting for warehouse confirmation');
const confirmation = await ctx.waitForCallback(
'warehouse-confirm',
async (callbackId) => {
// 콜백 ID를 창고 시스템에 전송
await fetch(`https://api.warehouse.com/notify`, {
method: 'POST',
body: JSON.stringify({ orderId: event.orderId, callbackId })
});
},
{ timeout: { seconds: 300 } }
);
// Step 4: 알림 전송
const notification = await ctx.step('send-notification', async () => {
ctx.logger.info('Sending notification');
// 예: 이메일 서비스 호출
await fetch(`https://api.email.com/send`, {
method: 'POST',
body: JSON.stringify({
to: event.customerEmail,
subject: 'Your order is confirmed',
body: `Order ${event.orderId} is confirmed.`
})
});
return { sent: true };
});
return { inventory, payment, confirmation, notification };
}
이 워크플로에서는 각 단계가 체크포인트에 저장됩니다. 함수가 일시 중지(예: 창고 콜백 대기)되면 이후 호출은 이전 단계들을 즉시 재생하여 부수 효과가 정확히 한 번만 실행되도록 보장합니다.