# 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에 남는다.