--- title: Runbook — outbox dead letter category: INTERNAL error_codes: [OUTBOX_DEAD_LETTER] severity: P1 owner: oncall last_updated: 2026-06-11 status: stub --- # Runbook: outbox dead letter (`runbook://outbox/dead-letter`) ## 1. Trigger 이 runbook은 다음 alert에서 발동됩니다. - alert name: `outbox_dead_letter` - alert payload 필수 field: `error.code=OUTBOX_DEAD_LETTER`, `event_type`, `event_id`, `correlation_id`, `runbook_link` - 임계: `outbox.publisher.published.total{outcome=DEAD}` > 0 (DEAD 전이는 자동 회복이 없으므로 단건도 점검 대상) ## 2. First Response (5분 이내) ### Step 1 — 확인 1. ERROR log에서 `OUTBOX_DEAD_LETTER` 라인 확인: `event_id`, `event_type`, `correlation_id`, 마지막 실패 원인 예외 추출 2. DB에서 DEAD row 확인: `SELECT * FROM outbox_event WHERE status = 'DEAD' ORDER BY occurred_at;` 3. **차단 영향 파악 (중요)**: strict per-aggregate FIFO 정책상 DEAD row는 같은 `aggregate_id`의 후행 이벤트를 계속 차단함 — `SELECT count(*) FROM outbox_event b WHERE b.status <> 'PUBLISHED' AND EXISTS (SELECT 1 FROM outbox_event d WHERE d.status='DEAD' AND d.aggregate_id=b.aggregate_id AND d.occurred_at < b.occurred_at);` ### Step 2 — 임시 격리 - DEAD는 max attempts(3) 소진의 최종 상태 — 자동 재시도 없음, 수동 개입 필수 - 차단된 aggregate가 비즈니스 크리티컬하면 아래 §4의 수동 처분(재발행 또는 skip)을 우선 수행 ## 3. Diagnosis - log query: `{service="app"} | error.code="OUTBOX_DEAD_LETTER" | stats count by event_type` - 마지막 실패 원인 분류: - poison event (payload 직렬화/계약 위반) → payload 자체 결함, 재발행해도 실패 — 수정 후 재발행 또는 skip - broker 장기 outage 중 attempts 소진 → broker 회복 후 재발행으로 해결 가능 - 구성 오류 (Kafka disabled 상태에서 producer 활성) → 구성 수정 후 재발행 - 가능한 원인 우선순위: 구성 오류 > broker outage > poison payload ## 4. Mitigation (수동 처분 — 둘 중 하나) - **재발행 (기본)**: 원인 해소 후 해당 row를 다시 claim 가능 상태로 되돌림 — `UPDATE outbox_event SET status = 'PENDING', attempt_count = 0, next_attempt_at = now() WHERE event_id = '' AND status = 'DEAD';` (consumer는 at-least-once + idempotencyKey dedupe 의무가 있으므로 중복 발행은 안전) - **skip (영구 폐기)**: 이벤트가 더 이상 유효하지 않으면 PUBLISHED로 마킹해 FIFO 차단을 해제 — `UPDATE outbox_event SET status = 'PUBLISHED' WHERE event_id = '' AND status = 'DEAD';` ⚠️ skip은 다운스트림에 영구 이벤트 갭을 만든다 — 비즈니스 오너 승인 후에만 수행하고 incident 기록에 남김 - 장기: poison event 재발 방지(payload 계약 테스트 보강), DEAD 빈발 event_type의 producer 검증 강화 ## 5. Escalation - 처분 판단(재발행 vs skip)이 불가하면 해당 이벤트의 비즈니스 오너에게 escalate - DEAD 누적이 특정 event_type에 집중되면 producer 코드 오너에게 page ## 6. Recovery / Verification - 회복 확인: `SELECT count(*) FROM outbox_event WHERE status='DEAD';` = 0, 차단됐던 aggregate의 후행 이벤트가 PUBLISHED로 전이 - `outbox.publisher.lag` 정상화(< 60s), 재발행분의 consumer dedupe 동작 확인 - post-incident: DEAD 원인 분류 기록, 같은 원인의 재발 방지 테스트 추가 ## 7. Related - error-codes.yaml rows: `OUTBOX_DEAD_LETTER` (INTERNAL, retryable=false) - metrics.yaml: `outbox.publisher.published.total{outcome=DEAD}`, `outbox.pending.size{status=DEAD}`, `outbox.publisher.lag` - 코드: `application-core` `PublishPendingOutboxEventsUseCase`(FAILED→DEAD 전이), `adapter-persistence` `outbox/OutboxEventJpaRepository`(FIFO 게이트 — DEAD가 후행 차단) - 관련 runbook: [[outbox-publish-failed]] - 관련 branch: [[feature-domain-event-outbox-contract]], [[feature-background-job-async-contract]] (max attempts/DLQ vocabulary SSOT) --- > **Stub 상태 안내**: 이 runbook은 skeleton 단계의 stub. 실제 broker·DLQ 토픽·승인 체계 확정 시 보강 필요.