Retryer.java

package org.codefilarete.tool;

import javax.annotation.Nullable;
import java.time.Duration;

import org.codefilarete.tool.function.ThrowingExecutable;
import org.codefilarete.tool.trace.Chrono;

/**
 * Basic implementation of a retryer processor.
 * User has to override {@link #shouldRetry(Result)} method to decide whether a retry must happen or not.
 * If max attempts have been reached then a {@link RetryException} will be thrown.
 * 
 * @author Guillaume Mary
 */
public abstract class Retryer {
	
	public static final Retryer NO_RETRY = new NoRetryer();
	
	private final int maxRetries;
	private final Duration retryDelay;
	
	/**
	 * Constructor with necessary parameters.
	 *
	 * @param maxRetries maximum attempts before action is not retried
	 * @param retryDelayMilliseconds delay between each attempt, in millisecond
	 */
	public Retryer(int maxRetries, long retryDelayMilliseconds) {
		this(maxRetries, Duration.ofMillis(retryDelayMilliseconds));
	}
	
	/**
	 * Constructor with necessary parameters.
	 * 
	 * @param maxRetries maximum attempts before action is not retried
	 * @param retryDelay delay between each attempt
	 */
	public Retryer(int maxRetries, Duration retryDelay) {
		this.maxRetries = maxRetries;
		this.retryDelay = retryDelay;
	}
	
	public <T, E extends Throwable> T execute(ThrowingExecutable<T, E> delegate, String description) throws E, RetryException {
		Executor<T, E> executor = new Executor<>(delegate, description);
		return executor.execute();
	}
	
	/**
	 * To be implemented to determine if a retry must happen or not.
	 * Parameter is either of type {@link Success} or {@link Failure} depending on code execution. Therefore, implementor may base its algorithm on
	 * "instanceof" checking, and some more "business" one
	 *
	 * @param result a {@link Success} or {@link Failure} instance containing details of code execution
	 * @return true if a retry must be triggered
	 */
	protected abstract boolean shouldRetry(Result result);
	
	public static class RetryException extends RuntimeException {
		
		public RetryException(String message) {
			super(message);
		}
		
		public RetryException(String message, Throwable cause) {
			super(message, cause);
		}
		
		public RetryException(String action, int tryCount, Duration retryDelay, Throwable cause) {
			this("Action \"" + action + "\" has been executed " + tryCount + " times every " + Chrono.format(retryDelay.toMillis()) + " and always failed", cause);
		}
	}
	
	/**
	 * Internal method for execution in order to make "tryCount" thread-safe
	 */
	private final class Executor<R, E extends Throwable> {
		private int tryCount = 0;
		private final ThrowingExecutable<R, E> delegateWithResult;
		private final String description;
		
		private Executor(ThrowingExecutable<R, E> delegateWithResult, String description) {
			this.delegateWithResult = delegateWithResult;
			this.description = description;
		}
		
		@SuppressWarnings("java:S1181" /* Throwable caught voluntarily to let caller handle what he wants */)
		public R execute() throws E, RetryException {
			try {
				tryCount++;
				R result = delegateWithResult.execute();
				if (shouldRetry(new Success<>(result))) {
					return retry(null);
				} else {
					return result;
				}
			} catch (Throwable t) {
				if (shouldRetry(new Failure<>(t))) {
					return retry(t);
				} else {
					throw t;
				}
			}
		}
		
		private R retry(@Nullable Throwable t) throws E, RetryException {
			if (tryCount < maxRetries) {
				waitRetryDelay();
				return execute();
			} else {
				throw new RetryException(description, tryCount, retryDelay, t);
			}
		}
		
		private void waitRetryDelay() {
			try {
				Thread.sleep(retryDelay.toMillis());
			} catch (InterruptedException ie) {
				Thread.currentThread().interrupt();
			}
		}
	}
	
	private static final class NoRetryer extends Retryer {
		public NoRetryer() {
			super(0, Duration.ZERO);
		}
		
		@Override
		protected boolean shouldRetry(Result result) {
			return false;
		}
	}
	
	/**
	 * Super type of {@link Failure} and {@link Success} for {@link Retryer#shouldRetry(Result)} method signature
	 */
	public interface Result {
		
	}
	
	/**
	 * Dedicated holder of exception when an error happen while {@link Retryer} executes caller code
	 *
	 * @param <T> error type
	 */
	public static class Failure<T extends Throwable> implements Result {
		
		private final T error;
		
		public Failure(T error) {
			this.error = error;
		}
		
		public T getError() {
			return error;
		}
	}
	
	/**
	 * Dedicated holder of result when {@link Retryer} successfully executed caller code
	 *
	 * @param <T> error type
	 */
	public static class Success<T> implements Result {
		
		private final T value;
		
		public Success(T value) {
			this.value = value;
		}
		
		public T getValue() {
			return value;
		}
	}
}