privatefinal List<RetryListener> listeners = new LinkedList<>();
publicCompositeRetryListener(){ }
publicCompositeRetryListener(List<RetryListener> listeners){ Assert.notEmpty(listeners, "RetryListener List must not be empty"); this.listeners.addAll(listeners); }
Strategy interface to define a retry policy. Also provides factory methods and a fluent builder API for creating retry policies with common configurations.
privatefinal Set<Class<? extends Throwable>> includes = new LinkedHashSet<>();
privatefinal Set<Class<? extends Throwable>> excludes = new LinkedHashSet<>();
private@Nullable Predicate<Throwable> predicate;
privateBuilder(){ // internal constructor }
public Builder backOff(BackOff backOff){ Assert.notNull(backOff, "BackOff must not be null"); this.backOff = backOff; returnthis; }
public Builder maxAttempts(long maxAttempts){ Assert.isTrue(maxAttempts > 0, "Max attempts must be greater than zero"); this.maxAttempts = maxAttempts; returnthis; }
public Builder jitter(Duration jitter){ Assert.isTrue(!jitter.isNegative(), () -> "Invalid jitter (%dms): must be >= 0.".formatted(jitter.toMillis())); this.jitter = jitter; returnthis; }
public Builder multiplier(double multiplier){ Assert.isTrue(multiplier >= 1, () -> "Invalid multiplier '" + multiplier + "': " + "must be greater than or equal to 1. A multiplier of 1 is equivalent to a fixed delay."); this.multiplier = multiplier; returnthis; }
@SafeVarargs// Making the method final allows us to use @SafeVarargs. @SuppressWarnings("varargs") publicfinal Builder includes(Class<? extends Throwable>... types){ Collections.addAll(this.includes, types); returnthis; }
public Builder includes(Collection<Class<? extends Throwable>> types){ this.includes.addAll(types); returnthis; }
@SafeVarargs// Making the method final allows us to use @SafeVarargs. @SuppressWarnings("varargs") publicfinal Builder excludes(Class<? extends Throwable>... types){ Collections.addAll(this.excludes, types); returnthis; }
public Builder excludes(Collection<Class<? extends Throwable>> types){ this.excludes.addAll(types); returnthis; }
@Override publicbooleanshouldRetry(Throwable throwable){ if (!this.excludes.isEmpty()) { for (Class<? extends Throwable> excludedType : this.excludes) { if (excludedType.isInstance(throwable)) { returnfalse; } } } if (!this.includes.isEmpty()) { boolean included = false; for (Class<? extends Throwable> includedType : this.includes) { if (includedType.isInstance(throwable)) { included = true; break; } } if (!included) { returnfalse; } } returnthis.predicate == null || this.predicate.test(throwable); }
@Override public BackOff getBackOff(){ returnthis.backOff; }
@Override public String toString(){ StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicy[", "]"); if (!this.includes.isEmpty()) { result.add("includes=" + names(this.includes)); } if (!this.excludes.isEmpty()) { result.add("excludes=" + names(this.excludes)); } if (this.predicate != null) { result.add("predicate=" + this.predicate.getClass().getSimpleName()); } result.add("backOff=" + this.backOff); return result.toString(); }
privatestatic String names(Set<Class<? extends Throwable>> types){ StringJoiner result = new StringJoiner(", ", "[", "]"); for (Class<? extends Throwable> type : types) { String name = type.getCanonicalName(); result.add(name != null? name : type.getName()); } return result.toString(); }
RetryTemplate
A basic implementation of RetryOperations that executes and potentially retries a Retryable operation based on a configured RetryPolicy. By default, a retryable operation will be retried at most 3 times with a fixed backoff of 1 second.
private RetryListener retryListener = new RetryListener() {};
publicRetryTemplate(){ }
publicRetryTemplate(RetryPolicy retryPolicy){ Assert.notNull(retryPolicy, "RetryPolicy must not be null"); this.retryPolicy = retryPolicy; }
publicvoidsetRetryPolicy(RetryPolicy retryPolicy){ Assert.notNull(retryPolicy, "Retry policy must not be null"); this.retryPolicy = retryPolicy; }
publicvoidsetRetryListener(RetryListener retryListener){ Assert.notNull(retryListener, "Retry listener must not be null"); this.retryListener = retryListener; }
@Override public <R> @NullableR execute(Retryable<? extends @Nullable R> retryable)throws RetryException { String retryableName = retryable.getName(); // Initial attempt 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; } catch (Throwable initialException) { logger.debug(initialException, () -> "Execution of retryable operation '%s' failed; initiating the retry process" .formatted(retryableName)); // Retry process starts here BackOffExecution backOffExecution = this.retryPolicy.getBackOff().start(); Deque<Throwable> exceptions = new ArrayDeque<>(); exceptions.add(initialException);
Throwable retryException = initialException; while (this.retryPolicy.shouldRetry(retryException)) { try { long duration = backOffExecution.nextBackOff(); if (duration == BackOffExecution.STOP) { break; } logger.debug(() -> "Backing off for %dms after retryable operation '%s'" .formatted(duration, retryableName)); Thread.sleep(duration); } catch (InterruptedException interruptedException) { Thread.currentThread().interrupt(); thrownew RetryException( "Unable to back off for retryable operation '%s'".formatted(retryableName), interruptedException); } logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName)); 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; } catch (Throwable currentAttemptException) { logger.debug(() -> "Retry attempt for operation '%s' failed due to '%s'" .formatted(retryableName, currentAttemptException)); this.retryListener.onRetryFailure(this.retryPolicy, retryable, currentAttemptException); exceptions.add(currentAttemptException); retryException = currentAttemptException; } }