Skip to content

Commit f64ff28

Browse files
committed
Expose RetryException to onRetryPolicyExhaustion (also in the signature)
Includes getRetryPolicy and getRetryListener accessors in RetryTemplate. Closes gh-35334
1 parent 2489cce commit f64ff28

File tree

5 files changed

+76
-47
lines changed

5 files changed

+76
-47
lines changed

spring-core/src/main/java/org/springframework/core/retry/RetryListener.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*
3030
* @author Mahmoud Ben Hassine
3131
* @author Sam Brannen
32+
* @author Juergen Hoeller
3233
* @since 7.0
3334
* @see CompositeRetryListener
3435
*/
@@ -64,9 +65,13 @@ default void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Thr
6465
* Called if the {@link RetryPolicy} is exhausted.
6566
* @param retryPolicy the {@code RetryPolicy}
6667
* @param retryable the {@code Retryable} operation
67-
* @param throwable the last exception thrown by the {@link Retryable} operation
68+
* @param exception the resulting {@link RetryException}, including the last operation
69+
* exception as a cause and all earlier operation exceptions as suppressed exceptions
70+
* @see RetryException#getCause()
71+
* @see RetryException#getSuppressed()
72+
* @see RetryException#getRetryCount()
6873
*/
69-
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
74+
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
7075
}
7176

7277
}

spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ public void setRetryPolicy(RetryPolicy retryPolicy) {
9090
this.retryPolicy = retryPolicy;
9191
}
9292

93+
/**
94+
* Return the current {@link RetryPolicy} that is in use
95+
* with this template.
96+
*/
97+
public RetryPolicy getRetryPolicy() {
98+
return this.retryPolicy;
99+
}
100+
93101
/**
94102
* Set the {@link RetryListener} to use.
95103
* <p>If multiple listeners are needed, use a
@@ -102,6 +110,14 @@ public void setRetryListener(RetryListener retryListener) {
102110
this.retryListener = retryListener;
103111
}
104112

113+
/**
114+
* Return the current {@link RetryListener} that is in use
115+
* with this template.
116+
*/
117+
public RetryListener getRetryListener() {
118+
return this.retryListener;
119+
}
120+
105121

106122
/**
107123
* Execute the supplied {@link Retryable} operation according to the configured
@@ -176,7 +192,7 @@ public void setRetryListener(RetryListener retryListener) {
176192
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
177193
exceptions.removeLast());
178194
exceptions.forEach(retryException::addSuppressed);
179-
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, lastException);
195+
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, retryException);
180196
throw retryException;
181197
}
182198
}

spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.jspecify.annotations.Nullable;
2323

24+
import org.springframework.core.retry.RetryException;
2425
import org.springframework.core.retry.RetryListener;
2526
import org.springframework.core.retry.RetryPolicy;
2627
import org.springframework.core.retry.RetryTemplate;
@@ -34,6 +35,7 @@
3435
* <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
3536
*
3637
* @author Mahmoud Ben Hassine
38+
* @author Juergen Hoeller
3739
* @since 7.0
3840
*/
3941
public class CompositeRetryListener implements RetryListener {
@@ -82,8 +84,8 @@ public void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Thro
8284
}
8385

8486
@Override
85-
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
86-
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
87+
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
88+
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, exception));
8789
}
8890

8991
}

spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ void configureRetryTemplate() {
6868
retryTemplate.setRetryListener(retryListener);
6969
}
7070

