Skip to content

Commit adcd7cb

Browse files
committed
Introduce RetryListener#onRetryableExecution callback with RetryState
Closes gh-35940
1 parent d0be180 commit adcd7cb

File tree

7 files changed

+274
-69
lines changed

7 files changed

+274
-69
lines changed

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

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
package org.springframework.core.retry;
1818

1919
import java.io.Serial;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
2023
import java.util.Objects;
2124

2225
/**
@@ -27,21 +30,16 @@
2730
* any exceptions from previous attempts as {@linkplain #getSuppressed() suppressed
2831
* exceptions}.
2932
*
30-
* <p>However, if an {@link InterruptedException} is encountered while
31-
* {@linkplain Thread#sleep(long) sleeping} for the current
32-
* {@link org.springframework.util.backoff.BackOff BackOff} duration, a
33-
* {@code RetryException} will contain the {@code InterruptedException} as the
34-
* {@linkplain #getCause() cause} and any exceptions from previous invocations
35-
* of the {@code Retryable} operation as {@linkplain #getSuppressed() suppressed
36-
* exceptions}.
33+
* <p>Implements the {@link RetryState} interface for exposing the final outcome,
34+
* as a parameter of the terminal listener methods on {@link RetryListener}.
3735
*
3836
* @author Mahmoud Ben Hassine
3937
* @author Juergen Hoeller
4038
* @author Sam Brannen
4139
* @since 7.0
4240
* @see RetryOperations
4341
*/
44-
public class RetryException extends Exception {
42+
public class RetryException extends Exception implements RetryState {
4543

4644
@Serial
4745
private static final long serialVersionUID = 1L;
@@ -50,14 +48,26 @@ public class RetryException extends Exception {
5048
/**
5149
* Create a new {@code RetryException} for the supplied message and cause.
5250
* @param message the detail message
53-
* @param cause the last exception thrown by the {@link Retryable} operation,
54-
* or an {@link InterruptedException} thrown while sleeping for the current
55-
* {@code BackOff} duration
51+
* @param cause the last exception thrown by the {@link Retryable} operation
5652
*/
5753
public RetryException(String message, Throwable cause) {
5854
super(message, Objects.requireNonNull(cause, "cause must not be null"));
5955
}
6056

57+
/**
58+
* Create a new {@code RetryException} for the supplied message and state.
59+
* @param message the detail message
60+
* @param retryState the final retry state
61+
* @since 7.0.2
62+
*/
63+
RetryException(String message, RetryState retryState) {
64+
super(message, retryState.getLastException());
65+
List<Throwable> exceptions = retryState.getExceptions();
66+
for (int i = 0; i < exceptions.size() - 1; i++) {
67+
addSuppressed(exceptions.get(i));
68+
}
69+
}
70+
6171

6272
/**
6373
* Get the last exception thrown by the {@link Retryable} operation, or an
@@ -73,8 +83,31 @@ public final Throwable getCause() {
7383
* Return the number of retry attempts, or 0 if no retry has been attempted
7484
* after the initial invocation at all.
7585
*/
86+
@Override
7687
public int getRetryCount() {
7788
return getSuppressed().length;
7889
}
7990

91+
/**
92+
* Return all invocation exceptions encountered, in the order of occurrence.
93+
* @since 7.0.2
94+
*/
95+
@Override
96+
public List<Throwable> getExceptions() {
97+
Throwable[] suppressed = getSuppressed();
98+
List<Throwable> exceptions = new ArrayList<>(suppressed.length + 1);
99+
Collections.addAll(exceptions, suppressed);
100+
exceptions.add(getCause());
101+
return Collections.unmodifiableList(exceptions);
102+
}
103+
104+
/**
105+
* Return the exception from the last invocation (also exposed as a cause).
106+
* @since 7.0.2
107+
*/
108+
@Override
109+
public Throwable getLastException() {
110+
return getCause();
111+
}
112+
80113
}

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
*/
3434
public interface RetryListener {
3535

36+
// Interception callbacks for retry attempts (not covering the initial invocation)
37+
3638
/**
3739
* Called before every retry attempt.
3840
* @param retryPolicy the {@link RetryPolicy}
@@ -59,15 +61,35 @@ default void onRetrySuccess(RetryPolicy retryPolicy, Retryable<?> retryable, @Nu
5961
default void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
6062
}
6163

64+
65+
// Execution callbacks for all invocation attempts and terminal scenarios
66+
67+
/**
68+
* Called after every attempt, including the initial invocation.
69+
* <p>The success of the attempt can be checked via {@link RetryState#isSuccessful()};
70+
* if not successful, the current exception can be introspected via
71+
* {@link RetryState#getLastException()}.
72+
* @param retryPolicy the {@link RetryPolicy}
73+
* @param retryable the {@link Retryable} operation
74+
* @param retryState the current state of retry processing
75+
* (this is a live instance reflecting the current state; not intended to be stored)
76+
* @since 7.0.2
77+
* @see RetryTemplate#execute(Retryable)
78+
* @see RetryState#isSuccessful()
79+
* @see RetryState#getLastException()
80+
* @see RetryState#getRetryCount()
81+
*/
82+
default void onRetryableExecution(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState) {
83+
}
84+
6285
/**
6386
* Called if the {@link RetryPolicy} is exhausted.
6487
* @param retryPolicy the {@code RetryPolicy}
6588
* @param retryable the {@link Retryable} operation
6689
* @param exception the resulting {@link RetryException}, with the last
6790
* exception thrown by the {@code Retryable} operation as the cause and any
6891
* exceptions from previous attempts as suppressed exceptions
69-
* @see RetryException#getCause()
70-
* @see RetryException#getSuppressed()
92+
* @see RetryException#getExceptions()
7193
* @see RetryException#getRetryCount()
7294
*/
7395
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
@@ -80,8 +102,7 @@ default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retry
80102
* @param exception the resulting {@link RetryException}, with an
81103
* {@link InterruptedException} as the cause and any exceptions from previous
82104
* invocations of the {@code Retryable} operation as suppressed exceptions
83-
* @see RetryException#getCause()
84-
* @see RetryException#getSuppressed()
105+
* @see RetryException#getExceptions()
85106
* @see RetryException#getRetryCount()
86107
*/
87108
default void onRetryPolicyInterruption(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
@@ -96,8 +117,7 @@ default void onRetryPolicyInterruption(RetryPolicy retryPolicy, Retryable<?> ret
96117
* exception thrown by the {@code Retryable} operation as the cause and any
97118
* exceptions from previous attempts as suppressed exceptions
98119
* @since 7.0.2
99-
* @see RetryException#getCause()
100-
* @see RetryException#getSuppressed()
120+
* @see RetryException#getExceptions()
101121
* @see RetryException#getRetryCount()
102122
*/
103123
default void onRetryPolicyTimeout(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.retry;
18+
19+
import java.util.List;
20+
21+
/**
22+
* A representation of the current retry state, including the
23+
* current retry count and the exceptions accumulated so far.
24+
*
25+
* <p>Used as a parameter for {@link RetryListener#onRetryableExecution}.
26+
* Implemented by {@link RetryException} as well, exposing the final outcome in
27+
* the terminal listener methods {@link RetryListener#onRetryPolicyExhaustion},
28+
* {@link RetryListener#onRetryPolicyInterruption} and
29+
* {@link RetryListener#onRetryPolicyTimeout}.
30+
*
31+
* @author Juergen Hoeller
32+
* @since 7.0.2
33+
*/
34+
public interface RetryState {
35+
36+
/**
37+
* Return the current retry count: 0 indicates the initial invocation,
38+
* 1 the first retry attempt, etc.
39+
* <p>This may indicate the current attempt or the final number of
40+
* retry attempts, depending on the time of the method call.
41+
*/
42+
int getRetryCount();
43+
44+
/**
45+
* Return the invocation exceptions accumulated so far,
46+
* in the order of occurrence.
47+
*/
48+
List<Throwable> getExceptions();
49+
50+
/**
51+
* Return the recorded exception from the last invocation.
52+
* @throws IllegalStateException if no exception has been recorded
53+
*/
54+
default Throwable getLastException() {
55+
List<Throwable> exceptions = getExceptions();
56+
if (exceptions.isEmpty()) {
57+
throw new IllegalStateException("No exception recorded");
58+
}
59+
return exceptions.get(exceptions.size() - 1);
60+
}
61+
62+
/**
63+
* Indicate whether a successful invocation has been accomplished.
64+
*/
65+
default boolean isSuccessful() {
66+
return getRetryCount() >= getExceptions().size();
67+
}
68+
69+
}

0 commit comments

Comments
 (0)