Contribution · Spring Kafka · Kotlin Coroutines

Spring Kafka — suspend listener async retry 버그 수정

suspend @KafkaListener + DefaultErrorHandler 조합에서 비동기 retry가 무한 재전송되는 회귀를 추적해 원인이 되는 큐 중복을 제거했습니다. 버그를 재현하는 회귀 테스트와 함께 머지됐습니다.

문제 — GH-4465

Spring Kafka 4.1 시리즈 직전에 머지된 #4254의 변경으로 비동기 리스너의 실패가 seekAfterHandling=trueCommonErrorHandler(예: DefaultErrorHandler)로 전달되도록 바뀌었습니다. 정상적으로는 seek 후 다음 poll에서 재배달되어야 하는데, ListenerConsumer#handleAsyncFailureRecordInRetryException을 받았을 때 failedRecords 큐에 다시 넣고 있었습니다. 결과적으로 같은 레코드가 매 루프마다 두 번 처리(큐에서 한 번, seek-induced re-delivery로 한 번)되고, 백오프가 소진되어 tracker entry가 recovery되더라도 큐에 남은 중복이 새 retry 사이클을 시작해 무한 재배달이 발생했습니다.

수정

RecordInRetryException catch에서 failedRecords.addLast(failedRecord) re-queue를 제거했습니다. 에러 핸들러가 이미 컨슈머를 실패 offset으로 seek 했고, FailedRecordTracker 엔트리는 (topic, partition, offset)으로 키잉돼 루프 반복에 걸쳐 살아남기 때문에 attempt 카운터는 자연스러운 재배달 위에서 그대로 누적됩니다.

회귀 테스트는 EnableKafkaKotlinCoroutinesTests에 추가했습니다. always-failing suspend 리스너에 DefaultErrorHandler(FixedBackOff(100L, 2L))를 적용하고 리스너 호출이 정확히 3회만 일어나는지 확인. 픽스 없이는 expected: 3 but was: 6으로 실패하고, 픽스 후에는 통과.

후속 — GH-4504

머지 후, 같은 reporter가 "두 개 이상의 레코드가 같은 파티션에서 실패하면 r1이 silent 유실되고 r2만 retry/recover된다"는 후속 이슈를 신고했습니다. 같은 파티션의 async failure들을 offset 순서대로 정렬해 에러 핸들러에 넘기는 fix를 PR로 제출했습니다 (PR #4505).

PR 목록

관련 자료