diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/Poller.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/Poller.java index 6bbd3b6601..d22a48fc9e 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/worker/Poller.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/Poller.java @@ -206,9 +206,11 @@ private class PollLoopTask implements Runnable { this.task = task; this.pollBackoffThrottler = new BackoffThrottler( - pollerOptions.getPollBackoffInitialInterval(), - pollerOptions.getPollBackoffMaximumInterval(), - pollerOptions.getPollBackoffCoefficient()); + pollerOptions.getBackoffInitialInterval(), + pollerOptions.getBackoffCongestionInitialInterval(), + pollerOptions.getBackoffMaximumInterval(), + pollerOptions.getBackoffCoefficient(), + pollerOptions.getBackoffMaximumJitterCoefficient()); } @Override @@ -242,7 +244,10 @@ public void run() { Thread.currentThread().interrupt(); } else { // Don't increase throttle on InterruptedException - pollBackoffThrottler.failure(); + pollBackoffThrottler.failure( + (e instanceof StatusRuntimeException) + ? ((StatusRuntimeException) e).getStatus().getCode() + : Status.Code.UNKNOWN); } uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), e); } finally { diff --git a/temporal-sdk/src/main/java/io/temporal/internal/worker/PollerOptions.java b/temporal-sdk/src/main/java/io/temporal/internal/worker/PollerOptions.java index 8cb78af9db..192e29c0f5 100644 --- a/temporal-sdk/src/main/java/io/temporal/internal/worker/PollerOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/internal/worker/PollerOptions.java @@ -54,9 +54,11 @@ public static final class Builder { private int maximumPollRateIntervalMilliseconds = 1000; private double maximumPollRatePerSecond; - private double pollBackoffCoefficient = 2; - private Duration pollBackoffInitialInterval = Duration.ofMillis(100); - private Duration pollBackoffMaximumInterval = Duration.ofMinutes(1); + private double backoffCoefficient = 2; + private Duration backoffInitialInterval = Duration.ofMillis(100); + private Duration backoffCongestionInitialInterval = Duration.ofMillis(1000); + private Duration backoffMaximumInterval = Duration.ofMinutes(1); + private double backoffMaximumJitterCoefficient = 0.1; private int pollThreadCount = 1; private String pollThreadNamePrefix; private Thread.UncaughtExceptionHandler uncaughtExceptionHandler; @@ -69,9 +71,11 @@ private Builder(PollerOptions options) { } this.maximumPollRateIntervalMilliseconds = options.getMaximumPollRateIntervalMilliseconds(); this.maximumPollRatePerSecond = options.getMaximumPollRatePerSecond(); - this.pollBackoffCoefficient = options.getPollBackoffCoefficient(); - this.pollBackoffInitialInterval = options.getPollBackoffInitialInterval(); - this.pollBackoffMaximumInterval = options.getPollBackoffMaximumInterval(); + this.backoffCoefficient = options.getBackoffCoefficient(); + this.backoffInitialInterval = options.getBackoffInitialInterval(); + this.backoffCongestionInitialInterval = options.getBackoffCongestionInitialInterval(); + this.backoffMaximumInterval = options.getBackoffMaximumInterval(); + this.backoffMaximumJitterCoefficient = options.getBackoffMaximumJitterCoefficient(); this.pollThreadCount = options.getPollThreadCount(); this.pollThreadNamePrefix = options.getPollThreadNamePrefix(); this.uncaughtExceptionHandler = options.getUncaughtExceptionHandler(); @@ -93,23 +97,41 @@ public Builder setMaximumPollRatePerSecond(double maximumPollRatePerSecond) { } /** Coefficient to use when calculating exponential delay in case of failures */ - public Builder setPollBackoffCoefficient(double pollBackoffCoefficient) { - this.pollBackoffCoefficient = pollBackoffCoefficient; + public Builder setBackoffCoefficient(double backoffCoefficient) { + this.backoffCoefficient = backoffCoefficient; return this; } /** - * Initial delay in case of failure. If backoff coefficient is 1 then it would be the constant - * delay between failing polls. + * Initial delay in case of regular failure. If backoff coefficient is 1 then it would be the + * constant delay between failing polls. */ - public Builder setPollBackoffInitialInterval(Duration pollBackoffInitialInterval) { - this.pollBackoffInitialInterval = pollBackoffInitialInterval; + public Builder setBackoffInitialInterval(Duration backoffInitialInterval) { + this.backoffInitialInterval = backoffInitialInterval; + return this; + } + + /** + * Initial delay in case of congestion-related failures (i.e. RESOURCE_EXHAUSTED errors). If + * backoff coefficient is 1 then it would be the constant delay between failing polls. + */ + public Builder setBackoffCongestionInitialInterval(Duration backoffCongestionInitialInterval) { + this.backoffCongestionInitialInterval = backoffCongestionInitialInterval; return this; } /** Maximum interval between polls in case of failures. */ - public Builder setPollBackoffMaximumInterval(Duration pollBackoffMaximumInterval) { - this.pollBackoffMaximumInterval = pollBackoffMaximumInterval; + public Builder setBackoffMaximumInterval(Duration backoffMaximumInterval) { + this.backoffMaximumInterval = backoffMaximumInterval; + return this; + } + + /** + * Maximum amount of jitter to apply. 0.2 means that actual retry time can be +/- 20% of the + * calculated time. Set to 0 to disable jitter. Must be lower than 1. Default is 0.1. + */ + public Builder setBackoffMaximumJitterCoefficient(double backoffMaximumJitterCoefficient) { + this.backoffMaximumJitterCoefficient = backoffMaximumJitterCoefficient; return this; } @@ -151,9 +173,11 @@ public PollerOptions build() { return new PollerOptions( maximumPollRateIntervalMilliseconds, maximumPollRatePerSecond, - pollBackoffCoefficient, - pollBackoffInitialInterval, - pollBackoffMaximumInterval, + backoffCoefficient, + backoffInitialInterval, + backoffCongestionInitialInterval, + backoffMaximumInterval, + backoffMaximumJitterCoefficient, pollThreadCount, uncaughtExceptionHandler, pollThreadNamePrefix); @@ -164,9 +188,11 @@ public PollerOptions build() { private final int maximumPollRateIntervalMilliseconds; private final double maximumPollRatePerSecond; - private final double pollBackoffCoefficient; - private final Duration pollBackoffInitialInterval; - private final Duration pollBackoffMaximumInterval; + private final double backoffCoefficient; + private final double backoffMaximumJitterCoefficient; + private final Duration backoffInitialInterval; + private final Duration backoffCongestionInitialInterval; + private final Duration backoffMaximumInterval; private final int pollThreadCount; private final Thread.UncaughtExceptionHandler uncaughtExceptionHandler; private final String pollThreadNamePrefix; @@ -174,17 +200,21 @@ public PollerOptions build() { private PollerOptions( int maximumPollRateIntervalMilliseconds, double maximumPollRatePerSecond, - double pollBackoffCoefficient, - Duration pollBackoffInitialInterval, - Duration pollBackoffMaximumInterval, + double backoffCoefficient, + Duration backoffInitialInterval, + Duration backoffCongestionInitialInterval, + Duration backoffMaximumInterval, + double backoffMaximumJitterCoefficient, int pollThreadCount, Thread.UncaughtExceptionHandler uncaughtExceptionHandler, String pollThreadNamePrefix) { this.maximumPollRateIntervalMilliseconds = maximumPollRateIntervalMilliseconds; this.maximumPollRatePerSecond = maximumPollRatePerSecond; - this.pollBackoffCoefficient = pollBackoffCoefficient; - this.pollBackoffInitialInterval = pollBackoffInitialInterval; - this.pollBackoffMaximumInterval = pollBackoffMaximumInterval; + this.backoffCoefficient = backoffCoefficient; + this.backoffInitialInterval = backoffInitialInterval; + this.backoffCongestionInitialInterval = backoffCongestionInitialInterval; + this.backoffMaximumInterval = backoffMaximumInterval; + this.backoffMaximumJitterCoefficient = backoffMaximumJitterCoefficient; this.pollThreadCount = pollThreadCount; this.uncaughtExceptionHandler = uncaughtExceptionHandler; this.pollThreadNamePrefix = pollThreadNamePrefix; @@ -198,16 +228,24 @@ public double getMaximumPollRatePerSecond() { return maximumPollRatePerSecond; } - public double getPollBackoffCoefficient() { - return pollBackoffCoefficient; + public double getBackoffCoefficient() { + return backoffCoefficient; + } + + public Duration getBackoffInitialInterval() { + return backoffInitialInterval; + } + + public Duration getBackoffCongestionInitialInterval() { + return backoffCongestionInitialInterval; } - public Duration getPollBackoffInitialInterval() { - return pollBackoffInitialInterval; + public Duration getBackoffMaximumInterval() { + return backoffMaximumInterval; } - public Duration getPollBackoffMaximumInterval() { - return pollBackoffMaximumInterval; + public double getBackoffMaximumJitterCoefficient() { + return backoffMaximumJitterCoefficient; } public int getPollThreadCount() { @@ -229,12 +267,16 @@ public String toString() { + maximumPollRateIntervalMilliseconds + ", maximumPollRatePerSecond=" + maximumPollRatePerSecond - + ", pollBackoffCoefficient=" - + pollBackoffCoefficient - + ", pollBackoffInitialInterval=" - + pollBackoffInitialInterval - + ", pollBackoffMaximumInterval=" - + pollBackoffMaximumInterval + + ", backoffCoefficient=" + + backoffCoefficient + + ", backoffInitialInterval=" + + backoffInitialInterval + + ", backoffCongestionInitialInterval=" + + backoffCongestionInitialInterval + + ", backoffMaximumInterval=" + + backoffMaximumInterval + + ", backoffMaximumJitterCoefficient=" + + backoffMaximumJitterCoefficient + ", pollThreadCount=" + pollThreadCount + ", pollThreadNamePrefix='" diff --git a/temporal-serviceclient/src/main/java/io/temporal/internal/BackoffThrottler.java b/temporal-serviceclient/src/main/java/io/temporal/internal/BackoffThrottler.java index 7b0633fd8e..38c9c23143 100644 --- a/temporal-serviceclient/src/main/java/io/temporal/internal/BackoffThrottler.java +++ b/temporal-serviceclient/src/main/java/io/temporal/internal/BackoffThrottler.java @@ -20,25 +20,34 @@ package io.temporal.internal; +import io.grpc.Status; +import io.grpc.Status.Code; import java.time.Duration; import java.util.Objects; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; /** - * Used to throttle code execution in presence of failures using exponential backoff logic. The - * formula used to calculate the next sleep interval is: + * Used to throttle code execution in presence of failures using exponential backoff logic. + * + *
The formula used to calculate the next sleep interval is: * *
* *
- * min(pow(backoffCoefficient, failureCount - 1) * initialSleep, maxSleep); + * jitter = random number in the range [-maxJitterCoefficient, +maxJitterCoefficient]; + * sleepTime = min(pow(backoffCoefficient, failureCount - 1) * initialSleep * (1 + jitter), maxSleep); ** + * where
initialSleep is either set to regularInitialSleep or
+ * congestionInitialSleep based on the most recent failure. Note that it means that
+ * attempt X can possibly get a shorter throttle than attempt X-1, if a non-congestion failure
+ * occurs after a congestion failure. This is the expected behaviour for all SDK.
+ *
* Example usage: * *
- * BackoffThrottler throttler = new BackoffThrottler(1000, 60000, 2);
+ * BackoffThrottler throttler = new BackoffThrottler(50, 1000, 60000, 2, 0.1);
* while(!stopped) {
* try {
* long throttleMs = throttler.getSleepTime();
@@ -50,7 +59,10 @@
* throttler.success();
* }
* catch (Exception e) {
- * throttler.failure();
+ * throttler.failure(
+ * (e instanceof StatusRuntimeException)
+ * ? ((StatusRuntimeException) e).getStatus().getCode()
+ * : Status.Code.UNKNOWN);
* }
* }
*
@@ -60,32 +72,63 @@
@NotThreadSafe
public final class BackoffThrottler {
- private final Duration initialSleep;
+ private final Duration regularInitialSleep;
+
+ private final Duration congestionInitialSleep;
private final Duration maxSleep;
private final double backoffCoefficient;
+ private final double maxJitterCoefficient;
+
private int failureCount = 0;
+ private Status.Code lastFailureCode = Code.OK;
+
/**
* Construct an instance of the throttler.
*
- * @param initialSleep time to sleep on the first failure
+ * @param regularInitialSleep time to sleep on the first failure (assuming regular failures)
+ * @param congestionInitialSleep time to sleep on the first failure (for congestion failures)
* @param maxSleep maximum time to sleep independently of number of failures
- * @param backoffCoefficient coefficient used to calculate the next time to sleep.
+ * @param backoffCoefficient coefficient used to calculate the next time to sleep
+ * @param maxJitterCoefficient maximum jitter coefficient (in the range [0.0, 1.0[) to randomly
+ * add or subtract to sleep time
*/
public BackoffThrottler(
- Duration initialSleep, @Nullable Duration maxSleep, double backoffCoefficient) {
- Objects.requireNonNull(initialSleep, "initialSleep");
- this.initialSleep = initialSleep;
+ Duration regularInitialSleep,
+ Duration congestionInitialSleep,
+ @Nullable Duration maxSleep,
+ double backoffCoefficient,
+ double maxJitterCoefficient) {
+ Objects.requireNonNull(regularInitialSleep, "regularInitialSleep");
+ Objects.requireNonNull(congestionInitialSleep, "congestionInitialSleep");
+ if (backoffCoefficient < 1.0) {
+ throw new IllegalArgumentException(
+ "backoff coefficient less than 1.0: " + backoffCoefficient);
+ }
+ if (maxJitterCoefficient < 0 || maxJitterCoefficient >= 1.0) {
+ throw new IllegalArgumentException(
+ "maxJitterCoefficient has to be >= 0 and < 1.0: " + maxJitterCoefficient);
+ }
+ this.regularInitialSleep = regularInitialSleep;
+ this.congestionInitialSleep = congestionInitialSleep;
this.maxSleep = maxSleep;
this.backoffCoefficient = backoffCoefficient;
+ this.maxJitterCoefficient = maxJitterCoefficient;
}
public long getSleepTime() {
if (failureCount == 0) return 0;
- double sleepMillis = Math.pow(backoffCoefficient, failureCount - 1) * initialSleep.toMillis();
+ Duration initial =
+ (lastFailureCode == Code.RESOURCE_EXHAUSTED) ? congestionInitialSleep : regularInitialSleep;
+
+ // Choose a random number in the range [-maxJitterCoefficient, +maxJitterCoefficient];
+ double jitter = Math.random() * maxJitterCoefficient * 2 - maxJitterCoefficient;
+ double sleepMillis =
+ Math.pow(backoffCoefficient, failureCount - 1) * initial.toMillis() * (1 + jitter);
+
if (maxSleep != null) {
return Math.min((long) sleepMillis, maxSleep.toMillis());
}
@@ -96,13 +139,15 @@ public int getAttemptCount() {
return failureCount;
}
- /** Reset failure count to 0. */
+ /** Reset failure count to 0 and clear last failure code. */
public void success() {
failureCount = 0;
+ lastFailureCode = Code.OK;
}
- /** Increment failure count. */
- public void failure() {
+ /** Increment failure count and set last failure code. */
+ public void failure(Status.Code failureCode) {
failureCount++;
+ lastFailureCode = failureCode;
}
}
diff --git a/temporal-serviceclient/src/main/java/io/temporal/internal/retryer/GrpcAsyncRetryer.java b/temporal-serviceclient/src/main/java/io/temporal/internal/retryer/GrpcAsyncRetryer.java
index ee5ebbe54d..65ebab1d45 100644
--- a/temporal-serviceclient/src/main/java/io/temporal/internal/retryer/GrpcAsyncRetryer.java
+++ b/temporal-serviceclient/src/main/java/io/temporal/internal/retryer/GrpcAsyncRetryer.java
@@ -22,6 +22,7 @@
import io.grpc.Context;
import io.grpc.Deadline;
+import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.temporal.api.workflowservice.v1.GetSystemInfoResponse;
import io.temporal.internal.BackoffThrottler;
@@ -66,8 +67,10 @@ public GrpcAsyncRetryer(
this.throttler =
new BackoffThrottler(
rpcOptions.getInitialInterval(),
+ rpcOptions.getCongestionInitialInterval(),
rpcOptions.getMaximumInterval(),
- rpcOptions.getBackoffCoefficient());
+ rpcOptions.getBackoffCoefficient(),
+ rpcOptions.getMaximumJitterCoefficient());
}
public CompletableFutureAt least one of the expiration or {@link #setMaximumAttempts(int)} is required to be set. + *
At least one of expiration or {@link #setMaximumAttempts(int)} is required to be set.
+ *
+ * @param expiration Maximum time to retry. Defaults to 1 minute, which is used if set to {@code
+ * null}.
*/
public Builder setExpiration(Duration expiration) {
if (expiration != null) {
@@ -142,12 +179,19 @@ public Builder setExpiration(Duration expiration) {
/**
* 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.
+ *
+ * @param backoffCoefficient Coefficient used to calculate the next retry interval. Defaults to
+ * 2.0, which is used if set to {@code 0}.
*/
public Builder setBackoffCoefficient(double backoffCoefficient) {
- if (backoffCoefficient < 1d) {
- throw new IllegalArgumentException("coefficient less than 1.0: " + backoffCoefficient);
+ if (backoffCoefficient != 0.0) {
+ if (!Double.isFinite(backoffCoefficient) || (backoffCoefficient < 1.0)) {
+ throw new IllegalArgumentException("coefficient has to be >= 1.0: " + backoffCoefficient);
+ }
+ this.backoffCoefficient = backoffCoefficient;
+ } else {
+ this.backoffCoefficient = 0.0;
}
- this.backoffCoefficient = backoffCoefficient;
return this;
}
@@ -155,10 +199,11 @@ public Builder setBackoffCoefficient(double backoffCoefficient) {
* When exceeded the amount of attempts, stop. Even if expiration time is not reached.
* Default is unlimited.
*
- *
At least one of the maximum attempts or {@link #setExpiration(Duration)} is required to be + *
At least one of maximum attempts or {@link #setExpiration(Duration)} is required to be
* set.
*
- * @param maximumAttempts Maximum number of attempts. Default will be used if set to {@code 0}.
+ * @param maximumAttempts Maximum number of attempts. Defaults to unlimited, which is used if
+ * set to {@code 0}.
*/
public Builder setMaximumAttempts(int maximumAttempts) {
if (maximumAttempts < 0) {
@@ -173,14 +218,39 @@ public Builder setMaximumAttempts(int maximumAttempts) {
* is the cap of the increase.
* Default is 100x of initial interval. Can't be less than {@link #setInitialInterval(Duration)}
*
- * @param maximumInterval the maximum interval value. Default will be used if set to {@code
- * null}.
+ * @param maximumInterval the maximum interval value. Defaults to 100x initial interval, which
+ * is used if set to {@code null}.
*/
public Builder setMaximumInterval(Duration maximumInterval) {
- if (maximumInterval != null && (maximumInterval.isNegative() || maximumInterval.isZero())) {
- throw new IllegalArgumentException("Invalid interval: " + maximumInterval);
+ if (maximumInterval != null) {
+ if ((maximumInterval.isNegative() || maximumInterval.isZero())) {
+ throw new IllegalArgumentException("Invalid interval: " + maximumInterval);
+ }
+ this.maximumInterval = maximumInterval;
+ } else {
+ this.maximumInterval = null;
+ }
+ return this;
+ }
+
+ /**
+ * Maximum amount of jitter to apply. 0.2 means that actual retry time can be +/- 20% of the
+ * calculated time. Set to 0 to disable jitter. Must be lower than 1. Default is 0.1.
+ *
+ * @param maximumJitterCoefficient Maximum amount of jitter. Default will be used if set to -1.
+ */
+ public Builder setMaximumJitterCoefficient(double maximumJitterCoefficient) {
+ if (maximumJitterCoefficient != -1.0) {
+ if (!Double.isFinite(maximumJitterCoefficient)
+ || maximumJitterCoefficient < 0
+ || maximumJitterCoefficient >= 1.0) {
+ throw new IllegalArgumentException(
+ "maximumJitterCoefficient has to be >= 0 and < 1.0: " + maximumJitterCoefficient);
+ }
+ this.maximumJitterCoefficient = maximumJitterCoefficient;
+ } else {
+ this.maximumJitterCoefficient = -1.0;
}
- this.maximumInterval = maximumInterval;
return this;
}
@@ -244,12 +314,17 @@ public Builder setRetryOptions(RpcRetryOptions o) {
}
setInitialInterval(
OptionsUtils.merge(initialInterval, o.getInitialInterval(), Duration.class));
+ setCongestionInitialInterval(
+ OptionsUtils.merge(congestionInitialInterval, o.getInitialInterval(), Duration.class));
setExpiration(OptionsUtils.merge(expiration, o.getExpiration(), Duration.class));
setMaximumInterval(
OptionsUtils.merge(maximumInterval, o.getMaximumInterval(), Duration.class));
setBackoffCoefficient(
OptionsUtils.merge(backoffCoefficient, o.getBackoffCoefficient(), double.class));
setMaximumAttempts(OptionsUtils.merge(maximumAttempts, o.getMaximumAttempts(), int.class));
+ if (o.getMaximumJitterCoefficient() != -1.0) {
+ setMaximumJitterCoefficient(o.getMaximumJitterCoefficient());
+ }
doNotRetry = merge(doNotRetry, o.getDoNotRetry());
validateBuildWithDefaults();
return this;
@@ -272,10 +347,12 @@ private List