/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.beam.sdk.util; import static com.google.common.base.Preconditions.checkArgument; 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; } }