Java tutorial
/* * Copyright (C) 2016 Google Inc. * * 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 com.google.cloud.dataflow.sdk.util; import static com.google.common.base.Preconditions.checkArgument; import com.google.api.client.util.BackOff; import com.google.common.base.MoreObjects; import org.joda.time.Duration; /** * A fluent builder for {@link BackOff} objects that allows customization of the retry algorithm. * * @see #DEFAULT for the default configuration parameters. */ public final class FluentBackoff { private static final double DEFAULT_EXPONENT = 1.5; private static final double DEFAULT_RANDOMIZATION_FACTOR = 0.5; private static final Duration DEFAULT_MIN_BACKOFF = Duration.standardSeconds(1); private static final Duration DEFAULT_MAX_BACKOFF = Duration.standardDays(1000); private static final int DEFAULT_MAX_RETRIES = Integer.MAX_VALUE; private static final Duration DEFAULT_MAX_CUM_BACKOFF = Duration.standardDays(1000); private final double exponent; private final Duration initialBackoff; private final Duration maxBackoff; private final Duration maxCumulativeBackoff; private final int maxRetries; /** * By default the {@link BackOff} created by this builder will use exponential backoff (base * exponent 1.5) with an initial backoff of 1 second. These parameters can be overridden with * {@link #withExponent(double)} and {@link #withInitialBackoff(Duration)}, * respectively, and the maximum backoff after exponential increase can be capped using {@link * FluentBackoff#withMaxBackoff(Duration)}. * * <p>The default {@link BackOff} does not limit the number of retries. To limit the backoff, the * maximum total number of retries can be set using {@link #withMaxRetries(int)}. The * total time spent in backoff can be time-bounded as well by configuring {@link * #withMaxCumulativeBackoff(Duration)}. If either of these limits are reached, calls * to {@link BackOff#nextBackOffMillis()} will return {@link BackOff#STOP} to signal that no more * retries should continue. */ public static final FluentBackoff DEFAULT = new FluentBackoff(DEFAULT_EXPONENT, DEFAULT_MIN_BACKOFF, DEFAULT_MAX_BACKOFF, DEFAULT_MAX_CUM_BACKOFF, DEFAULT_MAX_RETRIES); /** * Instantiates a {@link BackOff} that will obey the current configuration. * * @see FluentBackoff */ public BackOff backoff() { return new BackoffImpl(this); } /** * Returns a copy of this {@link FluentBackoff} that instead uses the specified exponent to * control the exponential growth of delay. * * <p>Does not modify this object. * * @see FluentBackoff */ public FluentBackoff withExponent(double exponent) { checkArgument(exponent > 0, "exponent %s must be greater than 0", exponent); return new FluentBackoff(exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries); } /** * Returns a copy of this {@link FluentBackoff} that instead uses the specified initial backoff * duration. * * <p>Does not modify this object. * * @see FluentBackoff */ public FluentBackoff withInitialBackoff(Duration initialBackoff) { checkArgument(initialBackoff.isLongerThan(Duration.ZERO), "initialBackoff %s must be at least 1 millisecond", initialBackoff); return new FluentBackoff(exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries); } /** * Returns a copy of this {@link FluentBackoff} that limits the maximum backoff of an individual * attempt to the specified duration. * * <p>Does not modify this object. * * @see FluentBackoff */ public FluentBackoff withMaxBackoff(Duration maxBackoff) { checkArgument(maxBackoff.getMillis() > 0, "maxBackoff %s must be at least 1 millisecond", maxBackoff); return new FluentBackoff(exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries); } /** * Returns a copy of this {@link FluentBackoff} that limits the total time spent in backoff * returned across all calls to {@link BackOff#nextBackOffMillis()}. * * <p>Does not modify this object. * * @see FluentBackoff */ public FluentBackoff withMaxCumulativeBackoff(Duration maxCumulativeBackoff) { checkArgument(maxCumulativeBackoff.isLongerThan(Duration.ZERO), "maxCumulativeBackoff %s must be at least 1 millisecond", maxCumulativeBackoff); return new FluentBackoff(exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries); } /** * Returns a copy of this {@link FluentBackoff} that limits the total number of retries, aka * the total number of calls to {@link BackOff#nextBackOffMillis()} before returning * {@link BackOff#STOP}. * * <p>Does not modify this object. * * @see FluentBackoff */ public FluentBackoff withMaxRetries(int maxRetries) { checkArgument(maxRetries >= 0, "maxRetries %s cannot be negative", maxRetries); return new FluentBackoff(exponent, initialBackoff, maxBackoff, maxCumulativeBackoff, maxRetries); } public String toString() { return MoreObjects.toStringHelper(FluentBackoff.class).add("exponent", exponent) .add("initialBackoff", initialBackoff).add("maxBackoff", maxBackoff).add("maxRetries", maxRetries) .add("maxCumulativeBackoff", maxCumulativeBackoff).toString(); } private static class BackoffImpl implements BackOff { // Customization of this backoff. private final FluentBackoff backoffConfig; // Current state private Duration currentCumulativeBackoff; private int currentRetry; @Override public void reset() { currentRetry = 0; currentCumulativeBackoff = Duration.ZERO; } @Override public long nextBackOffMillis() { // Maximum number of retries reached. if (currentRetry >= backoffConfig.maxRetries) { return BackOff.STOP; } // Maximum cumulative backoff reached. if (currentCumulativeBackoff.compareTo(backoffConfig.maxCumulativeBackoff) >= 0) { return BackOff.STOP; } double currentIntervalMillis = Math.min( backoffConfig.initialBackoff.getMillis() * Math.pow(backoffConfig.exponent, currentRetry), backoffConfig.maxBackoff.getMillis()); double randomOffset = (Math.random() * 2 - 1) * DEFAULT_RANDOMIZATION_FACTOR * currentIntervalMillis; long nextBackoffMillis = Math.round(currentIntervalMillis + randomOffset); // Cap to limit on cumulative backoff Duration remainingCumulative = backoffConfig.maxCumulativeBackoff.minus(currentCumulativeBackoff); nextBackoffMillis = Math.min(nextBackoffMillis, remainingCumulative.getMillis()); // Update state and return backoff. currentCumulativeBackoff = currentCumulativeBackoff.plus(nextBackoffMillis); currentRetry += 1; return nextBackoffMillis; } private BackoffImpl(FluentBackoff backoffConfig) { this.backoffConfig = backoffConfig; this.reset(); } public String toString() { return MoreObjects.toStringHelper(BackoffImpl.class).add("backoffConfig", backoffConfig) .add("currentRetry", currentRetry).add("currentCumulativeBackoff", currentCumulativeBackoff) .toString(); } } private FluentBackoff(double exponent, Duration initialBackoff, Duration maxBackoff, Duration maxCumulativeBackoff, int maxRetries) { this.exponent = exponent; this.initialBackoff = initialBackoff; this.maxBackoff = maxBackoff; this.maxRetries = maxRetries; this.maxCumulativeBackoff = maxCumulativeBackoff; } }