// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.util;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.twitter.common.base.ExceptionalSupplier;
import com.twitter.common.quantity.Amount;
import com.twitter.common.quantity.Time;
import java.util.logging.Logger;
/**
* 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 = Logger.getLogger(BackoffHelper.class.getName());
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 new BackoffHelper that uses truncated binary backoff starting at the given
* {@code initialBackoff} and maxing out at the given {@code maxBackoff}. This will either:
* <ul>
* <li>{@code stopAtMax == true} : throw {@code BackoffExpiredException} when maxBackoff is
* reached</li>
* <li>{@code stopAtMax == false} : continue backing off with maxBackoff</li>
* </ul>
*
* @param initialBackoff the initial amount of time to back off
* @param maxBackoff the maximum amount of time to back off
* @param stopAtMax if true, this will throw {@code BackoffStoppedException} when the max backoff is
* reached
*/
public BackoffHelper(Amount<Long, Time> initialBackoff, Amount<Long, Time> maxBackoff,
boolean stopAtMax) {
this(new TruncatedBinaryBackoff(initialBackoff, maxBackoff, stopAtMax));
}
/**
* 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(new ExceptionalSupplier<Boolean, E>() {
@Override public Boolean get() throws E {
Boolean result = task.get();
return Boolean.TRUE.equals(result) ? result : null;
}
});
}
/**
* 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 = backoffStrategy.calculateBackoffMs(currentBackoffMs);
LOG.fine("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);
}
}
}