Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,31 @@
* <p>Typically registered in a {@link RetryTemplate}, and can be composed using a
* {@link org.springframework.core.retry.support.CompositeRetryListener CompositeRetryListener}.
*
* <p>Several callbacks come in two overloads: a reduced variant and a
* full-argument variant that additionally receives the {@link RetryState}.
* The full-argument variant delegates to the reduced variant by default.
* Implement either the reduced method or the full-argument method, not both,
* to avoid double processing of the same callback.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @author Juergen Hoeller
* @author Chengang Guan
* @since 7.0
* @see org.springframework.core.retry.support.CompositeRetryListener
*/
public interface RetryListener {

/**
* Called after every attempt, including the initial invocation.
* <p>The {@code retryState} is a live instance reflecting the current state;
* not intended to be stored.
* <p>The success of the attempt can be checked via {@link RetryState#isSuccessful()};
* if not successful, the current exception can be introspected via
* {@link RetryState#getLastException()}.
* @param retryPolicy the {@link RetryPolicy}
* @param retryable the {@link Retryable} operation
* @param retryState the current state of retry processing
* (this is a live instance reflecting the current state; not intended to be stored)
* @since 7.0.2
* @see RetryTemplate#execute(Retryable)
* @see RetryState#isSuccessful()
Expand All @@ -53,13 +61,14 @@ default void onRetryableExecution(RetryPolicy retryPolicy, Retryable<?> retryabl

/**
* Called before every retry attempt.
* <p>The {@code retryState} is a live instance reflecting the current state;
* not intended to be stored.
* <p>For a corresponding callback after every retry attempt, consider
* {@link #onRetryableExecution(RetryPolicy, Retryable, RetryState)}, ignoring the
* initial attempt through a corresponding {@link RetryState#getRetryCount()} check.
* @param retryPolicy the {@link RetryPolicy}
* @param retryable the {@link Retryable} operation
* @param retryState the current state of retry processing
* (this is a live instance reflecting the current state; not intended to be stored)
* @since 7.0.4
* @see #onRetryableExecution(RetryPolicy, Retryable, RetryState)
*/
Expand All @@ -70,7 +79,8 @@ default void beforeRetry(RetryPolicy retryPolicy, Retryable<?> retryable, RetryS
/**
* Called before every retry attempt.
* <p>Called by {@link #beforeRetry(RetryPolicy, Retryable, RetryState)}.
* Implement either this reduced method or that full-argument method, not both.
* See the {@linkplain RetryListener class-level documentation} for the
* overload contract.
* @param retryPolicy the {@link RetryPolicy}
* @param retryable the {@link Retryable} operation
* @see #beforeRetry(RetryPolicy, Retryable, RetryState)
Expand All @@ -80,18 +90,55 @@ default void beforeRetry(RetryPolicy retryPolicy, Retryable<?> retryable) {

/**
* Called after the first successful retry attempt.
* <p>The {@code retryState} is a live instance reflecting the current state;
* for a success callback, the state has reached the end of its updates and
* can therefore be considered effectively stable.
* @param retryPolicy the {@link RetryPolicy}
* @param retryable the {@link Retryable} operation
* @param retryState the current state of retry processing
* @param result the result of the {@code Retryable} operation
* @since 7.1
*/
default void onRetrySuccess(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState, @Nullable Object result) {
onRetrySuccess(retryPolicy, retryable, result);
}

/**
* Called after the first successful retry attempt.
* <p>Called by {@link #onRetrySuccess(RetryPolicy, Retryable, RetryState, Object)}.
* See the {@linkplain RetryListener class-level documentation} for the
* overload contract.
* @param retryPolicy the {@link RetryPolicy}
* @param retryable the {@link Retryable} operation
* @param result the result of the {@code Retryable} operation
* @see #onRetrySuccess(RetryPolicy, Retryable, RetryState, Object)
*/
default void onRetrySuccess(RetryPolicy retryPolicy, Retryable<?> retryable, @Nullable Object result) {
}

/**
* Called after every failed retry attempt.
* <p>The {@code retryState} is a live instance reflecting the current state;
* not intended to be stored.
* @param retryPolicy the {@link RetryPolicy}
* @param retryable the {@link Retryable} operation
* @param retryState the current state of retry processing
* @param throwable the exception thrown by the {@code Retryable} operation
* @since 7.1
*/
default void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState, Throwable throwable) {
onRetryFailure(retryPolicy, retryable, throwable);
}

/**
* Called after every failed retry attempt.
* <p>Called by {@link #onRetryFailure(RetryPolicy, Retryable, RetryState, Throwable)}.
* See the {@linkplain RetryListener class-level documentation} for the
* overload contract.
* @param retryPolicy the {@link RetryPolicy}
* @param retryable the {@link Retryable} operation
* @param throwable the exception thrown by the {@code Retryable} operation
* @see #onRetryFailure(RetryPolicy, Retryable, RetryState, Throwable)
*/
default void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public RetryListener getRetryListener() {
logger.debug(currentException, () -> "Retry attempt for operation '%s' failed due to '%s'"
.formatted(retryableName, currentException));
retryState.addException(currentException);
this.retryListener.onRetryFailure(this.retryPolicy, retryable, currentException);
this.retryListener.onRetryFailure(this.retryPolicy, retryable, retryState, currentException);
this.retryListener.onRetryableExecution(this.retryPolicy, retryable, retryState);
lastException = currentException;
continue;
Expand All @@ -186,7 +186,7 @@ public RetryListener getRetryListener() {
// Did not enter catch block above -> retry success.
logger.debug(() -> "Retryable operation '%s' completed successfully after retry"
.formatted(retryableName));
this.retryListener.onRetrySuccess(this.retryPolicy, retryable, result);
this.retryListener.onRetrySuccess(this.retryPolicy, retryable, retryState, result);
this.retryListener.onRetryableExecution(this.retryPolicy, retryable, retryState);
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
* @author Mahmoud Ben Hassine
* @author Juergen Hoeller
* @author Sam Brannen
* @author Chengang Guan
* @since 7.0
*/
public class CompositeRetryListener implements RetryListener {
Expand Down Expand Up @@ -82,13 +83,13 @@ public void beforeRetry(RetryPolicy retryPolicy, Retryable<?> retryable, RetrySt
}

@Override
public void onRetrySuccess(RetryPolicy retryPolicy, Retryable<?> retryable, @Nullable Object result) {
this.listeners.forEach(listener -> listener.onRetrySuccess(retryPolicy, retryable, result));
public void onRetrySuccess(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState, @Nullable Object result) {
this.listeners.forEach(listener -> listener.onRetrySuccess(retryPolicy, retryable, retryState, result));
}

@Override
public void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
this.listeners.forEach(listener -> listener.onRetryFailure(retryPolicy, retryable, throwable));
public void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState, Throwable throwable) {
this.listeners.forEach(listener -> listener.onRetryFailure(retryPolicy, retryable, retryState, throwable));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @author Juergen Hoeller
* @author Chengang Guan
* @since 7.0
* @see RetryPolicyTests
*/
Expand Down Expand Up @@ -192,10 +193,12 @@ void retryableWithSuccessAfterInitialFailures() throws Exception {
// RetryListener interactions:
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, new CustomException("Boom 2"));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(RetryState.class),
eq(new CustomException("Boom 2")));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetrySuccess(retryPolicy, retryable, "finally succeeded");
inOrder.verify(retryListener).onRetrySuccess(eq(retryPolicy), eq(retryable), any(RetryState.class),
eq("finally succeeded"));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
argThat(state -> state.isSuccessful() && state.getRetryCount() == 2));
verifyNoMoreInteractions(retryListener);
Expand Down Expand Up @@ -226,8 +229,8 @@ public String getName() {
repeat(3, () -> {
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
new CustomException("Boom " + counter.incrementAndGet()));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable),
any(RetryState.class), eq(new CustomException("Boom " + counter.incrementAndGet())));
});
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 3));
Expand Down Expand Up @@ -304,7 +307,8 @@ public String getName() {
repeat(5, () -> {
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception);
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable),
any(RetryState.class), eq(exception));
});
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 5));
Expand Down Expand Up @@ -358,7 +362,8 @@ public String getName() {
repeat(2, () -> {
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable),
any(RetryState.class), any(Exception.class));
});
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 2));
Expand Down Expand Up @@ -423,10 +428,12 @@ public String getName() {
.satisfies(throwable -> {
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(RuntimeException.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable),
any(RetryState.class), any(RuntimeException.class));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(CustomFileNotFoundException.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable),
any(RetryState.class), any(CustomFileNotFoundException.class));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 2));
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
Expand Down Expand Up @@ -479,6 +486,7 @@ void supplierWithSuccessAfterInitialFailures() {
any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
any(RetryState.class),
eq(new CustomException("Boom 2")));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
Expand All @@ -488,6 +496,7 @@ void supplierWithSuccessAfterInitialFailures() {
any(RetryState.class));
inOrder.verify(retryListener).onRetrySuccess(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
any(RetryState.class),
eq("finally succeeded"));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
Expand Down Expand Up @@ -517,6 +526,7 @@ void supplierWithExhaustedPolicy() {
any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
any(RetryState.class),
eq(new CustomException("Boom " + counter.incrementAndGet())));
});
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
Expand Down Expand Up @@ -571,6 +581,7 @@ void runnableWithSuccessAfterInitialFailures() {
any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
any(RetryState.class),
eq(new CustomException("Boom 2")));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
Expand All @@ -580,6 +591,7 @@ void runnableWithSuccessAfterInitialFailures() {
any(RetryState.class));
inOrder.verify(retryListener).onRetrySuccess(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
any(RetryState.class),
eq(null));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
Expand Down Expand Up @@ -609,6 +621,7 @@ void runnableWithExhaustedPolicy() {
any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
any(RetryState.class),
eq(new CustomException("Boom " + counter.incrementAndGet())));
});
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
Expand Down Expand Up @@ -766,7 +779,8 @@ void retryableWithTimeoutExceededAfterFirstRetry() {
.satisfies(throwable -> {
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, new CustomException("Boom 2"));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable),
any(RetryState.class), eq(new CustomException("Boom 2")));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 1));
inOrder.verify(retryListener).onRetryPolicyTimeout(
Expand Down Expand Up @@ -805,8 +819,8 @@ void retryableWithTimeoutExceededAfterSecondRetry() {
repeat(2, () -> {
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class));
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
new CustomException("Boom " + counter.incrementAndGet()));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable),
any(RetryState.class), eq(new CustomException("Boom " + counter.incrementAndGet())));
});
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 2));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.springframework.core.retry.RetryException;
import org.springframework.core.retry.RetryListener;
import org.springframework.core.retry.RetryPolicy;
import org.springframework.core.retry.RetryState;
import org.springframework.core.retry.Retryable;

