/* * Copyright 2018 the original author or authors. * * 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 net.jodah.failsafe; import net.jodah.failsafe.internal.EventListener; import net.jodah.failsafe.util.concurrent.Scheduler; import java.time.Duration; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import static net.jodah.failsafe.internal.util.RandomDelay.randomDelay; import static net.jodah.failsafe.internal.util.RandomDelay.randomDelayInRange; /** * A PolicyExecutor that handles failures according to a {@link RetryPolicy}. * * @author Jonathan Halterman */ class RetryPolicyExecutor extends PolicyExecutor<RetryPolicy> { // Mutable state private volatile int failedAttempts; private volatile boolean retriesExceeded; /** The fixed, backoff, random or computed delay time in nanoseconds. */ private volatile long delayNanos = -1; // Listeners private final EventListener abortListener; private final EventListener failedAttemptListener; private final EventListener retriesExceededListener; private final EventListener retryListener; RetryPolicyExecutor(RetryPolicy retryPolicy, AbstractExecution execution, EventListener abortListener, EventListener failedAttemptListener, EventListener retriesExceededListener, EventListener retryListener) { super(retryPolicy, execution); this.abortListener = abortListener; this.failedAttemptListener = failedAttemptListener; this.retriesExceededListener = retriesExceededListener; this.retryListener = retryListener; } @Override protected Supplier<ExecutionResult> supply(Supplier<ExecutionResult> supplier, Scheduler scheduler) { return () -> { while (true) { ExecutionResult result = supplier.get(); if (retriesExceeded) return result; result = postExecute(result); if (result.isComplete()) return result; try { Thread.sleep(TimeUnit.NANOSECONDS.toMillis(result.getWaitNanos())); } catch (InterruptedException e) { if (!execution.interrupted) Thread.currentThread().interrupt(); return ExecutionResult.failure(new FailsafeException(e)); } // Call retry listener if (retryListener != null) retryListener.handle(result, execution); } }; } @Override @SuppressWarnings("unchecked") protected Supplier<CompletableFuture<ExecutionResult>> supplyAsync( Supplier<CompletableFuture<ExecutionResult>> supplier, Scheduler scheduler, FailsafeFuture<Object> future) { return () -> { CompletableFuture<ExecutionResult> promise = new CompletableFuture<>(); Callable<Object> callable = new Callable<Object>() { volatile ExecutionResult previousResult; @Override public Object call() { // Call retry listener if (retryListener != null && previousResult != null) retryListener.handle(previousResult, execution); // Propagate execution and handle result supplier.get().whenComplete((result, error) -> { if (error != null) promise.completeExceptionally(error); else if (result != null) { if (retriesExceeded) promise.complete(result); else { postExecuteAsync(result, scheduler, future).whenComplete((postResult, postError) -> { if (postError != null) promise.completeExceptionally(postError); else if (postResult != null) { if (retriesExceeded || postResult.isComplete()) promise.complete(postResult); else { // Guard against race with future.complete or future.cancel synchronized (future) { if (!future.isDone()) { try { previousResult = postResult; future.inject( (Future) scheduler.schedule(this, postResult.getWaitNanos(), TimeUnit.NANOSECONDS)); } catch (Throwable t) { // Hard scheduling failure promise.completeExceptionally(t); } } } } } }); } } }); return null; } }; try { callable.call(); } catch (Throwable t) { promise.completeExceptionally(t); } return promise; }; } @Override @SuppressWarnings("unchecked") protected ExecutionResult onFailure(ExecutionResult result) { if (failedAttemptListener != null) failedAttemptListener.handle(result, execution); failedAttempts++; long waitNanos = delayNanos; // Determine the computed delay Duration computedDelay = policy.computeDelay(execution); if (computedDelay != null) { waitNanos = computedDelay.toNanos(); } else { // Determine the fixed or random delay waitNanos = getFixedOrRandomDelayNanos(waitNanos); waitNanos = adjustForBackoff(waitNanos); delayNanos = waitNanos; } waitNanos = adjustForJitter(waitNanos); long elapsedNanos = execution.getElapsedTime().toNanos(); waitNanos = adjustForMaxDuration(waitNanos, elapsedNanos); // Calculate result boolean maxRetriesExceeded = policy.getMaxRetries() != -1 && failedAttempts > policy.getMaxRetries(); boolean maxDurationExceeded = policy.getMaxDuration() != null && elapsedNanos > policy.getMaxDuration().toNanos(); retriesExceeded = maxRetriesExceeded || maxDurationExceeded; boolean isAbortable = policy.isAbortable(result.getResult(), result.getFailure()); boolean shouldRetry = !result.isSuccess() && !isAbortable && !retriesExceeded && policy.allowsRetries(); boolean completed = isAbortable || !shouldRetry; boolean success = completed && result.isSuccess() && !isAbortable; // Call completion listeners if (abortListener != null && isAbortable) abortListener.handle(result, execution); else if (retriesExceededListener != null && !success && retriesExceeded) retriesExceededListener.handle(result, execution); return result.with(waitNanos, completed, success); } /** * Defaults async executions to not be complete until {@link #onFailure(ExecutionResult) says they are}. */ @Override protected CompletableFuture<ExecutionResult> onFailureAsync(ExecutionResult result, Scheduler scheduler, FailsafeFuture<Object> future) { return super.onFailureAsync(result.withNotComplete(), scheduler, future); } private long getFixedOrRandomDelayNanos(long waitNanos) { Duration delay = policy.getDelay(); Duration delayMin = policy.getDelayMin(); Duration delayMax = policy.getDelayMax(); if (waitNanos == -1 && delay != null && !delay.equals(Duration.ZERO)) waitNanos = delay.toNanos(); else if (delayMin != null && delayMax != null) waitNanos = randomDelayInRange(delayMin.toNanos(), delayMax.toNanos(), Math.random()); return waitNanos; } private long adjustForBackoff(long waitNanos) { if (execution.getAttemptCount() != 1 && policy.getMaxDelay() != null) waitNanos = (long) Math.min(waitNanos * policy.getDelayFactor(), policy.getMaxDelay().toNanos()); return waitNanos; } private long adjustForJitter(long waitNanos) { if (policy.getJitter() != null) waitNanos = randomDelay(waitNanos, policy.getJitter().toNanos(), Math.random()); else if (policy.getJitterFactor() > 0.0) waitNanos = randomDelay(waitNanos, policy.getJitterFactor(), Math.random()); return waitNanos; } private long adjustForMaxDuration(long waitNanos, long elapsedNanos) { if (policy.getMaxDuration() != null) { long maxRemainingWaitTime = policy.getMaxDuration().toNanos() - elapsedNanos; waitNanos = Math.min(waitNanos, maxRemainingWaitTime < 0 ? 0 : maxRemainingWaitTime); if (waitNanos < 0) waitNanos = 0; } return waitNanos; } }