RetryBuilder.java

package fr.sii.ogham.core.builder.retry;

import java.util.function.Predicate;
import java.util.function.UnaryOperator;

import fr.sii.ogham.core.async.Awaiter;
import fr.sii.ogham.core.async.ThreadSleepAwaiter;
import fr.sii.ogham.core.builder.Builder;
import fr.sii.ogham.core.builder.context.BuildContext;
import fr.sii.ogham.core.builder.env.EnvironmentBuilder;
import fr.sii.ogham.core.condition.Condition;
import fr.sii.ogham.core.fluent.AbstractParent;
import fr.sii.ogham.core.retry.ExponentialDelayRetry;
import fr.sii.ogham.core.retry.FixedDelayRetry;
import fr.sii.ogham.core.retry.FixedIntervalRetry;
import fr.sii.ogham.core.retry.PerExecutionDelayRetry;
import fr.sii.ogham.core.retry.RetryExecutor;
import fr.sii.ogham.core.retry.RetryStrategy;
import fr.sii.ogham.core.retry.RetryStrategyProvider;
import fr.sii.ogham.core.retry.SimpleRetryExecutor;

/**
 * Configures retry handling.
 * 
 * Ogham provides several strategies to handle retry:
 * 
 * <ul>
 * <li>{@link FixedDelayRetry}: wait for a fixed delay after the last
 * failure</li>
 * <li>{@link FixedIntervalRetry}: wait for a fixed delay between executions (do
 * not wait for the end of the action)</li>
 * <li>{@link ExponentialDelayRetry}: start with a delay, the next delay will be
 * doubled on so on</li>
 * <li>{@link PerExecutionDelayRetry}: provide a custom delay for each
 * execution</li>
 * </ul>
 * 
 * <p>
 * The {@link RetryExecutor} instance may be {@code null} if nothing has been
 * configured.
 * </p>
 * 
 * @author Aurélien Baudet
 *
 * @param <P>
 *            the type of the parent builder (when calling {@link #and()}
 *            method)
 */
public class RetryBuilder<P> extends AbstractParent<P> implements Builder<RetryExecutor> {
	private final BuildContext buildContext;
	private FixedDelayBuilder<RetryBuilder<P>> fixedDelay;
	private ExponentialDelayBuilder<RetryBuilder<P>> exponentialDelay;
	private PerExecutionDelayBuilder<RetryBuilder<P>> perExecutionDelay;
	private FixedIntervalBuilder<RetryBuilder<P>> fixedInterval;
	private Awaiter awaiter;
	private RetryExecutor executor;
	private RetryExecutorFactory executorFactory;
	private Predicate<Throwable> retryable;

	/**
	 * Initializes the builder with a parent builder. The parent builder is used
	 * when calling {@link #and()} method. The {@link EnvironmentBuilder} is
	 * used to evaluate properties when {@link #build()} method is called.
	 * 
	 * @param parent
	 *            the parent builder
	 * @param buildContext
	 *            for registering instances and property evaluation
	 */
	public RetryBuilder(P parent, BuildContext buildContext) {
		super(parent);
		this.buildContext = buildContext;
	}

	/**
	 * Retry several times with a fixed delay between each try until the maximum
	 * attempts is reached.
	 * 
	 * For example:
	 * 
	 * <pre>
	 * .fixedDelay()
	 *    .delay(500)
	 *    .maxRetries(5)
	 * </pre>
	 * 
	 * Means that a retry will be attempted every 500ms until 5 attempts are
	 * reached (inclusive). For example, you want to connect to an external
	 * system at t1=0 and the connection timeout (100ms) is triggered at
	 * t2=100ms. Using this retry will provide the following behavior:
	 * 
	 * <ul>
	 * <li>0: connect</li>
	 * <li>100: timeout</li>
	 * <li>600: connect</li>
	 * <li>700: timeout</li>
	 * <li>1200: connect</li>
	 * <li>1300: timeout</li>
	 * <li>1800: connect</li>
	 * <li>1900: timeout</li>
	 * <li>2400: connect</li>
	 * <li>2500: timeout</li>
	 * <li>3000: connect</li>
	 * <li>3100: timeout</li>
	 * <li>fail</li>
	 * </ul>
	 * 
	 * @return the builder to configure retry delay and maximum attempts
	 */
	public FixedDelayBuilder<RetryBuilder<P>> fixedDelay() {
		if (fixedDelay == null) {
			fixedDelay = new FixedDelayBuilder<>(this, buildContext);
		}
		return fixedDelay;
	}

	/**
	 * Retry several times with a delay that is doubled between each try until
	 * the maximum attempts is reached.
	 * 
	 * For example:
	 * 
	 * <pre>
	 * .exponentialDelay()
	 *    .initialDelay(500)
	 *    .maxRetries(5)
	 * </pre>
	 * 
	 * Means that a retry will be attempted every 500ms until 5 attempts are
	 * reached (inclusive). For example, you want to connect to an external
	 * system at t1=0 and the connection timeout (100ms) is triggered at
	 * t2=100ms. Using this retry will provide the following behavior:
	 * 
	 * <ul>
	 * <li>0: connect</li>
	 * <li>100: timeout</li>
	 * <li>600: connect</li>
	 * <li>700: timeout</li>
	 * <li>1700: connect</li>
	 * <li>1800: timeout</li>
	 * <li>3800: connect</li>
	 * <li>3900: timeout</li>
	 * <li>7900: connect</li>
	 * <li>8000: timeout</li>
	 * <li>16000: connect</li>
	 * <li>16100: timeout</li>
	 * <li>fail</li>
	 * </ul>
	 * 
	 * @return the builder to configure initial delay and maximum attempts
	 */
	public ExponentialDelayBuilder<RetryBuilder<P>> exponentialDelay() {
		if (exponentialDelay == null) {
			exponentialDelay = new ExponentialDelayBuilder<>(this, buildContext);
		}
		return exponentialDelay;
	}

	/**
	 * Retry several times with a fixed delay to wait after the last execution
	 * failure until the maximum attempts is reached. A specific delay is used
	 * for each execution. If there are more attempts than the configured
	 * delays, the last delays is used for remaining attempts.
	 * 
	 * 
	 * For example:
	 * 
	 * <pre>
	 * .perExecutionDelay()
	 *    .delays(500, 750, 1800)
	 *    .maxRetries(5)
	 * </pre>
	 * 
	 * Means that a retry will be attempted with specified delays until 5
	 * attempts are reached (inclusive). For example, you want to connect to an
	 * external system at t1=0 and the connection timeout (100ms) is triggered
	 * at t2=100ms. Using this retry will provide the following behavior:
	 * 
	 * <ul>
	 * <li>0: connect</li>
	 * <li>100: timeout</li>
	 * <li>600: connect</li>
	 * <li>700: timeout</li>
	 * <li>1450: connect</li>
	 * <li>1550: timeout</li>
	 * <li>3350: connect</li>
	 * <li>3450: timeout</li>
	 * <li>5250: connect</li>
	 * <li>5350: timeout</li>
	 * <li>7150: connect</li>
	 * <li>7250: timeout</li>
	 * <li>fail</li>
	 * </ul>
	 * 
	 * @return the builder to configure retry delay and maximum attempts
	 */
	public PerExecutionDelayBuilder<RetryBuilder<P>> perExecutionDelay() {
		if (perExecutionDelay == null) {
			perExecutionDelay = new PerExecutionDelayBuilder<>(this, buildContext);
		}
		return perExecutionDelay;
	}

