Skip to content

Commit 3812c75

Browse files
Fix exponential delay behavior (Azure#40869)
* Fix exponential delay behavior * Update change log * Fix RetryPolicy call to delay strategy * update changelog
1 parent 8f0fa30 commit 3812c75

File tree

5 files changed

+71
-5
lines changed

5 files changed

+71
-5
lines changed

sdk/core/Azure.Core/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
### Bugs Fixed
1010

11+
- Fixed exponential retry behavior so that delay milliseconds greater than `Int32.MaxValue` do not trigger an exception.
12+
- Fixed `DelayStrategy` behavior to no longer shift the delay to be used over by one attempt. Previously, the first delay would be what should have been used for the second, and the second was what should have been used for the third, etc. Note, this would only be observed when using `DelayStrategy` outside of a `RetryPolicy` or `RetryOptions`.
13+
1114
### Other Changes
1215
- Remove targets for .NET Core 2.1 and .NET 5 since they are out of support. Azure.Core is no longer compatible with .NET Core 2.1 after removal of target. The remaining targets are unchanged.
1316

sdk/core/Azure.Core/src/DelayStrategy.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ public abstract class DelayStrategy
1919
private readonly double _minJitterFactor;
2020
private readonly double _maxJitterFactor;
2121
private readonly TimeSpan _maxDelay;
22+
internal const double DefaultJitterFactor = 0.2;
2223

2324
/// <summary>
2425
/// Constructs a new instance of <see cref="DelayStrategy"/>. This constructor can be used by derived classes to customize the jitter factor and max delay.
2526
/// </summary>
2627
/// <param name="maxDelay">The max delay value to apply on an individual delay.</param>
2728
/// <param name="jitterFactor">The jitter factor to apply to each delay. For example, if the delay is 1 second with a jitterFactor of 0.2, the actual
2829
/// delay used will be a random double between 0.8 and 1.2. If set to 0, no jitter will be applied.</param>
29-
protected DelayStrategy(TimeSpan? maxDelay = default, double jitterFactor = 0.2)
30+
protected DelayStrategy(TimeSpan? maxDelay = default, double jitterFactor = DefaultJitterFactor)
3031
{
3132
// use same defaults as RetryOptions
3233
_minJitterFactor = 1 - jitterFactor;
@@ -82,7 +83,14 @@ public TimeSpan GetNextDelay(Response? response, int retryNumber) =>
8283

8384
private TimeSpan ApplyJitter(TimeSpan delay)
8485
{
85-
return TimeSpan.FromMilliseconds(_random.Next((int)(delay.TotalMilliseconds * _minJitterFactor), (int)(delay.TotalMilliseconds * _maxJitterFactor)));
86+
// get a random double between 0 and 1
87+
double randomDouble = _random.NextDouble();
88+
89+
// scale the double by the jitter range and then add it to the min
90+
randomDouble = randomDouble * (_maxJitterFactor - _minJitterFactor) + _minJitterFactor;
91+
92+
// apply the jitter to the delay
93+
return TimeSpan.FromMilliseconds(delay.TotalMilliseconds * randomDouble);
8694
}
8795

8896
/// <summary>

sdk/core/Azure.Core/src/ExponentialDelayStrategy.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public ExponentialDelayStrategy(
1919
}
2020

2121
protected override TimeSpan GetNextDelayCore(Response? response, int retryNumber) =>
22-
TimeSpan.FromMilliseconds((1 << retryNumber) * _delay.TotalMilliseconds);
22+
TimeSpan.FromMilliseconds((1 << (retryNumber - 1)) * _delay.TotalMilliseconds);
2323
}
24-
}
24+
}

sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ private TimeSpan GetNextDelayInternal(HttpMessage message)
264264
{
265265
return _delayStrategy.GetNextDelay(
266266
message.HasResponse ? message.Response : default,
267-
message.RetryNumber);
267+
message.RetryNumber + 1);
268268
}
269269
}
270270
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Azure.Core.TestFramework;
6+
using NUnit.Framework;
7+
8+
namespace Azure.Core.Tests
9+
{
10+
public class ExponentialDelayStrategyTests
11+
{
12+
[Test]
13+
public void ExponentialDelayIsWithinBoundsOfJitter()
14+
{
15+
var initialDelay = TimeSpan.FromSeconds(1);
16+
var strategy = DelayStrategy.CreateExponentialDelayStrategy(initialDelay, maxDelay: TimeSpan.MaxValue);
17+
var response = new MockResponse(400);
18+
for (int retryNumber = 1; retryNumber <= 30; retryNumber++)
19+
{
20+
AssertDelay(initialDelay, strategy.GetNextDelay(response, retryNumber), retryNumber);
21+
}
22+
}
23+
24+
[Test]
25+
public void ExponentialDelayRespectsMaxValue()
26+
{
27+
var initialDelay = TimeSpan.FromSeconds(1);
28+
var maxDelay = TimeSpan.FromSeconds(10);
29+
var strategy = DelayStrategy.CreateExponentialDelayStrategy(initialDelay, maxDelay: maxDelay);
30+
var response = new MockResponse(400);
31+
var delay = strategy.GetNextDelay(response, 30);
32+
Assert.LessOrEqual(delay.TotalMilliseconds, maxDelay.TotalMilliseconds);
33+
}
34+
35+
[Test]
36+
public void ExponentialDelayMillisecondsCanExceedMaxInt()
37+
{
38+
var initialDelay = TimeSpan.FromSeconds(1);
39+
var strategy = DelayStrategy.CreateExponentialDelayStrategy(initialDelay, maxDelay: TimeSpan.MaxValue);
40+
var response = new MockResponse(400);
41+
AssertDelay(initialDelay, strategy.GetNextDelay(response, 30), 30);
42+
}
43+
44+
private static void AssertDelay(TimeSpan initialDelay, TimeSpan currentDelay, int retryNumber, TimeSpan? maxDelay = default)
45+
{
46+
var max = maxDelay ?? TimeSpan.MaxValue;
47+
Assert.GreaterOrEqual(
48+
currentDelay.TotalMilliseconds,
49+
Math.Pow((1 - DelayStrategy.DefaultJitterFactor), retryNumber) * (1 << (retryNumber - 1)) * initialDelay.TotalMilliseconds);
50+
Assert.LessOrEqual(
51+
currentDelay.TotalMilliseconds,
52+
Math.Min(max.TotalMilliseconds, Math.Pow(1 + DelayStrategy.DefaultJitterFactor, retryNumber) * (1 << (retryNumber - 1)) * initialDelay.TotalMilliseconds));
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)