Files
clean-architecture-backend-…/docs/messaging/retry-dlq-redrive.md
T
DongHyeonka d646c2f12f feat(messaging): 브로커 중립 메시징 플랫폼 24개 leaf 추가
messaging-superpowers-package 설계서/계획서 기반 구현.
registry를 19 → 43 leaf로 확장하고 src/messaging 아래 24개 leaf를 등록.

- core-api: M1 publish/consume + M2 batch·delayed·pause-resume
- policy/transport-spi: 재시도 결정, DLQ orchestration, admission control, lifecycle
- kafka·rabbit(Stable): contiguous commit, confirm/return 상관, 배치, 보안 설정
- pulsar·nats(Experimental): 기본 비활성, live 인증 없음을 코드로 기록
- outbox/inbox/claim-check: 트랜잭션 결합, lease, 무결성 검증
- admin: plan → approve → execute를 타입으로 강제
- 문서 9종, infra compose 7종, JMH 벤치마크 3종

검증: 아키텍처 게이트 3종 통과, 24개 leaf 전부 check 통과,
messaging 테스트 604개 통과/0 실패.

미완: 계획서가 요구한 실 브로커 IT 40개 중 7개만 작성.
Rabbit 13 / Outbox 6 / Inbox 4 / NATS·Pulsar·Share 5 / testkit 2 /
starter·admin 3, 그리고 TLS·ACL 2개가 남음.
2026-08-14 14:55:38 +09:00

104 lines
4.3 KiB
Markdown

# Retry · DLQ · Redrive
## 자동 retry는 opt-in이다
일반 destination의 기본값은 **retry 없음**이다. 순서를 깨거나, 멱등하지 않은 side effect를
증폭시키거나, 이미 throttle된 downstream을 더 때리는 retry는 보이는 실패보다 나쁘다.
## 결정 순서
`DefaultRetryDecisionEngine`은 아래 순서를 위에서 아래로 평가한다.
```text
1. non-retryable category → parking(DeadLetter) 또는 Reject
2. attempt >= maxAttempts → DeadLetter
3. PRESERVE + ordered + orderedStream capability → PauseAndRetry
4. mode=PAUSE_PARTITION → PauseAndRetry
5. mode=RETRY_DESTINATION + ALLOW_REORDER → PublishToRetryDestination
6. mode=INLINE|BLOCKING → RetryInline
7. mode=BROKER_DELAYED + delayedDelivery capability → PublishToRetryDestination
8. 그 외 → DeadLetter
```
**retryability를 attempt 예산보다 먼저** 검사한다. deserialization 실패는 payload가 바뀌지 않으므로
재시도가 3번 더 실패할 뿐이다. 첫 delivery에서 바로 park한다.
**순서 보존 전략을 재발행 전략보다 먼저** 검사한다. 둘 다 설정되어 있어도 ordered destination이
reorder 경로로 흘러내리지 않는다.
## 기본 non-retryable
`DESERIALIZATION`, `AUTHENTICATION`, `AUTHORIZATION`, `CONFIGURATION`은 자동 retry하지 않는다.
매 redelivery마다 동일하게 실패하므로 부하만 늘어난다.
destination profile의 `retryableCategories`로 명시적으로 뒤집을 수는 있다.
## Backoff
`min(maxDelay, initialDelay * multiplier^(attempt-1))`, 이후 full jitter.
full jitter는 `[0, delay]` 균등 분포다. jitter가 없으면 같은 초에 실패한 모든 consumer가
같은 초에 재시도하고, downstream의 회복이 재시도 폭풍으로 즉시 무효화된다.
## Kafka: pause-and-seek vs retry topic
| 전략 | 순서 | 언제 |
|---|---|---|
| `PAUSE_PARTITION` | 유지 | ordered destination |
| `RETRY_DESTINATION` | 깨짐 | work queue, `ALLOW_REORDER` 명시 |
pause-and-seek는 메시지가 로그의 자기 자리를 떠나지 않는다. partition을 멈추고, 기다리고,
같은 offset으로 seek해 재전달한다. 뒤의 메시지도 함께 기다리며 이것이 의도된 동작이다.
## RabbitMQ: delayed retry queue
core broker에 per-message delay가 없으므로 **TTL + DLX**로 구현한다.
retry queue의 `x-message-ttl`이 만료되면 `x-dead-letter-exchange`를 통해 work queue로 되돌아간다.
주의: TTL 만료는 큐 **head**에서 평가된다. 하나의 retry queue에 서로 다른 delay를 섞으면
독립적으로 만료되지 않는다.
`basic.nack(requeue=true)`는 사용하지 않는다. delay 없이 큐 head로 되돌리므로 hot loop가 된다.
## DLQ: publish 확인 후 settlement
이것이 dead lettering이 데이터 손실이 되지 않게 하는 **유일한** 불변식이다.
```text
DLQ envelope 생성 (원래 messageId 유지)
→ DLQ publish
→ CONFIRMED 이면 source settlement
→ REJECTED / AMBIGUOUS 이면 source를 settlement하지 않음
```
source를 먼저 ACK하면, DLQ publish가 실패했을 때 메시지의 사본이 **어디에도 남지 않는다**.
broker는 이미 해제했고 DLQ는 받지 못했다.
AMBIGUOUS DLQ publish는 중복을 만든다. 이것이 의도된 trade다. DLQ는 사람이 읽는 곳이고
중복은 알아볼 수 있지만, 손실은 복구할 수 없다.
## DLQ envelope 내용
reserved header에만 기록한다. payload에 넣지 않는다.
```text
msg.failure-category, msg.failure-code, msg.origin-destination,
msg.retry-attempt, msg.first-failure-at, msg.last-failure-at
```
stack trace, exception message, secret header, 실제 key는 **넣지 않는다**.
DLQ는 원본 topic보다 오래 보관되고 더 많은 사람이 읽는다.
## Redrive
M4 Admin 전용이다. `DestructiveOperationGuard`를 통과해야 한다.
- admin credential 필요 (application runtime은 보유하지 않는다)
- 유효기간 내 `AdminApproval` 필요
- dry-run은 항상 허용 (계획이 공짜여야 사람이 계획한다)
- batch 상한 100건
- source == target 금지
- `redriveId``messageId`와 별개다. 재구동 루프를 식별하기 위해서다.
redrive도 **publish → settlement** 순서다. republish가 confirm되지 않은 메시지는
DLQ에 남는다.