All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.uber.cadence.common.RetryOptions Maven / Gradle / Ivy

There is a newer version: 3.12.5
Show newest version
/*
 *  Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 *  Modifications copyright (C) 2017 Uber Technologies, Inc.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"). You may not
 *  use this file except in compliance with the License. A copy of the License is
 *  located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 *  or in the "license" file accompanying this file. This file 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.uber.cadence.common;

import com.google.common.base.Defaults;
import com.uber.cadence.workflow.ActivityFailureException;
import com.uber.cadence.workflow.ChildWorkflowFailureException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public final class RetryOptions {

  private static final double DEFAULT_BACKOFF_COEFFICIENT = 2.0;
  private static final int DEFAULT_MAXIMUM_MULTIPLIER = 100;

  /**
   * Merges annotation with explicitly provided RetryOptions. If there is conflict RetryOptions
   * takes precedence.
   */
  public static RetryOptions merge(MethodRetry r, RetryOptions o) {
    if (r == null) {
      if (o == null) {
        return null;
      }
      return new RetryOptions.Builder(o).validateBuildWithDefaults();
    }
    if (o == null) {
      o = new RetryOptions.Builder().build();
    }
    Duration initial = merge(r.initialIntervalSeconds(), o.getInitialInterval());
    RetryOptions.Builder builder = new RetryOptions.Builder();
    if (initial != null) {
      builder.setInitialInterval(initial);
    }
    Duration expiration = merge(r.expirationSeconds(), o.getExpiration());
    if (expiration != null) {
      builder.setExpiration(expiration);
    }
    Duration maximum = merge(r.maximumIntervalSeconds(), o.getMaximumInterval());
    if (maximum != null) {
      builder.setMaximumInterval(maximum);
    }
    double coefficient = merge(r.backoffCoefficient(), o.getBackoffCoefficient(), double.class);
    if (coefficient != 0d) {
      builder.setBackoffCoefficient(coefficient);
    } else {
      builder.setBackoffCoefficient(DEFAULT_BACKOFF_COEFFICIENT);
    }
    return builder
        .setMaximumAttempts(merge(r.maximumAttempts(), o.getMaximumAttempts(), int.class))
        .setDoNotRetry(merge(r.doNotRetry(), o.getDoNotRetry()))
        .validateBuildWithDefaults();
  }

  /** The parameter options takes precedence. */
  public RetryOptions merge(RetryOptions o) {
    if (o == null) {
      return this;
    }
    return new RetryOptions.Builder()
        .setInitialInterval(merge(getInitialInterval(), o.getInitialInterval(), Duration.class))
        .setExpiration(merge(getExpiration(), o.getExpiration(), Duration.class))
        .setMaximumInterval(merge(getMaximumInterval(), o.getMaximumInterval(), Duration.class))
        .setBackoffCoefficient(
            merge(getBackoffCoefficient(), o.getBackoffCoefficient(), double.class))
        .setMaximumAttempts(merge(getMaximumAttempts(), o.getMaximumAttempts(), int.class))
        .setDoNotRetry(merge(getDoNotRetry(), o.getDoNotRetry()))
        .validateBuildWithDefaults();
  }

  @SafeVarargs
  public final RetryOptions addDoNotRetry(Class... doNotRetry) {
    if (doNotRetry == null) {
      return this;
    }

    double backoffCoefficient = getBackoffCoefficient();
    if (backoffCoefficient == 0) {
      backoffCoefficient = DEFAULT_BACKOFF_COEFFICIENT;
    }

    RetryOptions.Builder builder =
        new RetryOptions.Builder()
            .setInitialInterval(getInitialInterval())
            .setExpiration(getExpiration())
            .setMaximumInterval(getMaximumInterval())
            .setBackoffCoefficient(backoffCoefficient)
            .setDoNotRetry(merge(getDoNotRetry(), Arrays.asList(doNotRetry)));

    if (getMaximumAttempts() > 0) {
      builder.setMaximumAttempts(getMaximumAttempts());
    }
    return builder.validateBuildWithDefaults();
  }

  public static final class Builder {

    private Duration initialInterval;

    private Duration expiration;

    private double backoffCoefficient;

    private int maximumAttempts;

    private Duration maximumInterval;

    private List> doNotRetry;

    public Builder() {}

    public Builder(RetryOptions o) {
      if (o == null) {
        return;
      }
      this.backoffCoefficient = o.getBackoffCoefficient();
      this.maximumAttempts = o.getMaximumAttempts();
      this.expiration = o.getExpiration();
      this.initialInterval = o.getInitialInterval();
      this.maximumInterval = o.getMaximumInterval();
      this.doNotRetry = o.getDoNotRetry();
    }

    /**
     * Interval of the first retry. If coefficient is 1.0 then it is used for all retries. Required.
     */
    public Builder setInitialInterval(Duration initialInterval) {
      Objects.requireNonNull(initialInterval);
      if (initialInterval.isNegative() || initialInterval.isZero()) {
        throw new IllegalArgumentException("Invalid interval: " + initialInterval);
      }
      this.initialInterval = initialInterval;
      return this;
    }

    /**
     * Maximum time to retry. Required. When exceeded the retries stop even if maximum retries is
     * not reached yet.
     */
    public Builder setExpiration(Duration expiration) {
      Objects.requireNonNull(expiration);
      if (expiration.isNegative() || expiration.isZero()) {
        throw new IllegalArgumentException("Invalid interval: " + expiration);
      }
      this.expiration = expiration;
      return this;
    }

    /**
     * Coefficient used to calculate the next retry interval. The next retry interval is previous
     * interval multiplied by this coefficient. Must be 1 or larger. Default is 2.0.
     */
    public Builder setBackoffCoefficient(double backoffCoefficient) {
      if (backoffCoefficient < 1d) {
        throw new IllegalArgumentException("coefficient less than 1.0: " + backoffCoefficient);
      }
      this.backoffCoefficient = backoffCoefficient;
      return this;
    }

    /**
     * Maximum number of attempts. When exceeded the retries stop even if not expired yet. Must be 1
     * or bigger. Default is unlimited.
     */
    public Builder setMaximumAttempts(int maximumAttempts) {
      if (maximumAttempts < 1) {
        throw new IllegalArgumentException("Invalid maximumAttempts: " + maximumAttempts);
      }
      this.maximumAttempts = maximumAttempts;
      return this;
    }

    /**
     * Maximum interval between retries. Exponential backoff leads to interval increase. This value
     * is the cap of the increase. Default is 100x of initial interval.
     */
    public Builder setMaximumInterval(Duration maximumInterval) {
      Objects.requireNonNull(maximumInterval);
      if (maximumInterval.isNegative() || maximumInterval.isZero()) {
        throw new IllegalArgumentException("Invalid interval: " + maximumInterval);
      }
      this.maximumInterval = maximumInterval;
      return this;
    }

    /**
     * List of exceptions to retry. When matching an exact match is used. So adding
     * RuntimeException.class to this list is going to include only RuntimeException itself, not all
     * of its subclasses. The reason for such behaviour is to be able to support server side retries
     * without knowledge of Java exception hierarchy. When considering an exception type a cause of
     * {@link com.uber.cadence.workflow.ActivityFailureException} and {@link
     * com.uber.cadence.workflow.ChildWorkflowFailureException} is looked at.
     *
     * 

{@link Error} and {@link java.util.concurrent.CancellationException} are never retried and * are not even passed to this filter. */ @SafeVarargs public final Builder setDoNotRetry(Class... doNotRetry) { if (doNotRetry != null) { this.doNotRetry = Arrays.asList(doNotRetry); } return this; } /** * Build RetryOptions without performing validation as validation should be done after merging * with {@link MethodRetry}. */ public RetryOptions build() { return new RetryOptions( initialInterval, backoffCoefficient, expiration, maximumAttempts, maximumInterval, doNotRetry); } /** Validates property values and builds RetryOptions with default values. */ public RetryOptions validateBuildWithDefaults() { double backoff = backoffCoefficient; if (backoff == 0d) { backoff = DEFAULT_BACKOFF_COEFFICIENT; } RetryOptions result = new RetryOptions( initialInterval, backoff, expiration, maximumAttempts, maximumInterval, doNotRetry); result.validate(); return result; } } private final Duration initialInterval; private final double backoffCoefficient; private final Duration expiration; private final int maximumAttempts; private final Duration maximumInterval; private final List> doNotRetry; private RetryOptions( Duration initialInterval, double backoffCoefficient, Duration expiration, int maximumAttempts, Duration maximumInterval, List> doNotRetry) { this.initialInterval = initialInterval; this.backoffCoefficient = backoffCoefficient; this.expiration = expiration; this.maximumAttempts = maximumAttempts; this.maximumInterval = maximumInterval; this.doNotRetry = doNotRetry != null ? Collections.unmodifiableList(doNotRetry) : null; } public Duration getInitialInterval() { return initialInterval; } public double getBackoffCoefficient() { return backoffCoefficient; } public Duration getExpiration() { return expiration; } public int getMaximumAttempts() { return maximumAttempts; } public Duration getMaximumInterval() { return maximumInterval; } public void validate() { if (initialInterval == null) { throw new IllegalStateException("required property initialInterval not set"); } if (expiration == null && maximumAttempts <= 0) { throw new IllegalArgumentException( "both MaximumAttempts and Expiration on retry policy are not set, at least one of them must be set"); } if (maximumInterval != null && maximumInterval.compareTo(initialInterval) < 0) { throw new IllegalStateException( "maximumInterval(" + maximumInterval + ") cannot be smaller than initialInterval(" + initialInterval); } if (backoffCoefficient != 0d && backoffCoefficient < 1.0) { throw new IllegalArgumentException("coefficient less than 1"); } if (maximumAttempts != 0 && maximumAttempts < 0) { throw new IllegalArgumentException("negative maximum attempts"); } } /** * @return null if not configured. When merging with annotation it makes a difference. null means * use values from an annotation. Empty list means do not retry on anything. */ public List> getDoNotRetry() { return doNotRetry; } @Override public String toString() { return "RetryOptions{" + "initialInterval=" + initialInterval + ", backoffCoefficient=" + backoffCoefficient + ", expiration=" + expiration + ", maximumAttempts=" + maximumAttempts + ", maximumInterval=" + maximumInterval + ", doNotRetry=" + doNotRetry + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RetryOptions that = (RetryOptions) o; return Double.compare(that.backoffCoefficient, backoffCoefficient) == 0 && maximumAttempts == that.maximumAttempts && Objects.equals(initialInterval, that.initialInterval) && Objects.equals(expiration, that.expiration) && Objects.equals(maximumInterval, that.maximumInterval) && Objects.equals(doNotRetry, that.doNotRetry); } @Override public int hashCode() { return Objects.hash( initialInterval, backoffCoefficient, expiration, maximumAttempts, maximumInterval, doNotRetry); } private static G merge(G annotation, G options, Class type) { if (!Defaults.defaultValue(type).equals(options)) { return options; } return annotation; } private static Duration merge(long aSeconds, Duration o) { if (o != null) { return o; } return aSeconds == 0 ? null : Duration.ofSeconds(aSeconds); } private static Class[] merge( Class[] a, List> o) { if (o != null) { @SuppressWarnings("unchecked") Class[] result = new Class[o.size()]; return o.toArray(result); } return a.length == 0 ? null : a; } private Class[] merge( List> o1, List> o2) { if (o2 != null) { @SuppressWarnings("unchecked") Class[] result = new Class[o2.size()]; return o2.toArray(result); } if (o1.size() > 0) { @SuppressWarnings("unchecked") Class[] result = new Class[o1.size()]; return o1.toArray(result); } return null; } public long calculateSleepTime(long attempt) { double coefficient = backoffCoefficient == 0d ? DEFAULT_BACKOFF_COEFFICIENT : backoffCoefficient; double sleepMillis = Math.pow(coefficient, attempt - 1) * initialInterval.toMillis(); if (maximumInterval == null) { return (long) Math.min(sleepMillis, initialInterval.toMillis() * DEFAULT_MAXIMUM_MULTIPLIER); } return Math.min((long) sleepMillis, maximumInterval.toMillis()); } public boolean shouldRethrow(Throwable e, long attempt, long elapsed, long sleepTime) { if (e instanceof ActivityFailureException || e instanceof ChildWorkflowFailureException) { e = e.getCause(); } if (doNotRetry != null) { for (Class doNotRetry : doNotRetry) { if (doNotRetry.equals(e.getClass())) { return true; } } } // Attempt that failed. if (maximumAttempts != 0 && attempt >= maximumAttempts) { return true; } return expiration != null && elapsed + sleepTime >= expiration.toMillis(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy