Skip to content

Commit 21c740a

Browse files
authored
Add retry to gRPC calls that failed due to transient errors (#714)
1 parent 90e4732 commit 21c740a

9 files changed

Lines changed: 972 additions & 252 deletions
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
namespace Microsoft.DurableTask.Worker.Grpc;
77

88
/// <summary>
9-
/// Helpers for computing reconnect backoff delays in the gRPC worker.
9+
/// Helpers for computing reconnect and retry backoff delays in the gRPC worker.
1010
/// </summary>
11-
static class ReconnectBackoff
11+
static class GrpcBackoff
1212
{
1313
/// <summary>
1414
/// Creates a random source for reconnect jitter using an explicit random seed so multiple workers on
@@ -32,10 +32,11 @@ public static Random CreateRandom()
3232
/// <param name="baseDelay">The base delay used for the exponential growth.</param>
3333
/// <param name="cap">The maximum delay before jitter is applied.</param>
3434
/// <param name="random">The random source used for jitter.</param>
35+
/// <param name="fullJitter">If true, applies full jitter. If false, applies a smaller jitter that is biased towards the upper bound.</param>
3536
/// <returns>The computed jittered delay.</returns>
36-
public static TimeSpan Compute(int attempt, TimeSpan baseDelay, TimeSpan cap, Random random)
37+
public static TimeSpan Compute(int attempt, TimeSpan baseDelay, TimeSpan cap, Random random, bool fullJitter)
3738
{
38-
if (baseDelay <= TimeSpan.Zero)
39+
if (baseDelay <= TimeSpan.Zero || cap <= TimeSpan.Zero)
3940
{
4041
return TimeSpan.Zero;
4142
}
@@ -48,13 +49,13 @@ public static TimeSpan Compute(int attempt, TimeSpan baseDelay, TimeSpan cap, Ra
4849
// Cap the exponent to avoid overflow in 2^attempt for pathological attempt values.
4950
int safeAttempt = Math.Min(attempt, 30);
5051

51-
double capMs = Math.Max(0, cap.TotalMilliseconds);
5252
double exponentialMs = baseDelay.TotalMilliseconds * Math.Pow(2, safeAttempt);
53-
double upperBoundMs = Math.Min(capMs, exponentialMs);
53+
double upperBoundMs = Math.Min(cap.TotalMilliseconds, exponentialMs);
54+
55+
double jitteredMs = fullJitter
56+
? random.NextDouble() * upperBoundMs
57+
: upperBoundMs + (random.NextDouble() * (upperBoundMs * .2));
5458

55-
// Full jitter intentionally allows any value in the retry window. The wide spread keeps many
56-
// workers that saw the same outage from reconnecting in lockstep against the backend.
57-
double jitteredMs = random.NextDouble() * upperBoundMs;
5859
return TimeSpan.FromMilliseconds(jitteredMs);
5960
}
6061
}

src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs

Lines changed: 163 additions & 94 deletions
Large diffs are not rendered by default.

src/Worker/Grpc/GrpcDurableTaskWorkerOptions.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,29 @@ internal class InternalOptions
135135
/// </summary>
136136
public TimeSpan ReconnectBackoffCap { get; set; } = TimeSpan.FromSeconds(30);
137137

138+
/// <summary>
139+
/// Gets or sets the maximum number of attempts the worker will make when retrying a transient
140+
/// gRPC call (such as completing or abandoning a work item). Once this many attempts have failed,
141+
/// the most recent exception is rethrown. Defaults to 10.
142+
/// </summary>
143+
public int TransientRetryMaxAttempts { get; set; } = 10;
144+
145+
/// <summary>
146+
/// Gets or sets the initial delay used when computing exponential backoff between retries of a
147+
/// transient gRPC call. The delay doubles after each failed attempt, and the exponential component
148+
/// is capped at <see cref="TransientRetryBackoffCap"/> before jitter is applied. In the default
149+
/// biased-jitter mode, the final delay may therefore slightly exceed
150+
/// <see cref="TransientRetryBackoffCap"/>. Defaults to 200 ms.
151+
/// </summary>
152+
public TimeSpan TransientRetryBackoffBase { get; set; } = TimeSpan.FromMilliseconds(200);
153+
154+
/// <summary>
155+
/// Gets or sets the cap applied to the exponential backoff component between retries of a transient
156+
/// gRPC call before jitter is applied. In the default biased-jitter mode, the final computed delay
157+
/// may be slightly greater than this value. Defaults to 15 seconds.
158+
/// </summary>
159+
public TimeSpan TransientRetryBackoffCap { get; set; } = TimeSpan.FromSeconds(15);
160+
138161
/// <summary>
139162
/// Gets or sets an optional callback invoked when the worker requests a fresh gRPC channel after
140163
/// repeated connect failures. The callback receives the previously-used channel and should return

src/Worker/Grpc/Logs.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ static partial class Logs
9999
public static partial void ReceivedHealthPing(this ILogger logger);
100100

101101
[LoggerMessage(EventId = 76, Level = LogLevel.Information, Message = "Work-item stream ended by the backend (graceful close). Will reconnect.")]
102-
public static partial void StreamEndedByPeer(this ILogger logger);
102+
public static partial void StreamEndedByPeer(this ILogger logger);
103+
104+
[LoggerMessage(EventId = 77, Level = LogLevel.Warning, Message = "Transient gRPC error for '{OperationName}'. Attempt {Attempt} of {MaxAttempts}. Retrying in {BackoffMs} ms. StatusCode={StatusCode}")]
105+
public static partial void TransientGrpcRetry(this ILogger logger, string operationName, int attempt, int maxAttempts, double backoffMs, int statusCode, Exception exception);
103106
}
104107
}

0 commit comments

Comments
 (0)