package io.smallrye.mutiny.groups; import static io.smallrye.mutiny.helpers.ParameterValidation.nonNull; import static io.smallrye.mutiny.helpers.ParameterValidation.validate; import java.time.Duration; import java.util.function.Function; import java.util.function.Predicate; import org.reactivestreams.Publisher; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.helpers.ExponentialBackoff; import io.smallrye.mutiny.helpers.ParameterValidation; import io.smallrye.mutiny.infrastructure.Infrastructure; import io.smallrye.mutiny.operators.multi.MultiRetryOp; import io.smallrye.mutiny.operators.multi.MultiRetryWhenOp; public class MultiRetry<T> { private final Multi<T> upstream; private Duration initialBackOff = Duration.ofSeconds(1); private Duration maxBackoff = ExponentialBackoff.MAX_BACKOFF; private double jitter = ExponentialBackoff.DEFAULT_JITTER; private boolean backOffConfigured = false; public MultiRetry(Multi<T> upstream) { this.upstream = nonNull(upstream, "upstream"); } /** * Produces a {@link Multi} resubscribing to the current {@link Multi} until it gets a items followed * by a completion events. * <p> * On every failure, it re-subscribes, indefinitely. * * @return the {@link Multi} */ public Multi<T> indefinitely() { return atMost(Long.MAX_VALUE); } /** * Produces a {@link Multi} resubscribing to the current {@link Multi} at most {@code numberOfAttempts} time, * until it gets items followed by the completion event. On every failure, it re-subscribes. * <p> * If the number of attempt is reached, the last failure is propagated. * * @param numberOfAttempts the number of attempt, must be greater than zero * @return a new {@link Multi} retrying at most {@code numberOfAttempts} times to subscribe to the current * {@link Multi} until it gets an item. When the number of attempt is reached, the last failure is propagated. */ public Multi<T> atMost(long numberOfAttempts) { ParameterValidation.positive(numberOfAttempts, "numberOfAttempts"); if (backOffConfigured) { Function<Multi<Throwable>, Publisher<Long>> whenStreamFactory = ExponentialBackoff .randomExponentialBackoffFunction(numberOfAttempts, initialBackOff, maxBackoff, jitter, Infrastructure.getDefaultWorkerPool()); return Infrastructure.onMultiCreation( new MultiRetryWhenOp<>(upstream, whenStreamFactory)); } else { return Infrastructure.onMultiCreation(new MultiRetryOp<>(upstream, numberOfAttempts)); } } /** * Produces a {@link Multi} resubscribing to the current {@link Multi} until {@code expireAt} time or * until it gets items followed by the completion event. On every failure, it re-subscribes. * <p> * If expiration time is passed, the last failure is propagated. * Backoff must be configured. * * @param expireAt absolute time in millis that specifies when to give up * @return a new {@link Multi} retrying to subscribe to the current * {@link Multi} until it gets an item or until expiration {@code expireAt}. When the expiration is reached, * the last failure is propagated. * * @throws IllegalArgumentException if back off not configured, */ public Multi<T> expireAt(long expireAt) { if (!backOffConfigured) { throw new IllegalArgumentException( "Invalid retry configuration, `expiresAt/expiresIn` must be used with a back-off configuration"); } Function<Multi<Throwable>, Publisher<Long>> whenStreamFactory = ExponentialBackoff .randomExponentialBackoffFunctionExpireAt(expireAt, initialBackOff, maxBackoff, jitter, Infrastructure.getDefaultWorkerPool()); return Infrastructure.onMultiCreation( new MultiRetryWhenOp<>(upstream, whenStreamFactory)); } /** * Produces a {@link Multi} resubscribing to the current {@link Multi} until {@code expireIn} time or * until it gets items followed by the completion event. On every failure, it re-subscribes. * <p> * If expiration time is passed, the last failure is propagated. * Backoff must be configured. * * @param expireIn relative time in millis that specifies when to give up * @return a new {@link Multi} retrying to subscribe to the current * {@link Multi} until it gets an item or until expiration {@code expireIn}. When the expiration is reached, * the last failure is propagated. * * @throws IllegalArgumentException if back off not configured, */ public Multi<T> expireIn(long expireIn) { return expireAt(System.currentTimeMillis() + expireIn); } /** * Produces a {@code Multi} resubscribing to the current {@link Multi} until the given predicate returns {@code false}. * The predicate is called with the failure emitted by the current {@link Multi}. * * @param predicate the predicate that determines if a re-subscription may happen in case of a specific failure, * must not be {@code null}. If the predicate returns {@code true} for the given failure, a * re-subscription is attempted. * @return the new {@code Multi} instance */ public Multi<T> until(Predicate<? super Throwable> predicate) { ParameterValidation.nonNull(predicate, "predicate"); if (backOffConfigured) { throw new IllegalArgumentException( "Invalid retry configuration, `until` cannot be used with a back-off configuration"); } Function<Multi<Throwable>, Publisher<Long>> whenStreamFactory = stream -> stream.onItem() .produceUni(failure -> Uni.createFrom().<Long> emitter(emitter -> { try { if (predicate.test(failure)) { emitter.complete(1L); } else { emitter.fail(failure); } } catch (Throwable ex) { emitter.fail(ex); } })) .concatenate(); return Infrastructure.onMultiCreation(new MultiRetryWhenOp<>(upstream, whenStreamFactory)); } /** * Produces a {@link Multi} resubscribing to the current {@link Multi} when the {@link Publisher} produced by the * given method emits an item. * As {@link #atMost(long)}, on every failure, it re-subscribes. However, a <em>delay</em> is introduced before * re-subscribing. The re-subscription happens when the produced streams emits an item. If this stream fails, * the downstream gets a failure. It the streams completes, the downstream completes. * * @param whenStreamFactory the function used to produce the stream triggering the re-subscription, must not be * {@code null}, must not produce {@code null} * @return a new {@link Multi} retrying re-subscribing to the current {@link Multi} when the companion stream, * produced by {@code whenStreamFactory} emits an item. */ public Multi<T> when(Function<Multi<Throwable>, ? extends Publisher<?>> whenStreamFactory) { if (backOffConfigured) { throw new IllegalArgumentException( "Invalid retry configuration, `when` cannot be used with a back-off configuration"); } return Infrastructure.onMultiCreation(new MultiRetryWhenOp<>(upstream, whenStreamFactory)); } /** * Configures a back-off delay between to attempt to re-subscribe. A random factor (jitter) is applied to increase * the delay when several failures happen. * * @param initialBackOff the initial back-off duration, must not be {@code null}, must not be negative. * @return this object to configure the retry policy. */ public MultiRetry<T> withBackOff(Duration initialBackOff) { return withBackOff(initialBackOff, ExponentialBackoff.MAX_BACKOFF); } /** * Configures a back-off delay between to attempt to re-subscribe. A random factor (jitter) is applied to increase * he delay when several failures happen. The max delays is {@code maxBackOff}. * * @param initialBackOff the initial back-off duration, must not be {@code null}, must not be negative. * @param maxBackOff the max back-off duration, must not be {@code null}, must not be negative. * @return this object to configure the retry policy. */ public MultiRetry<T> withBackOff(Duration initialBackOff, Duration maxBackOff) { this.backOffConfigured = true; this.initialBackOff = validate(initialBackOff, "initialBackOff"); this.maxBackoff = validate(maxBackOff, "maxBackOff"); return this; } /** * Configures the random factor when using back-off. By default, it's set to 0.5. * * @param jitter the jitter. Must be in [0.0, 1.0] * @return this object to configure the retry policy. */ public MultiRetry<T> withJitter(double jitter) { if (jitter < 0 || jitter > 1.0) { throw new IllegalArgumentException("Invalid `jitter`, the value must be in [0.0, 1.0]"); } this.backOffConfigured = true; this.jitter = jitter; return this; } }