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

de.mklinger.qetcher.client.impl.retry.Retrier Maven / Gradle / Ivy

There is a newer version: 2.0.42.rc
Show newest version
package de.mklinger.qetcher.client.impl.retry;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.mklinger.qetcher.client.QetcherRemoteException;
import de.mklinger.qetcher.client.common.concurrent.Delay;

/**
 * @author Marc Klinger - mklinger[at]mklinger[dot]de
 */
public class Retrier implements Runnable {
	private static final Logger LOG = LoggerFactory.getLogger(Retrier.class);

	private final String id;
	private final Supplier> action;
	private final CompletableFuture cf;
	private final AtomicInteger tries;
	private final AtomicReference error;
	private final Predicate isRetryCandidate;
	private final int maxTries;
	private final long maxWaitTimeMillis;

	public Retrier(final CompletableFuture cf, final Supplier> action, Predicate isRetryCandidate, int maxTries, final long maxWaitTimeMillis) {
		this.id = getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this));
		this.cf = cf;
		this.action = action;
		this.tries = new AtomicInteger(0);
		this.error = new AtomicReference<>();
		this.isRetryCandidate = isRetryCandidate;
		this.maxTries = maxTries;
		this.maxWaitTimeMillis = maxWaitTimeMillis;
	}

	@Override
	public void run() {
		tries.incrementAndGet();

		if (tries.get() > 1 && LOG.isInfoEnabled()) {
			LOG.info("{}: Starting try #{}", id, tries.get());
		}

		action.get()
		.thenAccept(this::complete)
		.exceptionally(this::handleException);
	}

	private void complete(T result) {
		if (tries.get() > 1 && LOG.isInfoEnabled()) {
			LOG.info("{}: Completed successfully after try #{}", id, tries.get());
		}

		cf.complete(result);
	}

	private Void handleException(Throwable newError) {
		final Throwable e = unwrapCompletionException(newError);

		if (!this.error.compareAndSet(null, newRetryFailedException(e))) {
			this.error.get().addSuppressed(e);
		}

		if (tries.get() < maxTries && isRetryCandidate.test(newError)) {
			triggerNextTry(newError);
		} else {
			completeExceptionally();
		}

		return null;
	}

	private void triggerNextTry(Throwable newError) {
		final long waitTimeMillis = getWaitTimeMillis();

		if (LOG.isInfoEnabled()) {
			LOG.info("{}: Triggering try #{} in {} millis after exception: {}",
					id,
					tries.get() + 1,
					waitTimeMillis,
					unwrapCompletionException(newError).toString());
		}

		Delay.delayedExecutor(waitTimeMillis, TimeUnit.MILLISECONDS).execute(this);
	}

	private long getWaitTimeMillis() {
		return Math.min(
				maxWaitTimeMillis,
				50L * tries.get() + 50L + ThreadLocalRandom.current().nextLong(50));
	}

	private void completeExceptionally() {
		// always log final retry errors with stack trace:
		if (tries.get() > 1) {
			LOG.warn("{}: Completing exceptionally after {} tries", id, tries.get(), this.error.get());
		} else {
			LOG.warn("{}: Completing exceptionally after first try (no retries)", id, this.error.get());
		}
		cf.completeExceptionally(this.error.get());
	}

	private RuntimeException newRetryFailedException(final Throwable newError) {
		if (newError instanceof QetcherRemoteException) {
			return new RetryFailedQetcherRemoteException((QetcherRemoteException) newError);
		} else {
			return new RetryFailedGenericException(newError);
		}
	}

	private Throwable unwrapCompletionException(final Throwable error) {
		if (error instanceof CompletionException && error.getCause() != null) {
			return error.getCause();
		} else {
			return error;
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy