Files
tech-log-backend/docs/runbooks/outbox-dead-letter.md
T

4.1 KiB

title, category, error_codes, severity, owner, last_updated, status
title category error_codes severity owner last_updated status
Runbook — outbox dead letter INTERNAL
OUTBOX_DEAD_LETTER
P1 oncall 2026-06-11 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 = '<id>' AND status = 'DEAD'; (consumer는 at-least-once + idempotencyKey dedupe 의무가 있으므로 중복 발행은 안전)
  • skip (영구 폐기): 이벤트가 더 이상 유효하지 않으면 PUBLISHED로 마킹해 FIFO 차단을 해제 — UPDATE outbox_event SET status = 'PUBLISHED' WHERE event_id = '<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 원인 분류 기록, 같은 원인의 재발 방지 테스트 추가
  • 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 토픽·승인 체계 확정 시 보강 필요.