Spring Kafka · open · GH-4272
PR #4674 — GH-4272: Bound recovery failures per record
ConsumerRecordRecoverer가 같은 레코드에 대해 계속 throw하면 (예: DLT 브로커 장애) DefaultErrorHandler는 그 offset으로 seek해 무한 재전달하고, 컨슈머는 영원히 앞으로 나가지 못합니다. resetStateOnRecoveryFailure와 seekAfterError는 재전달 방식만 바꿀 뿐 끊어 주지 않습니다. 본 PR은 maxRecoveryFailures 옵션을 추가해, recovery가 N번 실패한 레코드를 ERROR 로그와 함께 recovered 취급(seek 생략, ack mode대로 offset 커밋)하도록 bound합니다. 기본값은 무제한으로 기존 동작 그대로.
맥락 — GH-4272
backoff가 소진되면 FailedRecordTracker.recovered()가 recoverer를 호출하는데, recoverer가 throw하면 예외가 그대로 올라가고 SeekUtils.doSeeks가 그 레코드를 seek 대상에 포함시킵니다. 다음 poll에서 같은 offset이 다시 들어오고, 다시 recoverer가 실패하고, 다시 seek. 이슈 리포터가 시도한 setResetStateOnExceptionChange(false), setSeekAfterError(false) 어느 쪽도 이 루프를 끊지 못합니다 — 전자는 backoff를 다시 도는지 여부, 후자는 seek 대신 이전 poll 레코드를 재사용하는지 여부만 결정하기 때문입니다. 재시도 없이 FixedBackOff(0, 0)으로 바로 recoverer를 부르는 noRetries 경로도 동일하게 무한입니다.
수정
FailedRecordProcessor.setMaxRecoveryFailures(int)추가.DefaultErrorHandler와DefaultAfterRollbackProcessor가 둘 다 상속받아 그대로 노출. 인자는 0보다 커야 하며 기본값은 무제한.FailedRecordTracker에서 recovery 실패 횟수를 스레드·파티션별로 세고, 한도에 도달하면 마지막 recovery 예외와 함께 ERROR 로그를 남긴 뒤recovered()가true를 반환. 이후는 정상 recovered와 동일 — seek 생략,ackAfterHandle에 따라 offset 커밋.RetryListener.recoveryFailed는 마지막 실패를 포함해 매번 그대로 호출되므로 기존 관측 지점은 유지.- 카운터는 recovery 성공 시, 같은 파티션에서 다른 offset이 실패할 때, 그리고
clearThreadState/clearThreadStateFor에서 초기화.
설계 선택 — 카운터를 retry 상태와 분리
처음 떠오르는 자리는 기존 FailedRecord(offset, backoff execution, delivery attempts)에 필드를 하나 얹는 것이지만, 그 상태는 resetStateOnRecoveryFailure=true(기본값)일 때 recovery 실패 직후 삭제되고, noRetries 경로는 애초에 FailedRecord를 만들지 않습니다. 둘 다 이 기능이 반드시 커버해야 하는 경로라서, 카운터는 같은 Map<Thread, Map<TopicPartition, …>> 모양의 별도 구조로 두었습니다. 기본값(무제한)에서는 이 맵에 아무것도 쓰지 않아 기존 경로의 동작과 상태는 그대로입니다.
회귀 테스트
FailedRecordTrackerTests6건 — 기본값 무제한,noRetries경로에서 N회 후 skip, backoff 리셋을 거쳐도 카운트 유지,resetStateOnRecoveryFailure=false, 레코드·파티션별 독립 카운트와 성공·clear 시 초기화, 인자 검사.DefaultErrorHandlerRecordTests2건 —handleRemaining에서 첫 실패는RecordInRetryException+ seek, 두 번째는 seek 없이 정상 반환하는지 mock consumer로 검증.seekAfterError=false의handleOne경로도 동일하게 확인.- 관련 에러 핸들러 테스트 클래스 9개(
SeekToCurrentRecovererTests,DefaultAfterRollbackProcessorTests,FallbackBatchErrorHandlerTests등) 전부 통과.
문서
annotation-error-handling.adoc의 recovery 실패 안내 3곳(DefaultErrorHandler, DefaultAfterRollbackProcessor, DeadLetterPublishingRecoverer)에 4.2 옵션 한 단락씩, whats-new.adoc에 4.2 섹션 추가.