|
|
|
@ -32,11 +32,11 @@ import org.springframework.util.backoff.FixedBackOff; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* A basic implementation of {@link RetryOperations} that invokes and potentially |
|
|
|
* A basic implementation of {@link RetryOperations} that invokes and potentially |
|
|
|
* retries a {@link RetryCallback} based on a configured {@link RetryPolicy} and |
|
|
|
* retries a {@link Retryable} operation based on a configured {@link RetryPolicy} |
|
|
|
* {@link BackOff} policy. |
|
|
|
* and {@link BackOff} policy. |
|
|
|
* |
|
|
|
* |
|
|
|
* <p>By default, a callback will be invoked at most 3 times with a fixed backoff |
|
|
|
* <p>By default, a retryable operation will be invoked at most 3 times with a |
|
|
|
* of 1 second. |
|
|
|
* fixed backoff of 1 second. |
|
|
|
* |
|
|
|
* |
|
|
|
* <p>A {@link RetryListener} can be {@linkplain #setRetryListener(RetryListener) |
|
|
|
* <p>A {@link RetryListener} can be {@linkplain #setRetryListener(RetryListener) |
|
|
|
* registered} to intercept and inject behavior during key retry phases (before a |
|
|
|
* registered} to intercept and inject behavior during key retry phases (before a |
|
|
|
@ -52,7 +52,7 @@ import org.springframework.util.backoff.FixedBackOff; |
|
|
|
* @see RetryPolicy |
|
|
|
* @see RetryPolicy |
|
|
|
* @see BackOff |
|
|
|
* @see BackOff |
|
|
|
* @see RetryListener |
|
|
|
* @see RetryListener |
|
|
|
* @see RetryCallback |
|
|
|
* @see Retryable |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public class RetryTemplate implements RetryOperations { |
|
|
|
public class RetryTemplate implements RetryOperations { |
|
|
|
|
|
|
|
|
|
|
|
@ -128,29 +128,30 @@ public class RetryTemplate implements RetryOperations { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Execute the supplied {@link RetryCallback} according to the configured |
|
|
|
* Execute the supplied {@link Retryable} according to the configured retry |
|
|
|
* retry and backoff policies. |
|
|
|
* and backoff policies. |
|
|
|
* <p>If the callback succeeds, its result will be returned. Otherwise, a |
|
|
|
* <p>If the {@code Retryable} succeeds, its result will be returned. Otherwise, |
|
|
|
* {@link RetryException} will be thrown to the caller. |
|
|
|
* a {@link RetryException} will be thrown to the caller. |
|
|
|
* @param retryCallback the callback to call initially and retry if needed |
|
|
|
* @param retryable the {@code Retryable} to execute and retry if needed |
|
|
|
* @param <R> the type of the result |
|
|
|
* @param <R> the type of the result |
|
|
|
* @return the result of the callback, if any |
|
|
|
* @return the result of the {@code Retryable}, if any |
|
|
|
* @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions |
|
|
|
* @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions |
|
|
|
* encountered during retry attempts are available as suppressed exceptions |
|
|
|
* encountered during retry attempts are available as suppressed exceptions |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
@Override |
|
|
|
@Override |
|
|
|
public <R extends @Nullable Object> R execute(RetryCallback<R> retryCallback) throws RetryException { |
|
|
|
public <R extends @Nullable Object> R execute(Retryable<R> retryable) throws RetryException { |
|
|
|
String callbackName = retryCallback.getName(); |
|
|
|
String retryableName = retryable.getName(); |
|
|
|
// Initial attempt
|
|
|
|
// Initial attempt
|
|
|
|
try { |
|
|
|
try { |
|
|
|
logger.debug(() -> "Preparing to execute callback '" + callbackName + "'"); |
|
|
|
logger.debug(() -> "Preparing to execute retryable operation '%s'".formatted(retryableName)); |
|
|
|
R result = retryCallback.run(); |
|
|
|
R result = retryable.run(); |
|
|
|
logger.debug(() -> "Callback '" + callbackName + "' completed successfully"); |
|
|
|
logger.debug(() -> "Retryable operation '%s' completed successfully".formatted(retryableName)); |
|
|
|
return result; |
|
|
|
return result; |
|
|
|
} |
|
|
|
} |
|
|
|
catch (Throwable initialException) { |
|
|
|
catch (Throwable initialException) { |
|
|
|
logger.debug(initialException, |
|
|
|
logger.debug(initialException, |
|
|
|
() -> "Execution of callback '" + callbackName + "' failed; initiating the retry process"); |
|
|
|
() -> "Execution of retryable operation '%s' failed; initiating the retry process" |
|
|
|
|
|
|
|
.formatted(retryableName)); |
|
|
|
// Retry process starts here
|
|
|
|
// Retry process starts here
|
|
|
|
RetryExecution retryExecution = this.retryPolicy.start(); |
|
|
|
RetryExecution retryExecution = this.retryPolicy.start(); |
|
|
|
BackOffExecution backOffExecution = this.backOffPolicy.start(); |
|
|
|
BackOffExecution backOffExecution = this.backOffPolicy.start(); |
|
|
|
@ -158,25 +159,27 @@ public class RetryTemplate implements RetryOperations { |
|
|
|
|
|
|
|
|
|
|
|
Throwable retryException = initialException; |
|
|
|
Throwable retryException = initialException; |
|
|
|
while (retryExecution.shouldRetry(retryException)) { |
|
|
|
while (retryExecution.shouldRetry(retryException)) { |
|
|
|
logger.debug(() -> "Preparing to retry callback '" + callbackName + "'"); |
|
|
|
logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName)); |
|
|
|
try { |
|
|
|
try { |
|
|
|
this.retryListener.beforeRetry(retryExecution); |
|
|
|
this.retryListener.beforeRetry(retryExecution); |
|
|
|
R result = retryCallback.run(); |
|
|
|
R result = retryable.run(); |
|
|
|
this.retryListener.onRetrySuccess(retryExecution, result); |
|
|
|
this.retryListener.onRetrySuccess(retryExecution, result); |
|
|
|
logger.debug(() -> "Callback '" + callbackName + "' completed successfully after retry"); |
|
|
|
logger.debug(() -> "Retryable operation '%s' completed successfully after retry" |
|
|
|
|
|
|
|
.formatted(retryableName)); |
|
|
|
return result; |
|
|
|
return result; |
|
|
|
} |
|
|
|
} |
|
|
|
catch (Throwable currentAttemptException) { |
|
|
|
catch (Throwable currentAttemptException) { |
|
|
|
this.retryListener.onRetryFailure(retryExecution, currentAttemptException); |
|
|
|
this.retryListener.onRetryFailure(retryExecution, currentAttemptException); |
|
|
|
try { |
|
|
|
try { |
|
|
|
long duration = backOffExecution.nextBackOff(); |
|
|
|
long duration = backOffExecution.nextBackOff(); |
|
|
|
logger.debug(() -> "Retry callback '" + callbackName + "' failed due to '" + |
|
|
|
logger.debug(() -> "Retryable operation '%s' failed due to '%s'; backing off for %dms" |
|
|
|
currentAttemptException.getMessage() + "'; backing off for " + duration + "ms"); |
|
|
|
.formatted(retryableName, currentAttemptException.getMessage(), duration)); |
|
|
|
Thread.sleep(duration); |
|
|
|
Thread.sleep(duration); |
|
|
|
} |
|
|
|
} |
|
|
|
catch (InterruptedException interruptedException) { |
|
|
|
catch (InterruptedException interruptedException) { |
|
|
|
Thread.currentThread().interrupt(); |
|
|
|
Thread.currentThread().interrupt(); |
|
|
|
throw new RetryException("Unable to back off for retry callback '" + callbackName + "'", |
|
|
|
throw new RetryException( |
|
|
|
|
|
|
|
"Unable to back off for retryable operation '%s'".formatted(retryableName), |
|
|
|
interruptedException); |
|
|
|
interruptedException); |
|
|
|
} |
|
|
|
} |
|
|
|
suppressedExceptions.add(currentAttemptException); |
|
|
|
suppressedExceptions.add(currentAttemptException); |
|
|
|
@ -185,8 +188,9 @@ public class RetryTemplate implements RetryOperations { |
|
|
|
} |
|
|
|
} |
|
|
|
// The RetryPolicy has exhausted at this point, so we throw a RetryException with the
|
|
|
|
// The RetryPolicy has exhausted at this point, so we throw a RetryException with the
|
|
|
|
// initial exception as the cause and remaining exceptions as suppressed exceptions.
|
|
|
|
// initial exception as the cause and remaining exceptions as suppressed exceptions.
|
|
|
|
RetryException finalException = new RetryException("Retry policy for callback '" + callbackName + |
|
|
|
RetryException finalException = new RetryException( |
|
|
|
"' exhausted; aborting execution", initialException); |
|
|
|
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName), |
|
|
|
|
|
|
|
initialException); |
|
|
|
suppressedExceptions.forEach(finalException::addSuppressed); |
|
|
|
suppressedExceptions.forEach(finalException::addSuppressed); |
|
|
|
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException); |
|
|
|
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException); |
|
|
|
throw finalException; |
|
|
|
throw finalException; |
|
|
|
|