From 1786eb2901d6ea8c9eec286f1aaab72020e0ab2d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:22:52 +0200 Subject: [PATCH] Introduce RetryInterruptedException to address off-by-one error Prior to this commit, a RetryException thrown for an InterruptedException returned the wrong value from getRetryCount(). Specifically, the count was one more than it should have been, since the suppressed exception list contains the initial exception as well as all retry attempt exceptions. To address that, this commit introduces an internal RetryInterruptedException which accounts for this off-by-one error. Closes gh-35434 --- .../core/retry/RetryTemplate.java | 18 +++++++++++++++++- .../core/retry/RetryTemplateTests.java | 4 +--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java index 9d374d5e467..f7d9aad5cb5 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java @@ -164,7 +164,7 @@ public class RetryTemplate implements RetryOperations { } catch (InterruptedException interruptedException) { Thread.currentThread().interrupt(); - RetryException retryException = new RetryException( + RetryException retryException = new RetryInterruptedException( "Unable to back off for retryable operation '%s'".formatted(retryableName), interruptedException); exceptions.forEach(retryException::addSuppressed); @@ -200,4 +200,20 @@ public class RetryTemplate implements RetryOperations { } } + private static class RetryInterruptedException extends RetryException { + + private static final long serialVersionUID = 1L; + + + RetryInterruptedException(String message, InterruptedException cause) { + super(message, cause); + } + + @Override + public int getRetryCount() { + return (getSuppressed().length - 1); + } + + } + } diff --git a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java index 8e8bbaa2fcf..42a92dea3ab 100644 --- a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java +++ b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java @@ -238,9 +238,7 @@ class RetryTemplateTests { .withMessageMatching("Unable to back off for retryable operation '.+?'") .withCause(interruptedException) .satisfies(throwable -> assertThat(throwable.getSuppressed()).containsExactly(exception)) - // TODO Fix retry count for InterruptedException scenario. - // Retry count should actually be 0. - .satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(1)) + .satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero()) .satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyInterruption(retryPolicy, retryable, throwable)); verifyNoMoreInteractions(retryListener);