71+
@Test
72+
void checkRetryTemplateConfiguration() {
73+
assertThat(retryTemplate.getRetryPolicy()).isSameAs(retryPolicy);
74+
assertThat(retryTemplate.getRetryListener()).isSameAs(retryListener);
75+
}
76+
7177
@Test
7278
void retryWithImmediateSuccess() throws Exception {
7379
AtomicInteger invocationCount = new AtomicInteger();
@@ -99,10 +105,9 @@ void retryWithInitialFailureAndZeroRetriesRetryPolicy() {
99105
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
100106
.withCause(exception)
101107
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
102-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
108+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
109+
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
103110

104-
// RetryListener interactions:
105-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
106111
verifyNoMoreInteractions(retryListener);
107112
}
108113

@@ -122,10 +127,9 @@ void retryWithInitialFailureAndZeroRetriesFixedBackOffPolicy() {
122127
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
123128
.withCause(exception)
124129
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
125-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
130+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
131+
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
126132

127-
// RetryListener interactions:
128-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
129133
verifyNoMoreInteractions(retryListener);
130134
}
131135

@@ -145,10 +149,9 @@ void retryWithInitialFailureAndZeroRetriesBackOffPolicyFromBuilder() {
145149
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
146150
.withCause(exception)
147151
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
148-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
152+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
153+
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
149154

150-
// RetryListener interactions:
151-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
152155
verifyNoMoreInteractions(retryListener);
153156
}
154157

@@ -194,18 +197,19 @@ public String getName() {
194197
assertThatExceptionOfType(RetryException.class)
195198
.isThrownBy(() -> retryTemplate.execute(retryable))
196199
.withMessage("Retry policy for operation 'test' exhausted; aborting execution")
197-
.withCause(new CustomException("Boom 4"));
200+
.withCause(new CustomException("Boom 4"))
201+
.satisfies(throwable -> {
202+
invocationCount.set(1);
203+
repeat(3, () -> {
204+
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
205+
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
206+
new CustomException("Boom " + invocationCount.incrementAndGet()));
207+
});
208+
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
209+
});
198210
// 4 = 1 initial invocation + 3 retry attempts
199211
assertThat(invocationCount).hasValue(4);
200212

201-
// RetryListener interactions:
202-
invocationCount.set(1);
203-
repeat(3, () -> {
204-
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
205-
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
206-
new CustomException("Boom " + invocationCount.incrementAndGet()));
207-
});
208-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, new CustomException("Boom 4"));
209213
verifyNoMoreInteractions(retryListener);
210214
}
211215

@@ -240,16 +244,17 @@ public String getName() {
240244
assertThatExceptionOfType(RetryException.class)
241245
.isThrownBy(() -> retryTemplate.execute(retryable))
242246
.withMessage("Retry policy for operation 'always fails' exhausted; aborting execution")
243-
.withCause(exception);
247+
.withCause(exception)
248+
.satisfies(throwable -> {
249+
repeat(5, () -> {
250+
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
251+
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception);
252+
});
253+
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
254+
});
244255
// 6 = 1 initial invocation + 5 retry attempts
245256
assertThat(invocationCount).hasValue(6);
246257

247-
// RetryListener interactions:
248-
repeat(5, () -> {
249-
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
250-
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception);
251-
});
252-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
253258
verifyNoMoreInteractions(retryListener);
254259
}
255260

@@ -291,17 +296,17 @@ public String getName() {
291296
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(FileNotFoundException.class),
292297
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class)
293298
))
294-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2));
299+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
300+
.satisfies(throwable -> {
301+
repeat(2, () -> {
302+
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
303+
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class));
304+
});
305+
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
306+
});
295307
// 3 = 1 initial invocation + 2 retry attempts
296308
assertThat(invocationCount).hasValue(3);
297309

298-
// RetryListener interactions:
299-
repeat(2, () -> {
300-
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
301-
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class));
302-
});
303-
inOrder.verify(retryListener).onRetryPolicyExhaustion(
304-
eq(retryPolicy), eq(retryable), any(IllegalStateException.class));
305310
verifyNoMoreInteractions(retryListener);
306311
}
307312

@@ -354,17 +359,17 @@ public String getName() {
354359
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
355360
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class)
356361
))
357-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2));
362+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
363+
.satisfies(throwable -> {
364+
repeat(2, () -> {
365+
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
366+
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class));
367+
});
368+
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
369+
});
358370
// 3 = 1 initial invocation + 2 retry attempts
359371
assertThat(invocationCount).hasValue(3);
360372

361-
// RetryListener interactions:
362-
repeat(2, () -> {
363-
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
364-
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class));
365-
});
366-
inOrder.verify(retryListener).onRetryPolicyExhaustion(
367-
eq(retryPolicy), eq(retryable), any(CustomFileNotFoundException.class));
368373
verifyNoMoreInteractions(retryListener);
369374
}
370375

spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.junit.jupiter.api.BeforeEach;
2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.core.retry.RetryException;
2425
import org.springframework.core.retry.RetryListener;
2526
import org.springframework.core.retry.RetryPolicy;
2627
import org.springframework.core.retry.Retryable;
@@ -83,7 +84,7 @@ void onRetryFailure() {
8384

8485
@Test
8586
void onRetryPolicyExhaustion() {
86-
Exception exception = new Exception();
87+
RetryException exception = new RetryException("", new Exception());
8788
compositeRetryListener.onRetryPolicyExhaustion(retryPolicy, retryable, exception);
8889

8990
verify(listener1).onRetryPolicyExhaustion(retryPolicy, retryable, exception);

0 commit comments

Comments
 (0)