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

io.github.resilience4j.retry.internal.RetryImpl Maven / Gradle / Ivy

Go to download

Resilience4j is a lightweight, easy-to-use fault tolerance library designed for Java8 and functional programming

There is a newer version: 2.2.0
Show newest version
/*
 *
 *  Copyright 2016 Robert Winkler
 *
 *  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
 *
 *         http://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 io.github.resilience4j.retry.internal;

import io.github.resilience4j.core.EventConsumer;
import io.github.resilience4j.core.EventProcessor;
import io.github.resilience4j.core.IntervalBiFunction;
import io.github.resilience4j.core.functions.CheckedConsumer;
import io.github.resilience4j.core.functions.Either;
import io.github.resilience4j.core.lang.Nullable;
import io.github.resilience4j.retry.MaxRetriesExceeded;
import io.github.resilience4j.retry.MaxRetriesExceededException;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.event.*;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class RetryImpl implements Retry {


    /*package*/ static CheckedConsumer sleepFunction = Thread::sleep;
    private final Metrics metrics;
    private final RetryEventProcessor eventProcessor;
    @Nullable
    private final Predicate resultPredicate;

    @Nullable
    private final BiConsumer consumeResultBeforeRetryAttempt;

    private final String name;
    private final RetryConfig config;
    private final Map tags;

    private final int maxAttempts;
    private final boolean failAfterMaxAttempts;
    private final IntervalBiFunction intervalBiFunction;
    private final Predicate exceptionPredicate;
    private final LongAdder succeededAfterRetryCounter;
    private final LongAdder failedAfterRetryCounter;
    private final LongAdder succeededWithoutRetryCounter;
    private final LongAdder failedWithoutRetryCounter;

    public RetryImpl(String name, RetryConfig config) {
        this(name, config, Collections.emptyMap());
    }

    public RetryImpl(String name, RetryConfig config, Map tags) {
        this.name = name;
        this.config = config;
        this.tags = tags;
        this.maxAttempts = config.getMaxAttempts();
        this.failAfterMaxAttempts = config.isFailAfterMaxAttempts();
        this.intervalBiFunction = config.getIntervalBiFunction();
        this.exceptionPredicate = config.getExceptionPredicate();
        this.resultPredicate = config.getResultPredicate();
        this.consumeResultBeforeRetryAttempt = config.getConsumeResultBeforeRetryAttempt();
        this.metrics = this.new RetryMetrics();
        this.eventProcessor = new RetryEventProcessor();
        succeededAfterRetryCounter = new LongAdder();
        failedAfterRetryCounter = new LongAdder();
        succeededWithoutRetryCounter = new LongAdder();
        failedWithoutRetryCounter = new LongAdder();
    }

    public static void setSleepFunction(CheckedConsumer sleepFunction) {
        RetryImpl.sleepFunction = sleepFunction;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getName() {
        return name;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Context context() {
        return new ContextImpl();
    }

    @Override
    @SuppressWarnings("unchecked")
    public AsyncContext asyncContext() {
        return new AsyncContextImpl();
    }

    @Override
    public RetryConfig getRetryConfig() {
        return config;
    }

    public Map getTags() {
        return tags;
    }

    private void publishRetryEvent(Supplier event) {
        if (eventProcessor.hasConsumers()) {
            eventProcessor.consumeEvent(event.get());
        }
    }

    @Override
    public EventPublisher getEventPublisher() {
        return eventProcessor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Metrics getMetrics() {
        return this.metrics;
    }

    public final class ContextImpl implements Retry.Context {

        private final AtomicInteger numOfAttempts = new AtomicInteger(0);
        private final AtomicReference lastException = new AtomicReference<>();
        private final AtomicReference lastRuntimeException = new AtomicReference<>();

        private ContextImpl() {
        }

        @Override
        public void onComplete() {
            int currentNumOfAttempts = numOfAttempts.get();
            if (currentNumOfAttempts > 0 && currentNumOfAttempts < maxAttempts) {
                succeededAfterRetryCounter.increment();
                Throwable throwable = Optional.ofNullable(lastException.get())
                    .orElse(lastRuntimeException.get());
                publishRetryEvent(
                    () -> new RetryOnSuccessEvent(getName(), currentNumOfAttempts, throwable));
            } else {
                if (currentNumOfAttempts >= maxAttempts) {
                    failedAfterRetryCounter.increment();
                    Throwable throwable = Optional.ofNullable(lastException.get())
                        .or(() -> Optional.ofNullable(lastRuntimeException.get()))
                        .filter(p -> !failAfterMaxAttempts)
                        .orElse(new MaxRetriesExceeded(
                            "max retries is reached out for the result predicate check"
                        ));
                    publishRetryEvent(() -> new RetryOnErrorEvent(name, currentNumOfAttempts, throwable));

                    if (failAfterMaxAttempts) {
                        throw MaxRetriesExceededException.createMaxRetriesExceededException(RetryImpl.this);
                    }
                } else {
                    succeededWithoutRetryCounter.increment();
                }
            }
        }

        @Override
        public boolean onResult(T result) {
            if (null != resultPredicate && resultPredicate.test(result)) {
                int currentNumOfAttempts = numOfAttempts.incrementAndGet();
                if (currentNumOfAttempts >= maxAttempts) {
                    return false;
                } else {
                    if(consumeResultBeforeRetryAttempt != null){
                        consumeResultBeforeRetryAttempt.accept(currentNumOfAttempts, result);
                    }
                    waitIntervalAfterFailure(currentNumOfAttempts, Either.right(result));
                    return true;
                }
            }
            return false;
        }

        @Override
        public void onError(Exception exception) throws Exception {
            if (exceptionPredicate.test(exception)) {
                lastException.set(exception);
                throwOrSleepAfterException();
            } else {
                failedWithoutRetryCounter.increment();
                publishRetryEvent(() -> new RetryOnIgnoredErrorEvent(getName(), exception));
                throw exception;
            }
        }

        @Override
        public void onRuntimeError(RuntimeException runtimeException) {
            if (exceptionPredicate.test(runtimeException)) {
                lastRuntimeException.set(runtimeException);
                throwOrSleepAfterRuntimeException();
            } else {
                failedWithoutRetryCounter.increment();
                publishRetryEvent(() -> new RetryOnIgnoredErrorEvent(getName(), runtimeException));
                throw runtimeException;
            }
        }

        private void throwOrSleepAfterException() throws Exception {
            int currentNumOfAttempts = numOfAttempts.incrementAndGet();
            Exception throwable = lastException.get();
            if (currentNumOfAttempts >= maxAttempts) {
                failedAfterRetryCounter.increment();
                publishRetryEvent(
                    () -> new RetryOnErrorEvent(getName(), currentNumOfAttempts, throwable));
                throw throwable;
            } else {
                waitIntervalAfterFailure(currentNumOfAttempts, Either.left(throwable));
            }
        }

        private void throwOrSleepAfterRuntimeException() {
            int currentNumOfAttempts = numOfAttempts.incrementAndGet();
            RuntimeException throwable = lastRuntimeException.get();
            if (currentNumOfAttempts >= maxAttempts) {
                failedAfterRetryCounter.increment();
                publishRetryEvent(
                    () -> new RetryOnErrorEvent(getName(), currentNumOfAttempts, throwable));
                throw throwable;
            } else {
                waitIntervalAfterFailure(currentNumOfAttempts, Either.left(throwable));
            }
        }

        private void waitIntervalAfterFailure(int currentNumOfAttempts, Either either) {
            // wait interval until the next attempt should start
            long interval = intervalBiFunction.apply(numOfAttempts.get(), either);
            publishRetryEvent(
                () -> new RetryOnRetryEvent(getName(), currentNumOfAttempts, either.swap().getOrNull(), interval));
            try {
                sleepFunction.accept(interval);
            } catch (Exception ex) {
                throw lastRuntimeException.get();
            } catch (Throwable e) {
                throw lastRuntimeException.get();
            }
        }

    }

    public final class AsyncContextImpl implements Retry.AsyncContext {

        private final AtomicInteger numOfAttempts = new AtomicInteger(0);
        private final AtomicReference lastException = new AtomicReference<>();

        @Override
        public void onComplete() {
            int currentNumOfAttempts = numOfAttempts.get();
            if (currentNumOfAttempts > 0 && currentNumOfAttempts < maxAttempts) {
                succeededAfterRetryCounter.increment();
                publishRetryEvent(
                    () -> new RetryOnSuccessEvent(name, currentNumOfAttempts, lastException.get()));
            } else {
                if (currentNumOfAttempts >= maxAttempts) {
                    failedAfterRetryCounter.increment();
                    Throwable throwable = Optional.ofNullable(lastException.get())
                        .filter(p -> !failAfterMaxAttempts)
                        .orElse(new MaxRetriesExceeded(
                            "max retries is reached out for the result predicate check"
                        ));

                    publishRetryEvent(() -> new RetryOnErrorEvent(name, currentNumOfAttempts, throwable));

                    if (failAfterMaxAttempts) {
                        throw MaxRetriesExceededException.createMaxRetriesExceededException(RetryImpl.this);
                    }
                } else {
                    succeededWithoutRetryCounter.increment();

                }
            }
        }

        @Override
        public long onError(Throwable throwable) {
            // Handle the case if the completable future throw CompletionException wrapping the original exception
            // where original exception is the one to retry not the CompletionException.
            if (throwable instanceof CompletionException || throwable instanceof ExecutionException) {
                Throwable cause = throwable.getCause();
                return handleThrowable(cause);
            } else {
                return handleThrowable(throwable);
            }

        }

        private long handleThrowable(Throwable throwable) {
            if (!exceptionPredicate.test(throwable)) {
                failedWithoutRetryCounter.increment();
                publishRetryEvent(() -> new RetryOnIgnoredErrorEvent(getName(), throwable));
                return -1;
            }
            return handleOnError(throwable);
        }

        private long handleOnError(Throwable throwable) {
            lastException.set(throwable);
            int attempt = numOfAttempts.incrementAndGet();
            if (attempt >= maxAttempts) {
                failedAfterRetryCounter.increment();
                publishRetryEvent(() -> new RetryOnErrorEvent(name, attempt, throwable));
                return -1;
            }

            long interval = intervalBiFunction.apply(attempt, Either.left(throwable));
            publishRetryEvent(() -> new RetryOnRetryEvent(getName(), attempt, throwable, interval));
            return interval;
        }

        @Override
        public long onResult(T result) {
            if (null != resultPredicate && resultPredicate.test(result)) {
                int attempt = numOfAttempts.incrementAndGet();
                if (attempt >= maxAttempts) {
                    if(consumeResultBeforeRetryAttempt != null){
                        consumeResultBeforeRetryAttempt.accept(attempt, result);
                    }
                    return -1;
                }
                return intervalBiFunction.apply(attempt, Either.right(result));
            } else {
                return -1;
            }
        }
    }

    public final class RetryMetrics implements Metrics {

        private RetryMetrics() {
        }

        @Override
        public long getNumberOfSuccessfulCallsWithoutRetryAttempt() {
            return succeededWithoutRetryCounter.longValue();
        }

        @Override
        public long getNumberOfFailedCallsWithoutRetryAttempt() {
            return failedWithoutRetryCounter.longValue();
        }

        @Override
        public long getNumberOfSuccessfulCallsWithRetryAttempt() {
            return succeededAfterRetryCounter.longValue();
        }

        @Override
        public long getNumberOfFailedCallsWithRetryAttempt() {
            return failedAfterRetryCounter.longValue();
        }
    }

    private class RetryEventProcessor extends EventProcessor implements
        EventConsumer, EventPublisher {

        @Override
        public void consumeEvent(RetryEvent event) {
            super.processEvent(event);
        }

        @Override
        public EventPublisher onRetry(EventConsumer onRetryEventConsumer) {
            registerConsumer(RetryOnRetryEvent.class.getName(), onRetryEventConsumer);
            return this;
        }

        @Override
        public EventPublisher onSuccess(EventConsumer onSuccessEventConsumer) {
            registerConsumer(RetryOnSuccessEvent.class.getName(), onSuccessEventConsumer);
            return this;
        }

        @Override
        public EventPublisher onError(EventConsumer onErrorEventConsumer) {
            registerConsumer(RetryOnErrorEvent.class.getName(), onErrorEventConsumer);
            return this;
        }

        @Override
        public EventPublisher onIgnoredError(
            EventConsumer onIgnoredErrorEventConsumer) {
            registerConsumer(RetryOnIgnoredErrorEvent.class.getName(), onIgnoredErrorEventConsumer);
            return this;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy