/** * 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 org.apache.aurora.common.util; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import org.apache.aurora.common.base.ExceptionalSupplier; import org.apache.aurora.common.quantity.Amount; import org.apache.aurora.common.quantity.Time; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A utility for dealing with backoffs of retryable actions. * * <p>TODO(John Sirois): investigate synergies with BackoffDecider. * * @author John Sirois */ public class BackoffHelper { private static final Logger LOG = LoggerFactory.getLogger(BackoffHelper.class); private static final Amount<Long,Time> DEFAULT_INITIAL_BACKOFF = Amount.of(1L, Time.SECONDS); private static final Amount<Long,Time> DEFAULT_MAX_BACKOFF = Amount.of(1L, Time.MINUTES); private final Clock clock; private final BackoffStrategy backoffStrategy; /** * Creates a new BackoffHelper that uses truncated binary backoff starting at a 1 second backoff * and maxing out at a 1 minute backoff. */ public BackoffHelper() { this(DEFAULT_INITIAL_BACKOFF, DEFAULT_MAX_BACKOFF); } /** * Creates a new BackoffHelper that uses truncated binary backoff starting at the given * {@code initialBackoff} and maxing out at the given {@code maxBackoff}. * * @param initialBackoff the initial amount of time to back off * @param maxBackoff the maximum amount of time to back off */ public BackoffHelper(Amount<Long, Time> initialBackoff, Amount<Long, Time> maxBackoff) { this(new TruncatedBinaryBackoff(initialBackoff, maxBackoff)); } /** * Creates a BackoffHelper that uses the given {@code backoffStrategy} to calculate backoffs * between retries. * * @param backoffStrategy the backoff strategy to use */ public BackoffHelper(BackoffStrategy backoffStrategy) { this(Clock.SYSTEM_CLOCK, backoffStrategy); } @VisibleForTesting BackoffHelper(Clock clock, BackoffStrategy backoffStrategy) { this.clock = Preconditions.checkNotNull(clock); this.backoffStrategy = Preconditions.checkNotNull(backoffStrategy); } /** * Executes the given task using the configured backoff strategy until the task succeeds as * indicated by returning {@code true}. * * @param task the retryable task to execute until success * @throws InterruptedException if interrupted while waiting for the task to execute successfully * @throws BackoffStoppedException if the backoff stopped unsuccessfully * @throws E if the task throws */ public <E extends Exception> void doUntilSuccess(final ExceptionalSupplier<Boolean, E> task) throws InterruptedException, BackoffStoppedException, E { doUntilResult(() -> { Boolean result = task.get(); return Boolean.TRUE.equals(result) ? result : null; }); } /** * Gets {@link BackoffStrategy} instance the BackoffHelper is initialized with. * @return instance of {@link BackoffStrategy} used by BackoffHelper. */ public BackoffStrategy getBackoffStrategy() { return backoffStrategy; } /** * Executes the given task using the configured backoff strategy until the task succeeds as * indicated by returning a non-null value. * * @param task the retryable task to execute until success * @return the result of the successfully executed task * @throws InterruptedException if interrupted while waiting for the task to execute successfully * @throws BackoffStoppedException if the backoff stopped unsuccessfully * @throws E if the task throws */ public <T, E extends Exception> T doUntilResult(ExceptionalSupplier<T, E> task) throws InterruptedException, BackoffStoppedException, E { T result = task.get(); // give an immediate try return (result != null) ? result : retryWork(task); } private <T, E extends Exception> T retryWork(ExceptionalSupplier<T, E> work) throws E, InterruptedException, BackoffStoppedException { long currentBackoffMs = 0; while (backoffStrategy.shouldContinue(currentBackoffMs)) { currentBackoffMs = backoffStrategy.calculateBackoffMs(currentBackoffMs); LOG.debug("Operation failed, backing off for " + currentBackoffMs + "ms"); clock.waitFor(currentBackoffMs); T result = work.get(); if (result != null) { return result; } } throw new BackoffStoppedException(String.format("Backoff stopped without succeeding.")); } /** * Occurs after the backoff strategy should stop. */ public static class BackoffStoppedException extends RuntimeException { public BackoffStoppedException(String msg) { super(msg); } } }