All Downloads are FREE. Search and download functionalities are using the official Maven repository.

reactor.util.retry.RetrySpec Maven / Gradle / Ivy

Go to download

Easy Redis Java client and Real-Time Data Platform. Valkey compatible. Sync/Async/RxJava3/Reactive API. Client side caching. Over 50 Redis based Java objects and services: JCache API, Apache Tomcat, Hibernate, Spring, Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Scheduler, RPC

There is a newer version: 3.40.2
Show newest version
/*
 * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package reactor.util.retry;

import java.time.Duration;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import reactor.util.context.ContextView;

/**
 * A simple count-based {@link Retry} strategy with configurable features. Use {@link Retry#max(long)},
 * {@link Retry#maxInARow(long)} or {@link Retry#indefinitely()} to obtain a preconfigured instance to start with.
 * 

* Only errors that match the {@link #filter(Predicate)} are retried (by default all), up to {@link #maxAttempts(long)} times. *

* When the maximum attempt of retries is reached, a runtime exception is propagated downstream which * can be pinpointed with {@link reactor.core.Exceptions#isRetryExhausted(Throwable)}. The cause of * the last attempt's failure is attached as said {@link reactor.core.Exceptions#retryExhausted(String, Throwable) retryExhausted} * exception's cause. This can be customized with {@link #onRetryExhaustedThrow(BiFunction)}. *

* Additionally, to help dealing with bursts of transient errors in a long-lived Flux as if each burst * had its own attempt counter, one can choose to set {@link #transientErrors(boolean)} to {@code true}. * The comparison to {@link #maxAttempts(long)} will then be done with the number of subsequent attempts * that failed without an {@link org.reactivestreams.Subscriber#onNext(Object) onNext} in between. *

* The {@link RetrySpec} is copy-on-write and as such can be stored as a "template" and further configured * by different components without a risk of modifying the original configuration. * * @author Simon Baslé */ public final class RetrySpec extends Retry { static final Duration MAX_BACKOFF = Duration.ofMillis(Long.MAX_VALUE); static final Consumer NO_OP_CONSUMER = rs -> {}; static final BiFunction, Mono> NO_OP_BIFUNCTION = (rs, m) -> m; static final BiFunction RETRY_EXCEPTION_GENERATOR = (builder, rs) -> Exceptions.retryExhausted("Retries exhausted: " + ( builder.isTransientErrors ? rs.totalRetriesInARow() + "/" + builder.maxAttempts + " in a row (" + rs.totalRetries() + " total)" : rs.totalRetries() + "/" + builder.maxAttempts ), rs.failure()); /** * The configured maximum for retry attempts. * * @see #maxAttempts(long) */ public final long maxAttempts; /** * The configured {@link Predicate} to filter which exceptions to retry. * @see #filter(Predicate) * @see #modifyErrorFilter(Function) */ public final Predicate errorFilter; /** * The configured transient error handling flag. * @see #transientErrors(boolean) */ public final boolean isTransientErrors; final Consumer doPreRetry; final Consumer doPostRetry; final BiFunction, Mono> asyncPreRetry; final BiFunction, Mono> asyncPostRetry; final BiFunction retryExhaustedGenerator; /** * Copy constructor. */ RetrySpec(ContextView retryContext, long max, Predicate aThrowablePredicate, boolean isTransientErrors, Consumer doPreRetry, Consumer doPostRetry, BiFunction, Mono> asyncPreRetry, BiFunction, Mono> asyncPostRetry, BiFunction retryExhaustedGenerator) { super(retryContext); this.maxAttempts = max; this.errorFilter = aThrowablePredicate::test; //massaging type this.isTransientErrors = isTransientErrors; this.doPreRetry = doPreRetry; this.doPostRetry = doPostRetry; this.asyncPreRetry = asyncPreRetry; this.asyncPostRetry = asyncPostRetry; this.retryExhaustedGenerator = retryExhaustedGenerator; } /** * Set the user provided {@link Retry#retryContext() context} that can be used to manipulate state on retries. * * @param retryContext a new snapshot of user provided data * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} */ public RetrySpec withRetryContext(ContextView retryContext) { return new RetrySpec( retryContext, this.maxAttempts, this.errorFilter, this.isTransientErrors, this.doPreRetry, this.doPostRetry, this.asyncPreRetry, this.asyncPostRetry, this.retryExhaustedGenerator); } /** * Set the maximum number of retry attempts allowed. 1 meaning "1 retry attempt": * the original subscription plus an extra re-subscription in case of an error, but * no more. * * @param maxAttempts the new retry attempt limit * @return a new copy of the {@link RetrySpec} which can either be further configured or used as a {@link Retry} */ public RetrySpec maxAttempts(long maxAttempts) { return new RetrySpec( this.retryContext, maxAttempts, this.errorFilter, this.isTransientErrors, this.doPreRetry, this.doPostRetry, this.asyncPreRetry, this.asyncPostRetry, this.retryExhaustedGenerator); } /** * Set the {@link Predicate} that will filter which errors can be retried. Exceptions * that don't pass the predicate will be propagated downstream and terminate the retry * sequence. Defaults to allowing retries for all exceptions. * * @param errorFilter the predicate to filter which exceptions can be retried * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} */ public RetrySpec filter(Predicate errorFilter) { return new RetrySpec( this.retryContext, this.maxAttempts, Objects.requireNonNull(errorFilter, "errorFilter"), this.isTransientErrors, this.doPreRetry, this.doPostRetry, this.asyncPreRetry, this.asyncPostRetry, this.retryExhaustedGenerator); } /** * Allows to augment a previously {@link #filter(Predicate) set} {@link Predicate} with * a new condition to allow retries of some exception or not. This can typically be used with * {@link Predicate#and(Predicate)} to combine existing predicate(s) with a new one. *

* For example: *


	 * //given
	 * RetrySpec retryTwiceIllegalArgument = Retry.max(2)
	 *     .filter(e -> e instanceof IllegalArgumentException);
	 *
	 * RetrySpec retryTwiceIllegalArgWithCause = retryTwiceIllegalArgument.modifyErrorFilter(old ->
	 *     old.and(e -> e.getCause() != null));
	 * 
* * @param predicateAdjuster a {@link Function} that returns a new {@link Predicate} given the * currently in place {@link Predicate} (usually deriving from the old predicate). * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} */ public RetrySpec modifyErrorFilter( Function, Predicate> predicateAdjuster) { Objects.requireNonNull(predicateAdjuster, "predicateAdjuster"); Predicate newPredicate = Objects.requireNonNull(predicateAdjuster.apply(this.errorFilter), "predicateAdjuster must return a new predicate"); return new RetrySpec( this.retryContext, this.maxAttempts, newPredicate, this.isTransientErrors, this.doPreRetry, this.doPostRetry, this.asyncPreRetry, this.asyncPostRetry, this.retryExhaustedGenerator); } /** * Add synchronous behavior to be executed before the retry trigger is emitted in * the companion publisher. This should not be blocking, as the companion publisher * might be executing in a shared thread. * * @param doBeforeRetry the synchronous hook to execute before retry trigger is emitted * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} * @see #doBeforeRetryAsync(Function) andDelayRetryWith for an asynchronous version */ public RetrySpec doBeforeRetry( Consumer doBeforeRetry) { return new RetrySpec( this.retryContext, this.maxAttempts, this.errorFilter, this.isTransientErrors, this.doPreRetry.andThen(doBeforeRetry), this.doPostRetry, this.asyncPreRetry, this.asyncPostRetry, this.retryExhaustedGenerator); } /** * Add synchronous behavior to be executed after the retry trigger is emitted in * the companion publisher. This should not be blocking, as the companion publisher * might be publishing events in a shared thread. * * @param doAfterRetry the synchronous hook to execute after retry trigger is started * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} * @see #doAfterRetryAsync(Function) andRetryThen for an asynchronous version */ public RetrySpec doAfterRetry(Consumer doAfterRetry) { return new RetrySpec( this.retryContext, this.maxAttempts, this.errorFilter, this.isTransientErrors, this.doPreRetry, this.doPostRetry.andThen(doAfterRetry), this.asyncPreRetry, this.asyncPostRetry, this.retryExhaustedGenerator); } /** * Add asynchronous behavior to be executed before the current retry trigger in the companion publisher, * thus delaying the resulting retry trigger with the additional {@link Mono}. * * @param doAsyncBeforeRetry the asynchronous hook to execute before original retry trigger is emitted * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} */ public RetrySpec doBeforeRetryAsync( Function> doAsyncBeforeRetry) { return new RetrySpec( this.retryContext, this.maxAttempts, this.errorFilter, this.isTransientErrors, this.doPreRetry, this.doPostRetry, (rs, m) -> asyncPreRetry.apply(rs, m).then(doAsyncBeforeRetry.apply(rs)), this.asyncPostRetry, this.retryExhaustedGenerator); } /** * Add asynchronous behavior to be executed after the current retry trigger in the companion publisher, * thus delaying the resulting retry trigger with the additional {@link Mono}. * * @param doAsyncAfterRetry the asynchronous hook to execute after original retry trigger is emitted * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} */ public RetrySpec doAfterRetryAsync( Function> doAsyncAfterRetry) { return new RetrySpec( this.retryContext, this.maxAttempts, this.errorFilter, this.isTransientErrors, this.doPreRetry, this.doPostRetry, this.asyncPreRetry, (rs, m) -> asyncPostRetry.apply(rs, m).then(doAsyncAfterRetry.apply(rs)), this.retryExhaustedGenerator); } /** * Set the generator for the {@link Exception} to be propagated when the maximum amount of retries * is exhausted. By default, throws an {@link Exceptions#retryExhausted(String, Throwable)} with the * message reflecting the total attempt index, transient attempt index and maximum retry count. * The cause of the last {@link reactor.util.retry.Retry.RetrySignal} is also added as the exception's cause. * * @param retryExhaustedGenerator the {@link Function} that generates the {@link Throwable} for the last * {@link reactor.util.retry.Retry.RetrySignal} * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} */ public RetrySpec onRetryExhaustedThrow(BiFunction retryExhaustedGenerator) { return new RetrySpec( this.retryContext, this.maxAttempts, this.errorFilter, this.isTransientErrors, this.doPreRetry, this.doPostRetry, this.asyncPreRetry, this.asyncPostRetry, Objects.requireNonNull(retryExhaustedGenerator, "retryExhaustedGenerator")); } /** * Set the transient error mode, indicating that the strategy being built should use * {@link reactor.util.retry.Retry.RetrySignal#totalRetriesInARow()} rather than * {@link reactor.util.retry.Retry.RetrySignal#totalRetries()}. * Transient errors are errors that could occur in bursts but are then recovered from by * a retry (with one or more onNext signals) before another error occurs. *

* In the case of a simple count-based retry, this means that the {@link #maxAttempts(long)} * is applied to each burst individually. * * @param isTransientErrors {@code true} to activate transient mode * @return a new copy of the {@link RetrySpec} which can either be further configured or used as {@link Retry} */ public RetrySpec transientErrors(boolean isTransientErrors) { return new RetrySpec( this.retryContext, this.maxAttempts, this.errorFilter, isTransientErrors, this.doPreRetry, this.doPostRetry, this.asyncPreRetry, this.asyncPostRetry, this.retryExhaustedGenerator); } //========== // strategy //========== @Override public Flux generateCompanion(Flux flux) { return Flux.deferContextual(cv -> flux .contextWrite(cv) .concatMap(retryWhenState -> { //capture the state immediately RetrySignal copy = retryWhenState.copy(); Throwable currentFailure = copy.failure(); long iteration = isTransientErrors ? copy.totalRetriesInARow() : copy.totalRetries(); if (currentFailure == null) { return Mono.error(new IllegalStateException("RetryWhenState#failure() not expected to be null")); } else if (!errorFilter.test(currentFailure)) { return Mono.error(currentFailure); } else if (iteration >= maxAttempts) { return Mono.error(retryExhaustedGenerator.apply(this, copy)); } else { return applyHooks(copy, Mono.just(iteration), doPreRetry, doPostRetry, asyncPreRetry, asyncPostRetry, cv); } }) .onErrorStop() ); } //=================== // utility functions //=================== static Mono applyHooks(RetrySignal copyOfSignal, Mono originalCompanion, final Consumer doPreRetry, final Consumer doPostRetry, final BiFunction, Mono> asyncPreRetry, final BiFunction, Mono> asyncPostRetry, final ContextView cv) { if (doPreRetry != NO_OP_CONSUMER) { try { doPreRetry.accept(copyOfSignal); } catch (Throwable e) { return Mono.error(e); } } Mono postRetrySyncMono; if (doPostRetry != NO_OP_CONSUMER) { postRetrySyncMono = Mono.fromRunnable(() -> doPostRetry.accept(copyOfSignal)); } else { postRetrySyncMono = Mono.empty(); } Mono preRetryMono = asyncPreRetry == NO_OP_BIFUNCTION ? Mono.empty() : asyncPreRetry.apply(copyOfSignal, Mono.empty()); Mono postRetryMono = asyncPostRetry != NO_OP_BIFUNCTION ? asyncPostRetry.apply(copyOfSignal, postRetrySyncMono) : postRetrySyncMono; return preRetryMono.then(originalCompanion).flatMap(postRetryMono::thenReturn).contextWrite(cv); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy