Introduce RetryListener#onRetryableExecution callback with RetryState
Closes gh-35940
This commit is contained in:
parent
d0be180a69
commit
adcd7cb4cb
|
|
@ -17,6 +17,9 @@
|
|||
package org.springframework.core.retry;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
|
|
@ -27,13 +30,8 @@ import java.util.Objects;
|
|||
* any exceptions from previous attempts as {@linkplain #getSuppressed() suppressed
|
||||
* exceptions}.
|
||||
*
|
||||
* <p>However, if an {@link InterruptedException} is encountered while
|
||||
* {@linkplain Thread#sleep(long) sleeping} for the current
|
||||
* {@link org.springframework.util.backoff.BackOff BackOff} duration, a
|
||||
* {@code RetryException} will contain the {@code InterruptedException} as the
|
||||
* {@linkplain #getCause() cause} and any exceptions from previous invocations
|
||||
* of the {@code Retryable} operation as {@linkplain #getSuppressed() suppressed
|
||||
* exceptions}.
|
||||
* <p>Implements the {@link RetryState} interface for exposing the final outcome,
|
||||
* as a parameter of the terminal listener methods on {@link RetryListener}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @author Juergen Hoeller
|
||||
|
|
@ -41,7 +39,7 @@ import java.util.Objects;
|
|||
* @since 7.0
|
||||
* @see RetryOperations
|
||||
*/
|
||||
public class RetryException extends Exception {
|
||||
public class RetryException extends Exception implements RetryState {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
|
@ -50,14 +48,26 @@ public class RetryException extends Exception {
|
|||
/**
|
||||
* Create a new {@code RetryException} for the supplied message and cause.
|
||||
* @param message the detail message
|
||||
* @param cause the last exception thrown by the {@link Retryable} operation,
|
||||
* or an {@link InterruptedException} thrown while sleeping for the current
|
||||
* {@code BackOff} duration
|
||||
* @param cause the last exception thrown by the {@link Retryable} operation
|
||||
*/
|
||||
public RetryException(String message, Throwable cause) {
|
||||
super(message, Objects.requireNonNull(cause, "cause must not be null"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code RetryException} for the supplied message and state.
|
||||
* @param message the detail message
|
||||
* @param retryState the final retry state
|
||||
* @since 7.0.2
|
||||
*/
|
||||
RetryException(String message, RetryState retryState) {
|
||||
super(message, retryState.getLastException());
|
||||
List<Throwable> exceptions = retryState.getExceptions();
|
||||
for (int i = 0; i < exceptions.size() - 1; i++) {
|
||||
addSuppressed(exceptions.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the last exception thrown by the {@link Retryable} operation, or an
|
||||
|
|
@ -73,8 +83,31 @@ public class RetryException extends Exception {
|
|||
* Return the number of retry attempts, or 0 if no retry has been attempted
|
||||
* after the initial invocation at all.
|
||||
*/
|
||||
@Override
|
||||
public int getRetryCount() {
|
||||
return getSuppressed().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all invocation exceptions encountered, in the order of occurrence.
|
||||
* @since 7.0.2
|
||||
*/
|
||||
@Override
|
||||
public List<Throwable> getExceptions() {
|
||||
Throwable[] suppressed = getSuppressed();
|
||||
List<Throwable> exceptions = new ArrayList<>(suppressed.length + 1);
|
||||
Collections.addAll(exceptions, suppressed);
|
||||
exceptions.add(getCause());
|
||||
return Collections.unmodifiableList(exceptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the exception from the last invocation (also exposed as a cause).
|
||||
* @since 7.0.2
|
||||
*/
|
||||
@Override
|
||||
public Throwable getLastException() {
|
||||
return getCause();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import org.jspecify.annotations.Nullable;
|
|||
*/
|
||||
public interface RetryListener {
|
||||
|
||||
// Interception callbacks for retry attempts (not covering the initial invocation)
|
||||
|
||||
/**
|
||||
* Called before every retry attempt.
|
||||
* @param retryPolicy the {@link RetryPolicy}
|
||||
|
|
@ -59,6 +61,27 @@ public interface RetryListener {
|
|||
default void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
|
||||
}
|
||||
|
||||
|
||||
// Execution callbacks for all invocation attempts and terminal scenarios
|
||||
|
||||
/**
|
||||
* Called after every attempt, including the initial invocation.
|
||||
* <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()
|
||||
* @see RetryState#getLastException()
|
||||
* @see RetryState#getRetryCount()
|
||||
*/
|
||||
default void onRetryableExecution(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called if the {@link RetryPolicy} is exhausted.
|
||||
* @param retryPolicy the {@code RetryPolicy}
|
||||
|
|
@ -66,8 +89,7 @@ public interface RetryListener {
|
|||
* @param exception the resulting {@link RetryException}, with the last
|
||||
* exception thrown by the {@code Retryable} operation as the cause and any
|
||||
* exceptions from previous attempts as suppressed exceptions
|
||||
* @see RetryException#getCause()
|
||||
* @see RetryException#getSuppressed()
|
||||
* @see RetryException#getExceptions()
|
||||
* @see RetryException#getRetryCount()
|
||||
*/
|
||||
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
|
||||
|
|
@ -80,8 +102,7 @@ public interface RetryListener {
|
|||
* @param exception the resulting {@link RetryException}, with an
|
||||
* {@link InterruptedException} as the cause and any exceptions from previous
|
||||
* invocations of the {@code Retryable} operation as suppressed exceptions
|
||||
* @see RetryException#getCause()
|
||||
* @see RetryException#getSuppressed()
|
||||
* @see RetryException#getExceptions()
|
||||
* @see RetryException#getRetryCount()
|
||||
*/
|
||||
default void onRetryPolicyInterruption(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
|
||||
|
|
@ -96,8 +117,7 @@ public interface RetryListener {
|
|||
* exception thrown by the {@code Retryable} operation as the cause and any
|
||||
* exceptions from previous attempts as suppressed exceptions
|
||||
* @since 7.0.2
|
||||
* @see RetryException#getCause()
|
||||
* @see RetryException#getSuppressed()
|
||||
* @see RetryException#getExceptions()
|
||||
* @see RetryException#getRetryCount()
|
||||
*/
|
||||
default void onRetryPolicyTimeout(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.retry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A representation of the current retry state, including the
|
||||
* current retry count and the exceptions accumulated so far.
|
||||
*
|
||||
* <p>Used as a parameter for {@link RetryListener#onRetryableExecution}.
|
||||
* Implemented by {@link RetryException} as well, exposing the final outcome in
|
||||
* the terminal listener methods {@link RetryListener#onRetryPolicyExhaustion},
|
||||
* {@link RetryListener#onRetryPolicyInterruption} and
|
||||
* {@link RetryListener#onRetryPolicyTimeout}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0.2
|
||||
*/
|
||||
public interface RetryState {
|
||||
|
||||
/**
|
||||
* Return the current retry count: 0 indicates the initial invocation,
|
||||
* 1 the first retry attempt, etc.
|
||||
* <p>This may indicate the current attempt or the final number of
|
||||
* retry attempts, depending on the time of the method call.
|
||||
*/
|
||||
int getRetryCount();
|
||||
|
||||
/**
|
||||
* Return the invocation exceptions accumulated so far,
|
||||
* in the order of occurrence.
|
||||
*/
|
||||
List<Throwable> getExceptions();
|
||||
|
||||
/**
|
||||
* Return the recorded exception from the last invocation.
|
||||
* @throws IllegalStateException if no exception has been recorded
|
||||
*/
|
||||
default Throwable getLastException() {
|
||||
List<Throwable> exceptions = getExceptions();
|
||||
if (exceptions.isEmpty()) {
|
||||
throw new IllegalStateException("No exception recorded");
|
||||
}
|
||||
return exceptions.get(exceptions.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether a successful invocation has been accomplished.
|
||||
*/
|
||||
default boolean isSuccessful() {
|
||||
return getRetryCount() >= getExceptions().size();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package org.springframework.core.retry;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
|
|
@ -135,80 +135,93 @@ public class RetryTemplate implements RetryOperations {
|
|||
*/
|
||||
@Override
|
||||
public <R extends @Nullable Object> R execute(Retryable<R> retryable) throws RetryException {
|
||||
String retryableName = retryable.getName();
|
||||
long startTime = System.currentTimeMillis();
|
||||
String retryableName = retryable.getName();
|
||||
MutableRetryState retryState = new MutableRetryState();
|
||||
|
||||
// Initial attempt
|
||||
logger.debug(() -> "Preparing to execute retryable operation '%s'".formatted(retryableName));
|
||||
R result;
|
||||
try {
|
||||
logger.debug(() -> "Preparing to execute retryable operation '%s'".formatted(retryableName));
|
||||
R result = retryable.execute();
|
||||
logger.debug(() -> "Retryable operation '%s' completed successfully".formatted(retryableName));
|
||||
return result;
|
||||
result = retryable.execute();
|
||||
}
|
||||
catch (Throwable initialException) {
|
||||
logger.debug(initialException,
|
||||
() -> "Execution of retryable operation '%s' failed; initiating the retry process"
|
||||
.formatted(retryableName));
|
||||
retryState.addException(initialException);
|
||||
this.retryListener.onRetryableExecution(this.retryPolicy, retryable, retryState);
|
||||
|
||||
// Retry process starts here
|
||||
BackOffExecution backOffExecution = this.retryPolicy.getBackOff().start();
|
||||
Deque<Throwable> exceptions = new ArrayDeque<>(4);
|
||||
exceptions.add(initialException);
|
||||
|
||||
Throwable lastException = initialException;
|
||||
long timeout = this.retryPolicy.getTimeout().toMillis();
|
||||
|
||||
while (this.retryPolicy.shouldRetry(lastException)) {
|
||||
checkIfTimeoutExceeded(timeout, startTime, 0, retryable, exceptions);
|
||||
checkIfTimeoutExceeded(timeout, startTime, 0, retryable, retryState);
|
||||
|
||||
try {
|
||||
long sleepTime = backOffExecution.nextBackOff();
|
||||
if (sleepTime == BackOffExecution.STOP) {
|
||||
break;
|
||||
}
|
||||
checkIfTimeoutExceeded(timeout, startTime, sleepTime, retryable, exceptions);
|
||||
checkIfTimeoutExceeded(timeout, startTime, sleepTime, retryable, retryState);
|
||||
logger.debug(() -> "Backing off for %dms after retryable operation '%s'"
|
||||
.formatted(sleepTime, retryableName));
|
||||
Thread.sleep(sleepTime);
|
||||
}
|
||||
catch (InterruptedException interruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
RetryException retryException = new RetryInterruptedException(
|
||||
"Unable to back off for retryable operation '%s'".formatted(retryableName),
|
||||
interruptedException);
|
||||
exceptions.forEach(retryException::addSuppressed);
|
||||
RetryException retryException = new RetryException(
|
||||
"Interrupted during back-off for retryable operation '%s'".formatted(retryableName),
|
||||
retryState);
|
||||
this.retryListener.onRetryPolicyInterruption(this.retryPolicy, retryable, retryException);
|
||||
throw retryException;
|
||||
}
|
||||
|
||||
logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName));
|
||||
retryState.increaseRetryCount();
|
||||
this.retryListener.beforeRetry(this.retryPolicy, retryable);
|
||||
try {
|
||||
this.retryListener.beforeRetry(this.retryPolicy, retryable);
|
||||
R result = retryable.execute();
|
||||
this.retryListener.onRetrySuccess(this.retryPolicy, retryable, result);
|
||||
logger.debug(() -> "Retryable operation '%s' completed successfully after retry"
|
||||
.formatted(retryableName));
|
||||
return result;
|
||||
result = retryable.execute();
|
||||
}
|
||||
catch (Throwable currentException) {
|
||||
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);
|
||||
exceptions.add(currentException);
|
||||
this.retryListener.onRetryableExecution(this.retryPolicy, retryable, retryState);
|
||||
lastException = currentException;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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.onRetryableExecution(this.retryPolicy, retryable, retryState);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The RetryPolicy has exhausted at this point, so we throw a RetryException with the
|
||||
// last exception as the cause and remaining exceptions as suppressed exceptions.
|
||||
RetryException retryException = new RetryException(
|
||||
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
|
||||
exceptions.removeLast());
|
||||
exceptions.forEach(retryException::addSuppressed);
|
||||
retryState);
|
||||
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, retryException);
|
||||
throw retryException;
|
||||
}
|
||||
|
||||
// Never entered initial catch block -> initial success.
|
||||
logger.debug(() -> "Retryable operation '%s' completed successfully".formatted(retryableName));
|
||||
this.retryListener.onRetryableExecution(this.retryPolicy, retryable, retryState);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void checkIfTimeoutExceeded(long timeout, long startTime, long sleepTime, Retryable<?> retryable,
|
||||
Deque<Throwable> exceptions) throws RetryException {
|
||||
RetryState retryState) throws RetryException {
|
||||
|
||||
if (timeout != 0) {
|
||||
if (timeout > 0) {
|
||||
// If sleepTime > 0, we are predicting what the effective elapsed time
|
||||
// would be if we were to sleep for sleepTime milliseconds.
|
||||
long elapsedTime = System.currentTimeMillis() + sleepTime - startTime;
|
||||
|
|
@ -219,8 +232,7 @@ public class RetryTemplate implements RetryOperations {
|
|||
.formatted(retryable.getName(), timeout, sleepTime) :
|
||||
"Retry policy for operation '%s' exceeded timeout (%dms); aborting execution"
|
||||
.formatted(retryable.getName(), timeout));
|
||||
RetryException retryException = new RetryException(message, exceptions.removeLast());
|
||||
exceptions.forEach(retryException::addSuppressed);
|
||||
RetryException retryException = new RetryException(message, retryState);
|
||||
this.retryListener.onRetryPolicyTimeout(this.retryPolicy, retryable, retryException);
|
||||
throw retryException;
|
||||
}
|
||||
|
|
@ -228,19 +240,33 @@ public class RetryTemplate implements RetryOperations {
|
|||
}
|
||||
|
||||
|
||||
private static class RetryInterruptedException extends RetryException {
|
||||
private static class MutableRetryState implements RetryState {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int retryCount;
|
||||
|
||||
private final List<Throwable> exceptions = new ArrayList<>(4);
|
||||
|
||||
RetryInterruptedException(String message, InterruptedException cause) {
|
||||
super(message, cause);
|
||||
public void increaseRetryCount(){
|
||||
this.retryCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRetryCount() {
|
||||
return (getSuppressed().length - 1);
|
||||
return this.retryCount;
|
||||
}
|
||||
|
||||
public void addException(Throwable exception) {
|
||||
this.exceptions.add(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Throwable> getExceptions() {
|
||||
return Collections.unmodifiableList(this.exceptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RetryState: retryCount=" + this.retryCount + ", exceptions=" + this.exceptions;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import org.jspecify.annotations.Nullable;
|
|||
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 org.springframework.util.Assert;
|
||||
|
||||
|
|
@ -85,6 +86,11 @@ public class CompositeRetryListener implements RetryListener {
|
|||
this.listeners.forEach(listener -> listener.onRetryFailure(retryPolicy, retryable, throwable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRetryableExecution(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState) {
|
||||
this.listeners.forEach(listener -> listener.onRetryableExecution(retryPolicy, retryable, retryState));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
|
||||
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, exception));
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
|
|
@ -91,7 +91,9 @@ class RetryTemplateTests {
|
|||
assertThat(invocationCount).hasValue(1);
|
||||
|
||||
// RetryListener interactions:
|
||||
verifyNoInteractions(retryListener);
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> state.isSuccessful() && state.getRetryCount() == 0));
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -110,6 +112,10 @@ class RetryTemplateTests {
|
|||
.withCause(exception)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
|
||||
.satisfies(throwable -> assertThat(throwable.getExceptions()).containsExactly(exception))
|
||||
.satisfies(throwable -> assertThat(throwable.getLastException()).isSameAs(exception))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 0)))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
|
|
@ -133,6 +139,10 @@ class RetryTemplateTests {
|
|||
.withCause(exception)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
|
||||
.satisfies(throwable -> assertThat(throwable.getExceptions()).containsExactly(exception))
|
||||
.satisfies(throwable -> assertThat(throwable.getLastException()).isSameAs(exception))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 0)))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
|
|
@ -155,6 +165,10 @@ class RetryTemplateTests {
|
|||
.withCause(exception)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
|
||||
.satisfies(throwable -> assertThat(throwable.getExceptions()).containsExactly(exception))
|
||||
.satisfies(throwable -> assertThat(throwable.getLastException()).isSameAs(exception))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 0)))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
|
|
@ -175,10 +189,14 @@ class RetryTemplateTests {
|
|||
assertThat(invocationCount).hasValue(3);
|
||||
|
||||
// RetryListener interactions:
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, new CustomException("Boom 2"));
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetrySuccess(retryPolicy, retryable, "finally succeeded");
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> state.isSuccessful() && state.getRetryCount() == 2));
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +209,6 @@ class RetryTemplateTests {
|
|||
public String execute() {
|
||||
throw new CustomException("Boom " + invocationCount.incrementAndGet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "test";
|
||||
|
|
@ -206,10 +223,13 @@ class RetryTemplateTests {
|
|||
.satisfies(throwable -> {
|
||||
var counter = new AtomicInteger(1);
|
||||
repeat(3, () -> {
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
|
||||
new CustomException("Boom " + counter.incrementAndGet()));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 3));
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
|
||||
});
|
||||
// 4 = 1 initial invocation + 3 retry attempts
|
||||
|
|
@ -237,10 +257,12 @@ class RetryTemplateTests {
|
|||
|
||||
assertThatExceptionOfType(RetryException.class)
|
||||
.isThrownBy(() -> retryTemplate.execute(retryable))
|
||||
.withMessageMatching("Unable to back off for retryable operation '.+?'")
|
||||
.withCause(interruptedException)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).containsExactly(exception))
|
||||
.withMessageMatching("Interrupted during back-off for retryable operation '.+?'")
|
||||
.withCause(exception)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 0)))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyInterruption(retryPolicy, retryable, throwable));
|
||||
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
|
|
@ -257,7 +279,6 @@ class RetryTemplateTests {
|
|||
invocationCount.incrementAndGet();
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "always fails";
|
||||
|
|
@ -280,9 +301,12 @@ class RetryTemplateTests {
|
|||
.withCause(exception)
|
||||
.satisfies(throwable -> {
|
||||
repeat(5, () -> {
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception);
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 5));
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
|
||||
});
|
||||
// 6 = 1 initial invocation + 5 retry attempts
|
||||
|
|
@ -305,7 +329,6 @@ class RetryTemplateTests {
|
|||
default -> "success";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "test";
|
||||
|
|
@ -332,9 +355,12 @@ class RetryTemplateTests {
|
|||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
|
||||
.satisfies(throwable -> {
|
||||
repeat(2, () -> {
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 2));
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
|
||||
});
|
||||
// 3 = 1 initial invocation + 2 retry attempts
|
||||
|
|
@ -376,7 +402,6 @@ class RetryTemplateTests {
|
|||
default -> "success";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "test";
|
||||
|
|
@ -395,10 +420,14 @@ class RetryTemplateTests {
|
|||
))
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
|
||||
.satisfies(throwable -> {
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(RuntimeException.class));
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class));
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), 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);
|
||||
});
|
||||
// 3 = 1 initial invocation + 2 retry attempts
|
||||
|
|
@ -429,7 +458,9 @@ class RetryTemplateTests {
|
|||
assertThat(invocationCount).hasValue(1);
|
||||
|
||||
// RetryListener interactions:
|
||||
verifyNoInteractions(retryListener);
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> state.isSuccessful() && state.getRetryCount() == 0));
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -453,13 +484,15 @@ class RetryTemplateTests {
|
|||
.withCause(exception)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 0)))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryWithTimeoutExceededAfterInitialFailure() throws Exception {
|
||||
void retryWithTimeoutExceededAfterInitialFailure() {
|
||||
RetryPolicy retryPolicy = RetryPolicy.builder()
|
||||
.timeout(Duration.ofMillis(10))
|
||||
.delay(Duration.ZERO)
|
||||
|
|
@ -478,6 +511,8 @@ class RetryTemplateTests {
|
|||
.isThrownBy(() -> retryTemplate.execute(retryable))
|
||||
.withMessageMatching("Retry policy for operation '.+?' exceeded timeout \\(10ms\\); aborting execution")
|
||||
.withCause(new CustomException("Boom 1"))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 0)))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyTimeout(
|
||||
eq(retryPolicy), eq(retryable), eq(throwable)));
|
||||
assertThat(invocationCount).hasValue(1);
|
||||
|
|
@ -486,7 +521,7 @@ class RetryTemplateTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void retryWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry() throws Exception {
|
||||
void retryWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry() {
|
||||
RetryPolicy retryPolicy = RetryPolicy.builder()
|
||||
.timeout(Duration.ofMillis(20))
|
||||
.delay(Duration.ofMillis(100)) // Delay > Timeout
|
||||
|
|
@ -507,6 +542,8 @@ class RetryTemplateTests {
|
|||
due to pending sleep time \\(100ms\\); preemptively aborting execution\
|
||||
""")
|
||||
.withCause(new CustomException("Boom 1"))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 0)))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyTimeout(
|
||||
eq(retryPolicy), eq(retryable), eq(throwable)));
|
||||
assertThat(invocationCount).hasValue(1);
|
||||
|
|
@ -515,7 +552,7 @@ class RetryTemplateTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void retryWithTimeoutExceededAfterFirstRetry() throws Exception {
|
||||
void retryWithTimeoutExceededAfterFirstRetry() {
|
||||
RetryPolicy retryPolicy = RetryPolicy.builder()
|
||||
.timeout(Duration.ofMillis(20))
|
||||
.delay(Duration.ZERO)
|
||||
|
|
@ -538,9 +575,11 @@ class RetryTemplateTests {
|
|||
.withMessageMatching("Retry policy for operation '.+?' exceeded timeout \\(20ms\\); aborting execution")
|
||||
.withCause(new CustomException("Boom 2"))
|
||||
.satisfies(throwable -> {
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any());
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, new CustomException("Boom 2"));
|
||||
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 1));
|
||||
inOrder.verify(retryListener).onRetryPolicyTimeout(
|
||||
eq(retryPolicy), eq(retryable), eq(throwable));
|
||||
});
|
||||
|
|
@ -550,7 +589,7 @@ class RetryTemplateTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void retryWithTimeoutExceededAfterSecondRetry() throws Exception {
|
||||
void retryWithTimeoutExceededAfterSecondRetry() {
|
||||
RetryPolicy retryPolicy = RetryPolicy.builder()
|
||||
.timeout(Duration.ofMillis(20))
|
||||
.delay(Duration.ZERO)
|
||||
|
|
@ -575,10 +614,13 @@ class RetryTemplateTests {
|
|||
.satisfies(throwable -> {
|
||||
var counter = new AtomicInteger(1);
|
||||
repeat(2, () -> {
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any());
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
|
||||
new CustomException("Boom " + counter.incrementAndGet()));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable),
|
||||
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 2));
|
||||
inOrder.verify(retryListener).onRetryPolicyTimeout(
|
||||
eq(retryPolicy), eq(retryable), eq(throwable));
|
||||
});
|
||||
|
|
@ -586,7 +628,6 @@ class RetryTemplateTests {
|
|||
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,16 @@ class CompositeRetryListenerTests {
|
|||
verify(listener3).onRetryFailure(retryPolicy, retryable, exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onRetryableExecution() {
|
||||
RetryException exception = new RetryException("", new Exception());
|
||||
compositeRetryListener.onRetryableExecution(retryPolicy, retryable, exception);
|
||||
|
||||
verify(listener1).onRetryableExecution(retryPolicy, retryable, exception);
|
||||
verify(listener2).onRetryableExecution(retryPolicy, retryable, exception);
|
||||
verify(listener3).onRetryableExecution(retryPolicy, retryable, exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onRetryPolicyExhaustion() {
|
||||
RetryException exception = new RetryException("", new Exception());
|
||||
|
|
|
|||
Loading…
Reference in New Issue