import static org.mockito.Mockito.mock;
Expand All @@ -34,6 +35,7 @@
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @author Chengang Guan
* @since 7.0
*/
class CompositeRetryListenerTests {
Expand Down Expand Up @@ -76,21 +78,23 @@ void beforeRetry() {
@Test
void onRetrySuccess() {
Object result = new Object();
compositeRetryListener.onRetrySuccess(retryPolicy, retryable, result);
RetryState successState = new RetryException("", new Exception());
compositeRetryListener.onRetrySuccess(retryPolicy, retryable, successState, result);

verify(listener1).onRetrySuccess(retryPolicy, retryable, result);
verify(listener2).onRetrySuccess(retryPolicy, retryable, result);
verify(listener3).onRetrySuccess(retryPolicy, retryable, result);
verify(listener1).onRetrySuccess(retryPolicy, retryable, successState, result);
verify(listener2).onRetrySuccess(retryPolicy, retryable, successState, result);
verify(listener3).onRetrySuccess(retryPolicy, retryable, successState, result);
}

@Test
void onRetryFailure() {
Exception exception = new Exception();
compositeRetryListener.onRetryFailure(retryPolicy, retryable, exception);
RetryState failState = new RetryException("", new Exception());
compositeRetryListener.onRetryFailure(retryPolicy, retryable, failState, exception);

verify(listener1).onRetryFailure(retryPolicy, retryable, exception);
verify(listener2).onRetryFailure(retryPolicy, retryable, exception);
verify(listener3).onRetryFailure(retryPolicy, retryable, exception);
verify(listener1).onRetryFailure(retryPolicy, retryable, failState, exception);
verify(listener2).onRetryFailure(retryPolicy, retryable, failState, exception);
verify(listener3).onRetryFailure(retryPolicy, retryable, failState, exception);
}

@Test
Expand Down
Loading