mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Expose RetryException to onRetryPolicyExhaustion (also in the signature)
Includes getRetryPolicy and getRetryListener accessors in RetryTemplate. Closes gh-35334
This commit is contained in:
@@ -29,6 +29,7 @@ import org.springframework.core.retry.support.CompositeRetryListener;
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0
|
||||
* @see CompositeRetryListener
|
||||
*/
|
||||
@@ -64,9 +65,13 @@ public interface RetryListener {
|
||||
* Called if the {@link RetryPolicy} is exhausted.
|
||||
* @param retryPolicy the {@code RetryPolicy}
|
||||
* @param retryable the {@code Retryable} operation
|
||||
* @param throwable the last exception thrown by the {@link Retryable} operation
|
||||
* @param exception the resulting {@link RetryException}, including the last operation
|
||||
* exception as a cause and all earlier operation exceptions as suppressed exceptions
|
||||
* @see RetryException#getCause()
|
||||
* @see RetryException#getSuppressed()
|
||||
* @see RetryException#getRetryCount()
|
||||
*/
|
||||
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
|
||||
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -90,6 +90,14 @@ public class RetryTemplate implements RetryOperations {
|
||||
this.retryPolicy = retryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current {@link RetryPolicy} that is in use
|
||||
* with this template.
|
||||
*/
|
||||
public RetryPolicy getRetryPolicy() {
|
||||
return this.retryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link RetryListener} to use.
|
||||
* <p>If multiple listeners are needed, use a
|
||||
@@ -102,6 +110,14 @@ public class RetryTemplate implements RetryOperations {
|
||||
this.retryListener = retryListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current {@link RetryListener} that is in use
|
||||
* with this template.
|
||||
*/
|
||||
public RetryListener getRetryListener() {
|
||||
return this.retryListener;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute the supplied {@link Retryable} operation according to the configured
|
||||
@@ -176,7 +192,7 @@ public class RetryTemplate implements RetryOperations {
|
||||
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
|
||||
exceptions.removeLast());
|
||||
exceptions.forEach(retryException::addSuppressed);
|
||||
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, lastException);
|
||||
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, retryException);
|
||||
throw retryException;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
|
||||
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.RetryTemplate;
|
||||
@@ -34,6 +35,7 @@ import org.springframework.util.Assert;
|
||||
* <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
|
||||
*
|
||||
* @author Mahmoud Ben Hassine
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0
|
||||
*/
|
||||
public class CompositeRetryListener implements RetryListener {
|
||||
@@ -82,8 +84,8 @@ public class CompositeRetryListener implements RetryListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
|
||||
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
|
||||
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, exception));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -68,6 +68,12 @@ class RetryTemplateTests {
|
||||
retryTemplate.setRetryListener(retryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkRetryTemplateConfiguration() {
|
||||
assertThat(retryTemplate.getRetryPolicy()).isSameAs(retryPolicy);
|
||||
assertThat(retryTemplate.getRetryListener()).isSameAs(retryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryWithImmediateSuccess() throws Exception {
|
||||
AtomicInteger invocationCount = new AtomicInteger();
|
||||
@@ -99,10 +105,9 @@ class RetryTemplateTests {
|
||||
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
|
||||
.withCause(exception)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
|
||||
// RetryListener interactions:
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@@ -122,10 +127,9 @@ class RetryTemplateTests {
|
||||
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
|
||||
.withCause(exception)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
|
||||
// RetryListener interactions:
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@@ -145,10 +149,9 @@ class RetryTemplateTests {
|
||||
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
|
||||
.withCause(exception)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
|
||||
|
||||
// RetryListener interactions:
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@@ -194,18 +197,19 @@ class RetryTemplateTests {
|
||||
assertThatExceptionOfType(RetryException.class)
|
||||
.isThrownBy(() -> retryTemplate.execute(retryable))
|
||||
.withMessage("Retry policy for operation 'test' exhausted; aborting execution")
|
||||
.withCause(new CustomException("Boom 4"));
|
||||
.withCause(new CustomException("Boom 4"))
|
||||
.satisfies(throwable -> {
|
||||
invocationCount.set(1);
|
||||
repeat(3, () -> {
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
|
||||
new CustomException("Boom " + invocationCount.incrementAndGet()));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
|
||||
});
|
||||
// 4 = 1 initial invocation + 3 retry attempts
|
||||
assertThat(invocationCount).hasValue(4);
|
||||
|
||||
// RetryListener interactions:
|
||||
invocationCount.set(1);
|
||||
repeat(3, () -> {
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
|
||||
new CustomException("Boom " + invocationCount.incrementAndGet()));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, new CustomException("Boom 4"));
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@@ -240,16 +244,17 @@ class RetryTemplateTests {
|
||||
assertThatExceptionOfType(RetryException.class)
|
||||
.isThrownBy(() -> retryTemplate.execute(retryable))
|
||||
.withMessage("Retry policy for operation 'always fails' exhausted; aborting execution")
|
||||
.withCause(exception);
|
||||
.withCause(exception)
|
||||
.satisfies(throwable -> {
|
||||
repeat(5, () -> {
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception);
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
|
||||
});
|
||||
// 6 = 1 initial invocation + 5 retry attempts
|
||||
assertThat(invocationCount).hasValue(6);
|
||||
|
||||
// RetryListener interactions:
|
||||
repeat(5, () -> {
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception);
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@@ -291,17 +296,17 @@ class RetryTemplateTests {
|
||||
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(FileNotFoundException.class),
|
||||
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class)
|
||||
))
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2));
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
|
||||
.satisfies(throwable -> {
|
||||
repeat(2, () -> {
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
|
||||
});
|
||||
// 3 = 1 initial invocation + 2 retry attempts
|
||||
assertThat(invocationCount).hasValue(3);
|
||||
|
||||
// RetryListener interactions:
|
||||
repeat(2, () -> {
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(
|
||||
eq(retryPolicy), eq(retryable), any(IllegalStateException.class));
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@@ -354,17 +359,17 @@ class RetryTemplateTests {
|
||||
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
|
||||
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class)
|
||||
))
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2));
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
|
||||
.satisfies(throwable -> {
|
||||
repeat(2, () -> {
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
|
||||
});
|
||||
// 3 = 1 initial invocation + 2 retry attempts
|
||||
assertThat(invocationCount).hasValue(3);
|
||||
|
||||
// RetryListener interactions:
|
||||
repeat(2, () -> {
|
||||
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
|
||||
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class));
|
||||
});
|
||||
inOrder.verify(retryListener).onRetryPolicyExhaustion(
|
||||
eq(retryPolicy), eq(retryable), any(CustomFileNotFoundException.class));
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.retry.RetryException;
|
||||
import org.springframework.core.retry.RetryListener;
|
||||
import org.springframework.core.retry.RetryPolicy;
|
||||
import org.springframework.core.retry.Retryable;
|
||||
@@ -83,7 +84,7 @@ class CompositeRetryListenerTests {
|
||||
|
||||
@Test
|
||||
void onRetryPolicyExhaustion() {
|
||||
Exception exception = new Exception();
|
||||
RetryException exception = new RetryException("", new Exception());
|
||||
compositeRetryListener.onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
|
||||
verify(listener1).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
|
||||
Reference in New Issue
Block a user