	/**
	 * Retry several times with a fixed delay between each try (no matter how
	 * long the execution of the action lasts) until the maximum attempts is
	 * reached. The next execution date is based on the execution start date of
	 * the first execution.
	 * 
	 * For example:
	 * 
	 * <pre>
	 * .fixedInterval()
	 *    .interval(500)
	 *    .maxRetries(5)
	 * </pre>
	 * 
	 * Means that a retry will be attempted every 500ms until 5 attempts are
	 * reached (inclusive). For example, you want to connect to an external
	 * system at t1=0 and the connection timeout (100ms) is triggered at
	 * t2=100ms. Using this retry will provide the following behavior:
	 * 
	 * 
	 * <ul>
	 * <li>0: connect</li>
	 * <li>100: timeout</li>
	 * <li>500: connect</li>
	 * <li>600: timeout</li>
	 * <li>1000: connect</li>
	 * <li>1100: timeout</li>
	 * <li>1500: connect</li>
	 * <li>1600: timeout</li>
	 * <li>2000: connect</li>
	 * <li>2100: timeout</li>
	 * <li>2500: connect</li>
	 * <li>2600: timeout</li>
	 * <li>fail</li>
	 * </ul>
	 * 
	 * 
	 * <strong>NOTE:</strong> The provided date doesn't take the duration of the
	 * execution in account. If an execution takes 1s to execute while retry
	 * delay is set to 500ms, there may have several executions in parallel.
	 * However, this totally depends on the {@link RetryExecutor}
	 * implementation. For example {@link SimpleRetryExecutor} won't run several
	 * executions in parallel. In this case, it will execute the action as soon
	 * as the previous one has failed therefore the delay may not be complied.
	 * 
	 * @return the builder to configure retry delay and maximum attempts
	 */
	public FixedIntervalBuilder<RetryBuilder<P>> fixedInterval() {
		if (fixedInterval == null) {
			fixedInterval = new FixedIntervalBuilder<>(this, buildContext);
		}
		return fixedInterval;
	}

	/**
	 * Change implementation used to wait for some delay between retries.
	 * 
	 * By default, {@link ThreadSleepAwaiter} is used (internally uses
	 * {@link Thread#sleep(long)} to wait for some point in time.
	 * 
	 * @param impl
	 *            the custom implementation
	 * @return this instance for fluent chaining
	 */
	public RetryBuilder<P> awaiter(Awaiter impl) {
		this.awaiter = impl;
		return this;
	}

	/**
	 * Use custom executor instead of default one ({@link SimpleRetryExecutor}).
	 * 
	 * <p>
	 * <strong>NOTE:</strong> Using custom executor doesn't take retry
	 * strategies into account. If you want to benefit from
	 * {@link RetryStrategy}s, use {@link #executor(RetryExecutorFactory)}
	 * instead.
	 * 
	 * <p>
	 * If {@link #executor(RetryExecutorFactory)} is also called, only the
	 * executor defined by this method is used and the factory is not used.
	 * 
	 * <p>
	 * If this method is called several times, only the last value is used.
	 * 
	 * <p>
	 * If you call with {@code null}, it removes any previous custom executor.
	 * 
	 * @param executor
	 *            the executor to use
	 * @return this builder for fluent chaining
	 */
	public RetryBuilder<P> executor(RetryExecutor executor) {
		this.executor = executor;
		return this;
	}

	/**
	 * Use a factory to create an instance of {@link RetryExecutor} while
	 * benefiting from configured {@link RetryStrategy}.
	 * 
	 * <p>
	 * The factory will receive the ready to use {@link RetryStrategyProvider}
	 * and the built {@link Awaiter}.
	 * 
	 * <p>
	 * If {@link #executor(RetryExecutor)} is also configured, the factory is
	 * not used.
	 * 
	 * <p>
	 * If this method is called several times, only the last factory is used.
	 * 
	 * <p>
	 * If you call with {@code null}, it removes any previous configured
	 * factory.
	 * 
	 * @param factory
	 *            the factory used to create the {@link RetryExecutor} instance
	 * @return this instance for fluent chaining
	 */
	public RetryBuilder<P> executor(RetryExecutorFactory factory) {
		this.executorFactory = factory;
		return this;
	}

