Files
clean-architecture-backend-…/docs/runbooks/outbox-publish-failed.md
T

4.4 KiB
Raw Blame History

title, category, error_codes, severity, owner, last_updated, status
title category error_codes severity owner last_updated status
Runbook — outbox publish 일시 실패 TRANSIENT_DEPENDENCY
OUTBOX_PUBLISH_FAILED
P2 oncall 2026-06-11 stub

Runbook: outbox publish 일시 실패 (runbook://outbox/publish-failed)

1. Trigger

이 runbook은 다음 alert에서 발동됩니다.

  • alert name: outbox_publish_failed_rate 또는 outbox_publisher_lag
  • alert payload 필수 field: error.code=OUTBOX_PUBLISH_FAILED, event_type, correlation_id, runbook_link
  • 임계 (metrics.yaml verbatim):
    • P2: outbox.publisher.published.total{outcome=FAILED} rate > 1% for 10m
    • P2: outbox.publisher.lag > 60s for 10m / P1: > 300s for 5m
    • P2: outbox.pending.size{status=PENDING} growing for 10m

2. First Response (5분 이내)

Step 1 — 확인

  1. ERROR log에서 OUTBOX_PUBLISH_FAILED 라인 확인: event_type, event_id, correlation_id, attempt_count 추출
  2. broker(기본 Kafka adapter) 상태 확인: APP_MESSAGING_KAFKA_ENABLED 값과 broker endpoint 가용성
    • Kafka disabled(default) 상태에서 outbox 이벤트가 append 되고 있으면 publish 경로가 AdapterDisabledException으로 전부 실패하는 구성 오류 — 이 경우 producer use case 쪽 활성화/구성을 먼저 의심
  3. outbox.pending.size status 분포 확인 (FAILED 누적 vs PENDING 누적)

Step 2 — 임시 격리

  • 일시 실패는 자동 backoff 재시도(30s × 2^(attempt-1) + jitter, max attempts 3)가 동작 — 즉시 수동 개입 불필요
  • broker 장기 다운이면 DEAD 전이 누적 전에 broker 회복을 우선 (max attempts 소진 시 runbook://outbox/dead-letter로 이관)
  • relay 자체를 멈춰야 하면 ca-skeleton.outbox.relay-enabled=false로 스케줄러 비활성 (이벤트는 outbox 테이블에 안전하게 보존됨 — 유실 없음)

3. Diagnosis

  • log query: {service="app"} | error.code="OUTBOX_PUBLISH_FAILED" | stats count by event_type
  • metric panel:
    • outbox.publisher.published.total{outcome} — FAILED 비율
    • outbox.publisher.lag{event_type} — 최고령 미발행 이벤트 age
    • outbox.pending.size{status} — 상태별 분포
  • DB 확인: SELECT status, count(*) FROM outbox_event GROUP BY status;
  • 가능한 원인:
    • broker outage/네트워크 → broker 측 회복 대기
    • Kafka adapter 미구성(enabled인데 brokers 누락은 기동 시 차단됨) / disabled 상태에서 producer 활성화
    • poison event (직렬화 불가/payload 계약 위반) → 재시도 무의미, attempts 소진 후 DEAD로 흘러감 (의도된 동작)
    • 동일 aggregate head 실패로 후행 이벤트가 FIFO 게이트에 차단되어 lag 증가 (strict per-aggregate FIFO — 설계 의도)

4. Mitigation

  • 단기: broker 회복 후 backoff 만료 시 자동 재발행 — outcome=PUBLISHED 회복 확인
  • IN_FLIGHT orphan(claim 후 crash)은 in-flight-timeout(기본 PT5M) 경과 후 자동 재claim — at-least-once이므로 중복 발행 가능, consumer dedupe(idempotencyKey)가 흡수
  • 장기: ca-skeleton.outbox.poll-interval/batch-size 조정, broker 가용성 SLA 점검, 빈발 event_type의 payload 계약 검토

5. Escalation

  • P1 lag(>300s 5m) 지속 + broker 회복 불가면 broker/infra 팀에 page
  • DEAD 전이가 발생하기 시작하면 runbook://outbox/dead-letter 절차로 이관

6. Recovery / Verification

  • 회복 확인 metric: outcome=FAILED rate < 1% 10분 지속, outbox.publisher.lag < 60s, outbox.pending.size{status=FAILED} 감소 추세
  • post-incident: 실패 구간의 DEAD row 유무 확인, consumer 측 중복 처리량 확인(dedupe 동작 검증), backoff/attempts 상수 재평가
  • error-codes.yaml rows: OUTBOX_PUBLISH_FAILED (TRANSIENT_DEPENDENCY, retryable=true, retry_after 30s)
  • metrics.yaml: outbox.publisher.published.total, outbox.publisher.lag, outbox.pending.size
  • 코드: application-core PublishPendingOutboxEventsUseCase(상태머신), adapter-persistence outbox/OutboxEventJpaRepository(SKIP LOCKED claim + FIFO 게이트), adapter-outbound messaging/outbox/KafkaOutboxMessagePublishAdapter(fail-closed)
  • 관련 runbook: outbox-dead-letter
  • 관련 branch: feature-domain-event-outbox-contract, feature-background-job-async-contract (retry/DLQ vocabulary SSOT)

Stub 상태 안내: 이 runbook은 skeleton 단계의 stub. 실제 broker 채택·alert 라우팅·대시보드 링크 확정 시 보강 필요.