diff --git a/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java b/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java index 367e9508..9d198c3b 100644 --- a/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java +++ b/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2022 the original author or authors. + * Copyright 2006-2023 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. @@ -37,11 +37,21 @@ import org.springframework.util.Assert; /** - * Fluent API to configure new instance of RetryTemplate. For detailed description of each - * builder method - see it's doc. + * Builder that provides a fluent API to configure new instances of {@link RetryTemplate}. * *

- * Examples:

{@code
+ * By default, the builder configures a {@link BinaryExceptionClassifier} that acts upon
+ * {@link Exception} and its subclasses without traversing causes, a
+ * {@link NoBackOffPolicy} and a {@link MaxAttemptsRetryPolicy} that attempts actions
+ * {@link MaxAttemptsRetryPolicy#DEFAULT_MAX_ATTEMPTS} times.
+ *
+ * 

+ * The builder is not thread-safe. + * + *

+ * Example usage: + * + *

{@code
  * RetryTemplate.builder()
  *      .maxAttempts(10)
  *      .exponentialBackoff(100, 2, 10000)
@@ -61,26 +71,6 @@
  *      .build();
  * }
* - *

- * The builder provides the following defaults: - *

- * - *

- * The builder supports only widely used properties of {@link RetryTemplate}. More - * specific properties can be configured directly (after building). - * - *

- * Not thread safe. Building should be performed in a single thread. Also, there is no - * guarantee that all constructors of all fields are thread safe in-depth (means employing - * only volatile and final writes), so, in concurrent environment, it is recommended to - * ensure presence of happens-before between publication and any usage. (e.g. publication - * via volatile write, or other safe publication technique) - * * @author Aleksandr Shamukov * @author Artem Bilan * @author Kim In Hoi @@ -100,13 +90,12 @@ public class RetryTemplateBuilder { /* ---------------- Configure retry policy -------------- */ /** - * Limits maximum number of attempts to provided value. - *

- * Invocation of this method does not discard default exception classification rule, - * that is "retry only on {@link Exception} and it's subclasses". - * @param maxAttempts includes initial attempt and all retries. E.g: maxAttempts = 3 - * means one initial attempt and two retries. + * Attempt an action no more than {@code maxAttempts} times. + * @param maxAttempts how many times an action should be attempted. A value of 3 would + * result in an initial attempt and two retries. * @return this + * @throws IllegalArgumentException if {@code maxAttempts} is 0 or less, or if another + * retry policy has already been selected. * @see MaxAttemptsRetryPolicy */ public RetryTemplateBuilder maxAttempts(int maxAttempts) { @@ -117,12 +106,11 @@ public RetryTemplateBuilder maxAttempts(int maxAttempts) { } /** - * Allows retry if there is no more than {@code timeout} millis since first attempt. - *

- * Invocation of this method does not discard default exception classification rule, - * that is "retry only on {@link Exception} and it's subclasses". - * @param timeout whole execution timeout in milliseconds + * Retry until {@code timeout} has passed since the initial attempt. + * @param timeout timeout in milliseconds * @return this + * @throws IllegalArgumentException if timeout is {@literal <=} 0 or if another retry + * policy has already been selected. * @see TimeoutRetryPolicy * @deprecated Use {@link #withTimeout(long)} instead. */ @@ -132,13 +120,13 @@ public RetryTemplateBuilder withinMillis(long timeout) { } /** - * Retry until {@code timeout} has passed since the initial attempt. + * Retry until {@code timeoutMillis} has passed since the initial attempt. * @param timeoutMillis timeout in milliseconds * @return this - * @see TimeoutRetryPolicy * @throws IllegalArgumentException if timeout is {@literal <=} 0 or if another retry - * policy has already been configured. + * policy has already been selected. * @since 2.0.2 + * @see TimeoutRetryPolicy */ public RetryTemplateBuilder withTimeout(long timeoutMillis) { Assert.isTrue(timeoutMillis > 0, "timeoutMillis should be greater than 0"); @@ -151,10 +139,10 @@ public RetryTemplateBuilder withTimeout(long timeoutMillis) { * Retry until {@code timeout} has passed since the initial attempt. * @param timeout duration for how long retries should be attempted * @return this - * @see TimeoutRetryPolicy * @throws IllegalArgumentException if timeout is {@code null} or 0, or if another - * retry policy has already been configured. + * retry policy has already been selected. * @since 2.0.2 + * @see TimeoutRetryPolicy */ public RetryTemplateBuilder withTimeout(Duration timeout) { Assert.notNull(timeout, "timeout must not be null"); @@ -162,12 +150,10 @@ public RetryTemplateBuilder withTimeout(Duration timeout) { } /** - * Allows infinite retry, do not limit attempts by number or time. - *

- * Invocation of this method does not discard default exception classification rule, - * that is "retry only on {@link Exception} and it's subclasses". + * Retry actions infinitely. * @return this - * @see TimeoutRetryPolicy + * @throws IllegalArgumentException if another retry policy has already been selected. + * @see AlwaysRetryPolicy */ public RetryTemplateBuilder infiniteRetry() { Assert.isNull(this.baseRetryPolicy, "You have already selected another retry policy"); @@ -176,13 +162,12 @@ public RetryTemplateBuilder infiniteRetry() { } /** - * If flexibility of this builder is not enough for you, you can provide your own - * {@link RetryPolicy} via this method. - *

- * Invocation of this method does not discard default exception classification rule, - * that is "retry only on {@link Exception} and it's subclasses". - * @param policy will be directly set to resulting {@link RetryTemplate} + * Use the provided {@link RetryPolicy}. + * @param policy {@link RetryPolicy} to use * @return this + * @throws IllegalArgumentException if another backoff policy has already been + * selected, if any argument is {@code null}, or if another retry policy has already + * been selected. */ public RetryTemplateBuilder customPolicy(RetryPolicy policy) { Assert.notNull(policy, "Policy should not be null"); @@ -194,15 +179,18 @@ public RetryTemplateBuilder customPolicy(RetryPolicy policy) { /* ---------------- Configure backoff policy -------------- */ /** - * Use exponential backoff policy. The formula of backoff period: + * Use an exponential backoff policy. The formula for the backoff period is: *

* {@code currentInterval = Math.min(initialInterval * Math.pow(multiplier, retryNum), maxInterval)} *

- * (for first attempt retryNum = 0) - * @param initialInterval in milliseconds - * @param multiplier see the formula above - * @param maxInterval in milliseconds + * For the first attempt, {@code retryNum = 0}. + * @param initialInterval initial sleep duration in milliseconds + * @param multiplier backoff interval multiplier + * @param maxInterval maximum backoff duration in milliseconds * @return this + * @throws IllegalArgumentException if another backoff policy has already been + * selected, if {@code initialInterval} is {@literal <} 1, if {@code multiplier} is + * {@literal <=} 1, or if {@code maxInterval} {@literal <=} {@code initialInterval}. * @see ExponentialBackOffPolicy */ public RetryTemplateBuilder exponentialBackoff(long initialInterval, double multiplier, long maxInterval) { @@ -210,19 +198,21 @@ public RetryTemplateBuilder exponentialBackoff(long initialInterval, double mult } /** - * Use exponential backoff policy. The formula of backoff period: + * Use an exponential backoff policy. The formula for the backoff period is: *

* {@code currentInterval = Math.min(initialInterval * Math.pow(multiplier, retryNum), maxInterval)} *

- * (for first attempt retryNum = 0) + * For the first attempt, {@code retryNum = 0}. * @param initialInterval initial sleep duration * @param multiplier backoff interval multiplier * @param maxInterval maximum backoff duration * @return this - * @see ExponentialBackOffPolicy - * @throws IllegalArgumentException if initialInterval is {@code null}, multiplier is - * {@literal <=} 1, or if maxInterval is {@code null} + * @throws IllegalArgumentException if another backoff policy has already been + * selected, if {@code initialInterval} is {@code null} or less than 1 millisecond, + * multiplier is {@literal <=} 1, or if {@code maxInterval} is {@code null} or + * {@literal <=} {@code initialInterval}. * @since 2.0.2 + * @see ExponentialBackOffPolicy */ public RetryTemplateBuilder exponentialBackoff(Duration initialInterval, double multiplier, Duration maxInterval) { Assert.notNull(initialInterval, "initialInterval must not be null"); @@ -231,17 +221,21 @@ public RetryTemplateBuilder exponentialBackoff(Duration initialInterval, double } /** - * Use exponential backoff policy. The formula of backoff period (without randomness): + * Use an exponential backoff policy. The formula for the backoff period is (without + * randomness): *

* {@code currentInterval = Math.min(initialInterval * Math.pow(multiplier, retryNum), maxInterval)} *

- * (for first attempt retryNum = 0) - * @param initialInterval in milliseconds - * @param multiplier see the formula above - * @param maxInterval in milliseconds - * @param withRandom adds some randomness to backoff intervals. For details, see - * {@link ExponentialRandomBackOffPolicy} + * For the first attempt, {@code retryNum = 0}. + * @param initialInterval initial sleep duration in milliseconds + * @param multiplier backoff interval multiplier + * @param maxInterval maximum backoff duration in milliseconds + * @param withRandom whether to use a {@link ExponentialRandomBackOffPolicy} (if + * {@code true}) or not * @return this + * @throws IllegalArgumentException if another backoff policy has already been + * selected, if {@code initialInterval} is {@literal <} 1, if {@code multiplier} is + * {@literal <=} 1, or if {@code maxInterval} {@literal <=} {@code initialInterval}. * @see ExponentialBackOffPolicy * @see ExponentialRandomBackOffPolicy */ @@ -261,22 +255,25 @@ public RetryTemplateBuilder exponentialBackoff(long initialInterval, double mult } /** - * Use exponential backoff policy. The formula of backoff period (without randomness): + * Use an exponential backoff policy. The formula for the backoff period is (without + * randomness): *

* {@code currentInterval = Math.min(initialInterval * Math.pow(multiplier, retryNum), maxInterval)} *

- * (for first attempt retryNum = 0) + * For the first attempt, {@code retryNum = 0}. * @param initialInterval initial sleep duration * @param multiplier backoff interval multiplier * @param maxInterval maximum backoff duration - * @param withRandom adds some randomness to backoff intervals. For details, see - * {@link ExponentialRandomBackOffPolicy} + * @param withRandom whether to use a {@link ExponentialRandomBackOffPolicy} (if + * {@code true}) or not * @return this + * @throws IllegalArgumentException if another backoff policy has already been + * selected, if {@code initialInterval} is {@code null} or less than 1 millisecond, if + * {@code multiplier} is {@literal <=} 1, or if {@code maxInterval} is {@code null} or + * {@literal <=} {@code initialInterval}. + * @since 2.0.2 * @see ExponentialBackOffPolicy * @see ExponentialRandomBackOffPolicy - * @throws IllegalArgumentException if initialInterval is {@code null}, multiplier is - * {@literal <=} 1, or maxInterval is {@code null} - * @since 2.0.2 */ public RetryTemplateBuilder exponentialBackoff(Duration initialInterval, double multiplier, Duration maxInterval, boolean withRandom) { @@ -286,9 +283,11 @@ public RetryTemplateBuilder exponentialBackoff(Duration initialInterval, double } /** - * Perform each retry after fixed amount of time. + * Perform each retry after a fixed amount of time. * @param interval fixed interval in milliseconds * @return this + * @throws IllegalArgumentException if another backoff policy has already been + * selected, or if {@code interval} is {@literal <} 1. * @see FixedBackOffPolicy */ public RetryTemplateBuilder fixedBackoff(long interval) { @@ -304,10 +303,10 @@ public RetryTemplateBuilder fixedBackoff(long interval) { * Perform each retry after fixed amount of time. * @param interval fixed backoff duration * @return this - * @see FixedBackOffPolicy * @throws IllegalArgumentException if another backoff policy has already been - * configured, interval is {@code null} or less than 1 millisecond + * selected, or if {@code interval} is {@code null} or less than 1 millisecond * @since 2.0.2 + * @see FixedBackOffPolicy */ public RetryTemplateBuilder fixedBackoff(Duration interval) { Assert.notNull(interval, "interval must not be null"); @@ -319,10 +318,13 @@ public RetryTemplateBuilder fixedBackoff(Duration interval) { } /** - * Use {@link UniformRandomBackOffPolicy}, see it's doc for details. - * @param minInterval in milliseconds - * @param maxInterval in milliseconds + * Use {@link UniformRandomBackOffPolicy}. + * @param minInterval minimal interval in milliseconds + * @param maxInterval maximal interval in milliseconds * @return this + * @throws IllegalArgumentException if another backoff policy has already been + * selected, if {@code minInterval} is {@literal <} 1, {@code maxInterval} is + * {@literal <} 1, or if {@code maxInterval} is {@literal <=} {@code minInterval}. * @see UniformRandomBackOffPolicy */ public RetryTemplateBuilder uniformRandomBackoff(long minInterval, long maxInterval) { @@ -342,11 +344,12 @@ public RetryTemplateBuilder uniformRandomBackoff(long minInterval, long maxInter * @param minInterval minimum backoff duration * @param maxInterval maximum backoff duration * @return this - * @see UniformRandomBackOffPolicy - * @throws IllegalArgumentException if minInterval is {@code null} or {@literal <} 1, - * maxInterval is {@code null} or {@literal <} 1, maxInterval {@literal >=} - * minInterval or if another backoff policy has already been configured. + * @throws IllegalArgumentException if another backoff policy has already been + * selected, if {@code minInterval} is {@code null} or {@literal <} 1, + * {@code maxInterval} is {@code null} or less than 1 millisecond, or if + * {@code maxInterval} {@literal <=} {@code minInterval}. * @since 2.0.2 + * @see UniformRandomBackOffPolicy */ public RetryTemplateBuilder uniformRandomBackoff(Duration minInterval, Duration maxInterval) { Assert.notNull(minInterval, "minInterval must not be null"); @@ -355,8 +358,10 @@ public RetryTemplateBuilder uniformRandomBackoff(Duration minInterval, Duration } /** - * Do not pause between attempts, retry immediately. + * Retry immediately without pausing between attempts. * @return this + * @throws IllegalArgumentException if another backoff policy has already been + * selected. * @see NoBackOffPolicy */ public RetryTemplateBuilder noBackoff() { @@ -366,9 +371,11 @@ public RetryTemplateBuilder noBackoff() { } /** - * You can provide your own {@link BackOffPolicy} via this method. - * @param backOffPolicy will be directly set to resulting {@link RetryTemplate} + * Use the provided {@link BackOffPolicy}. + * @param backOffPolicy {@link BackOffPolicy} to use * @return this + * @throws IllegalArgumentException if {@code backOffPolicy} is null or if another + * backoff policy has already been selected. */ public RetryTemplateBuilder customBackoff(BackOffPolicy backOffPolicy) { Assert.isNull(this.backOffPolicy, "You have already selected backoff policy"); @@ -380,16 +387,15 @@ public RetryTemplateBuilder customBackoff(BackOffPolicy backOffPolicy) { /* ---------------- Configure exception classifier -------------- */ /** - * Add a throwable to the while list of retryable exceptions. + * Add the {@link Throwable} and its subclasses to the list of exceptions that cause a + * retry. *

- * Warn: touching this method drops default {@code retryOn(Exception.class)} and you - * should configure whole classifier from scratch. - *

- * You should select the way you want to configure exception classifier: white list or - * black list. If you choose white list - use this method, if black - use - * {@link #notRetryOn(Class)} or {@link #notRetryOn(List)} - * @param throwable to be retryable (with it's subclasses) + * {@code retryOn()} and {@code notRetryOn()} are mutually exclusive. Trying to use + * both causes an {@link IllegalArgumentException}. + * @param throwable that causes a retry * @return this + * @throws IllegalArgumentException if {@code throwable} is {@code null}, or if + * {@link #notRetryOn} has already been used. * @see BinaryExceptionClassifierBuilder#retryOn * @see BinaryExceptionClassifier */ @@ -399,16 +405,15 @@ public RetryTemplateBuilder retryOn(Class throwable) { } /** - * Add a throwable to the black list of retryable exceptions. - *

- * Warn: touching this method drops default {@code retryOn(Exception.class)} and you - * should configure whole classifier from scratch. + * Add the {@link Throwable} and its subclasses to the list of exceptions that do not + * cause a retry. *

- * You should select the way you want to configure exception classifier: white list or - * black list. If you choose black list - use this method, if white - use - * {@link #retryOn(Class)} or {@link #retryOn(List)} - * @param throwable to be not retryable (with it's subclasses) + * {@code retryOn()} and {@code notRetryOn()} are mutually exclusive. Trying to use + * both causes an {@link IllegalArgumentException}. + * @param throwable that does not cause a retry * @return this + * @throws IllegalArgumentException if {@code throwable} is {@code null}, or if + * {@link #retryOn} has already been used. * @see BinaryExceptionClassifierBuilder#notRetryOn * @see BinaryExceptionClassifier */ @@ -418,16 +423,14 @@ public RetryTemplateBuilder notRetryOn(Class throwable) { } /** - * Add all throwables to the while list of retryable exceptions. - *

- * Warn: touching this method drops default {@code retryOn(Exception.class)} and you - * should configure whole classifier from scratch. + * Add the list of {@link Throwable} classes and their subclasses to the list of + * exceptions that cause a retry. *

- * You should select the way you want to configure exception classifier: white list or - * black list. If you choose white list - use this method, if black - use - * {@link #notRetryOn(Class)} or {@link #notRetryOn(List)} - * @param throwables to be retryable (with it's subclasses) + * {@code retryOn()} and {@code notRetryOn()} are mutually exclusive. Trying to use + * both causes an {@link IllegalArgumentException}. + * @param throwables that cause a retry * @return this + * @throws IllegalArgumentException if {@link #notRetryOn} has already been used. * @since 1.3.2 * @see BinaryExceptionClassifierBuilder#retryOn * @see BinaryExceptionClassifier @@ -440,16 +443,14 @@ public RetryTemplateBuilder retryOn(List> throwables) } /** - * Add all throwables to the black list of retryable exceptions. + * Add the list of {@link Throwable} classes and their subclasses to the list of + * exceptions that do not cause a retry. *

- * Warn: touching this method drops default {@code retryOn(Exception.class)} and you - * should configure whole classifier from scratch. - *

- * You should select the way you want to configure exception classifier: white list or - * black list. If you choose black list - use this method, if white - use - * {@link #retryOn(Class)} or {@link #retryOn(List)} - * @param throwables to be not retryable (with it's subclasses) + * {@code retryOn()} and {@code notRetryOn()} are mutually exclusive. Trying to use + * both causes an {@link IllegalArgumentException}. + * @param throwables that do not cause a retry * @return this + * @throws IllegalArgumentException if {@link #retryOn} has already been used. * @since 1.3.2 * @see BinaryExceptionClassifierBuilder#notRetryOn * @see BinaryExceptionClassifier @@ -462,12 +463,20 @@ public RetryTemplateBuilder notRetryOn(List> throwabl } /** - * Suppose throwing a {@code new MyLogicException(new IOException())}. This template - * will not retry on it:

{@code
+	 * Enable examining exception causes for {@link Throwable} instances that cause a
+	 * retry.
+	 * 

+ * Suppose the following {@code RetryTemplate}:

{@code
 	 * RetryTemplate.builder()
 	 *          .retryOn(IOException.class)
 	 *          .build()
-	 * }
but this will retry:
{@code
+	 * }
+ *

+ * It will act on code that throws an {@link java.io.IOException}, for example, + * {@code throw new IOException()}. But it will not retry the action if the + * {@link java.io.IOException} is wrapped in another exception, for example, + * {@code throw new MyException(new IOException())}. However, this + * {@link RetryTemplate} will:

{@code
 	 * RetryTemplate.builder()
 	 *          .retryOn(IOException.class)
 	 *          .traversingCauses()
@@ -484,10 +493,10 @@ public RetryTemplateBuilder traversingCauses() {
 	/* ---------------- Add listeners -------------- */
 
 	/**
-	 * Appends provided {@code listener} to {@link RetryTemplate}'s listener list.
+	 * Append the provided {@code listener} to {@link RetryTemplate}'s list of listeners.
 	 * @param listener to be appended
 	 * @return this
-	 * @see RetryTemplate
+	 * @throws IllegalArgumentException if {@code listener} is {@code null}.
 	 * @see RetryListener
 	 */
 	public RetryTemplateBuilder withListener(RetryListener listener) {
@@ -497,10 +506,10 @@ public RetryTemplateBuilder withListener(RetryListener listener) {
 	}
 
 	/**
-	 * Appends all provided {@code listeners} to {@link RetryTemplate}'s listener list.
+	 * Append all provided {@code listeners} to {@link RetryTemplate}'s list of listeners.
 	 * @param listeners to be appended
 	 * @return this
-	 * @see RetryTemplate
+	 * @throws IllegalArgumentException if any of the {@code listeners} is {@code null}.
 	 * @see RetryListener
 	 */
 	public RetryTemplateBuilder withListeners(List listeners) {
@@ -514,12 +523,15 @@ public RetryTemplateBuilder withListeners(List listeners) {
 	/* ---------------- Building -------------- */
 
 	/**
-	 * Finish configuration and build resulting {@link RetryTemplate}. For default
-	 * behaviour and concurrency note see class-level doc of {@link RetryTemplateBuilder}.
-	 * The {@code retryPolicy} of the returned {@link RetryTemplate} is always an instance
-	 * of {@link CompositeRetryPolicy}, that consists of one base policy, and of
-	 * {@link BinaryExceptionClassifierRetryPolicy}. The motivation is: whatever base
-	 * policy we use, exception classification is extremely recommended.
+	 * Build a new {@link RetryTemplate}.
+	 * 

+ * Supports building multiple instances. However, it is not possible to change the + * configuration between multiple {@code build()} calls. + *

+ * The {@code retryPolicy} of the returned {@link RetryTemplate} is always a + * {@link CompositeRetryPolicy} that consists of one base policy and of + * {@link BinaryExceptionClassifierRetryPolicy} to enable exception classification + * regardless of the base policy. * @return new instance of {@link RetryTemplate} */ public RetryTemplate build() { diff --git a/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java b/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java index 58799ec4..f0984179 100644 --- a/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java +++ b/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java @@ -308,7 +308,7 @@ public void testValidateInitAndMax() { } @Test - public void testValidateMeaninglessMultipier() { + public void testValidateMeaninglessMultiplier() { assertThatIllegalArgumentException() .isThrownBy(() -> RetryTemplate.builder().exponentialBackoff(100, 1, 200).build()); }