	/**
	 * A predicate that checks if the raised error should allow another retry or
	 * not. This is useful when an error is raised and the error is severe so it
	 * should stop immediately.
	 * 
	 * <p>
	 * The predicate returns {@code true} if the exception is not fatal and a
	 * retry may be attempted. It returns {@code false} if the exception is
	 * fatal and no more attempt should be executed and retry must stop
	 * immediately.
	 * 
	 * <p>
	 * If {@code null} is passed to this method, it removes any previously
	 * defined predicate. Therefore the default predicate is used: all
	 * {@link Exception}s are considered retryable but not {@link Error}s.
	 * 
	 * @param retryable
	 *            the predicate that returns true if the exception is not fatal
	 *            and a retry can be attempted
	 * @return this instance for fluent chaining
	 */
	public RetryBuilder<P> retryable(Predicate<Throwable> retryable) {
		this.retryable = retryable;
		return this;
	}

	/**
	 * Combine the current configured predicate with another.
	 * 
	 * <p>
	 * This may be useful when an implementation wants to add additional checks
	 * to the ones configured by Ogham core or other implementations.
	 * 
	 * <p>
	 * If no predicate is currently configured, a predicate that accepts
	 * everything is provided.
	 * 
	 * <p>
	 * <strong>NOTE:</strong> This is advanced usage mostly for developers that
	 * extend Ogham with new implementations.
	 * 
	 * @param merger
	 *            a function used to merge predicates
	 * @return this instance for fluent chaining
	 */
	public RetryBuilder<P> retryable(UnaryOperator<Predicate<Throwable>> merger) {
		retryable = merger.apply(retryable == null ? (error -> true) : retryable);
		return this;
	}

	/**
	 * A condition that checks if the raised error should allow another retry or
	 * not. This is useful when an error is raised and the error is severe so it
	 * should stop immediately.
	 * 
	 * <p>
	 * The condition returns {@code true} if the exception is not fatal and a
	 * retry may be attempted. It returns {@code false} if the exception is
	 * fatal and no more attempt should be executed and retry must stop
	 * immediately.
	 * 
	 * <p>
	 * If {@code null} is passed to this method, it removes any previously
	 * defined condition. Therefore the default condition is used: all
	 * {@link Exception}s are considered retryable but not {@link Error}s.
	 * 
	 * <p>
	 * This method internally calls {@link #retryable(Predicate)}.
	 * 
	 * @param retryable
	 *            the condition that returns true if the exception is not fatal
	 *            and a retry can be attempted
	 * @return this instance for fluent chaining
	 */
	@SuppressWarnings("squid:S1905")
	public RetryBuilder<P> retryable(Condition<Throwable> retryable) {
		return retryable(retryable == null ? (Predicate<Throwable>) null : retryable::accept);
	}

	@Override
	public RetryExecutor build() {
		if (executor != null) {
			return executor;
		}
		Builder<RetryStrategy> retryStrategy = buildRetryStrategy();
		if (retryStrategy == null) {
			return null;
		}
		BuilderToRetryStrategyProviderBridge retryProvider = new BuilderToRetryStrategyProviderBridge(retryStrategy);
		Awaiter builtAwaiter = buildAwaiter();
		if (executorFactory != null) {
			return executorFactory.create(retryProvider, builtAwaiter);
		}
		return buildContext.register(new SimpleRetryExecutor(retryProvider, builtAwaiter, buildRetryable()));
	}

	private Builder<RetryStrategy> buildRetryStrategy() {
		if (isConfigured(perExecutionDelay)) {
			return perExecutionDelay;
		}
		if (isConfigured(exponentialDelay)) {
			return exponentialDelay;
		}
		if (isConfigured(fixedInterval)) {
			return fixedInterval;
		}
		if (isConfigured(fixedDelay)) {
			return fixedDelay;
		}
		return null;
	}

	private static boolean isConfigured(Builder<RetryStrategy> builder) {
		if (builder == null) {
			return false;
		}
		return builder.build() != null;
	}

	private Awaiter buildAwaiter() {
		if (awaiter == null) {
			return new ThreadSleepAwaiter();
		}
		return awaiter;
	}

	private Predicate<Throwable> buildRetryable() {
		if (retryable == null) {
			return e -> e instanceof Exception;
		}
		return retryable;
	}
}