Browse Source

Expose RetryException to onRetryPolicyExhaustion (also in the signature)

Includes getRetryPolicy and getRetryListener accessors in RetryTemplate.

Closes gh-35334
pull/35370/head
Juergen Hoeller 7 months ago
parent
commit
f64ff2866a
  1. 9
      spring-core/src/main/java/org/springframework/core/retry/RetryListener.java
  2. 18
      spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java
  3. 6
      spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java
  4. 87
      spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java
  5. 3
      spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java

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

@ -29,6 +29,7 @@ import org.springframework.core.retry.support.CompositeRetryListener;
* *
* @author Mahmoud Ben Hassine * @author Mahmoud Ben Hassine
* @author Sam Brannen * @author Sam Brannen
* @author Juergen Hoeller
* @since 7.0 * @since 7.0
* @see CompositeRetryListener * @see CompositeRetryListener
*/ */
@ -64,9 +65,13 @@ public interface RetryListener {
* Called if the {@link RetryPolicy} is exhausted. * Called if the {@link RetryPolicy} is exhausted.
* @param retryPolicy the {@code RetryPolicy} * @param retryPolicy the {@code RetryPolicy}
* @param retryable the {@code Retryable} operation * @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) {
} }
} }

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

@ -90,6 +90,14 @@ public class RetryTemplate implements RetryOperations {
this.retryPolicy = retryPolicy; 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. * Set the {@link RetryListener} to use.
* <p>If multiple listeners are needed, use a * <p>If multiple listeners are needed, use a
@ -102,6 +110,14 @@ public class RetryTemplate implements RetryOperations {
this.retryListener = retryListener; 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 * 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), "Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
exceptions.removeLast()); exceptions.removeLast());
exceptions.forEach(retryException::addSuppressed); exceptions.forEach(retryException::addSuppressed);
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, lastException); this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, retryException);
throw retryException; throw retryException;
} }
} }

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

@ -21,6 +21,7 @@ import java.util.List;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.core.retry.RetryException;
import org.springframework.core.retry.RetryListener; import org.springframework.core.retry.RetryListener;
import org.springframework.core.retry.RetryPolicy; import org.springframework.core.retry.RetryPolicy;
import org.springframework.core.retry.RetryTemplate; 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}. * <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
* *
* @author Mahmoud Ben Hassine * @author Mahmoud Ben Hassine
* @author Juergen Hoeller
* @since 7.0 * @since 7.0
*/ */
public class CompositeRetryListener implements RetryListener { public class CompositeRetryListener implements RetryListener {
@ -82,8 +84,8 @@ public class CompositeRetryListener implements RetryListener {
} }
@Override @Override
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) { public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, throwable)); this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, exception));
} }
} }

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

@ -68,6 +68,12 @@ class RetryTemplateTests {
retryTemplate.setRetryListener(retryListener); retryTemplate.setRetryListener(retryListener);
} }
@Test
void checkRetryTemplateConfiguration() {
assertThat(retryTemplate.getRetryPolicy()).isSameAs(retryPolicy);
assertThat(retryTemplate.getRetryListener()).isSameAs(retryListener);
}
@Test @Test
void retryWithImmediateSuccess() throws Exception { void retryWithImmediateSuccess() throws Exception {
AtomicInteger invocationCount = new AtomicInteger(); AtomicInteger invocationCount = new AtomicInteger();
@ -99,10 +105,9 @@ class RetryTemplateTests {
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution") .withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
.withCause(exception) .withCause(exception)
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty()) .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); verifyNoMoreInteractions(retryListener);
} }
@ -122,10 +127,9 @@ class RetryTemplateTests {
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution") .withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
.withCause(exception) .withCause(exception)
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty()) .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); verifyNoMoreInteractions(retryListener);
} }
@ -145,10 +149,9 @@ class RetryTemplateTests {
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution") .withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
.withCause(exception) .withCause(exception)
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty()) .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); verifyNoMoreInteractions(retryListener);
} }
@ -194,18 +197,19 @@ class RetryTemplateTests {
assertThatExceptionOfType(RetryException.class) assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryable)) .isThrownBy(() -> retryTemplate.execute(retryable))
.withMessage("Retry policy for operation 'test' exhausted; aborting execution") .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 // 4 = 1 initial invocation + 3 retry attempts
assertThat(invocationCount).hasValue(4); 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); verifyNoMoreInteractions(retryListener);
} }
@ -240,16 +244,17 @@ class RetryTemplateTests {
assertThatExceptionOfType(RetryException.class) assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryable)) .isThrownBy(() -> retryTemplate.execute(retryable))
.withMessage("Retry policy for operation 'always fails' exhausted; aborting execution") .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 // 6 = 1 initial invocation + 5 retry attempts
assertThat(invocationCount).hasValue(6); 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); verifyNoMoreInteractions(retryListener);
} }
@ -291,17 +296,17 @@ class RetryTemplateTests {
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(FileNotFoundException.class), suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(FileNotFoundException.class),
suppressed2 -> assertThat(suppressed2).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(Exception.class));
});
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
});
// 3 = 1 initial invocation + 2 retry attempts // 3 = 1 initial invocation + 2 retry attempts
assertThat(invocationCount).hasValue(3); 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); verifyNoMoreInteractions(retryListener);
} }
@ -354,17 +359,17 @@ class RetryTemplateTests {
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class), suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
suppressed2 -> assertThat(suppressed2).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 // 3 = 1 initial invocation + 2 retry attempts
assertThat(invocationCount).hasValue(3); 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); verifyNoMoreInteractions(retryListener);
} }

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

@ -21,6 +21,7 @@ import java.util.List;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.retry.RetryException;
import org.springframework.core.retry.RetryListener; import org.springframework.core.retry.RetryListener;
import org.springframework.core.retry.RetryPolicy; import org.springframework.core.retry.RetryPolicy;
import org.springframework.core.retry.Retryable; import org.springframework.core.retry.Retryable;
@ -83,7 +84,7 @@ class CompositeRetryListenerTests {
@Test @Test
void onRetryPolicyExhaustion() { void onRetryPolicyExhaustion() {
Exception exception = new Exception(); RetryException exception = new RetryException("", new Exception());
compositeRetryListener.onRetryPolicyExhaustion(retryPolicy, retryable, exception); compositeRetryListener.onRetryPolicyExhaustion(retryPolicy, retryable, exception);
verify(listener1).onRetryPolicyExhaustion(retryPolicy, retryable, exception); verify(listener1).onRetryPolicyExhaustion(retryPolicy, retryable, exception);

Loading…
Cancel
Save