-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from reisenberger/addNewJitter
Add new jitter algorithm from Polly issue 530
- Loading branch information
Showing
5 changed files
with
251 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
src/Polly.Contrib.WaitAndRetry.Specs/DecorrelatedJitterBackoffV2Specs.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
using FluentAssertions; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace Polly.Contrib.WaitAndRetry.Specs | ||
{ | ||
public sealed class DecorrelatedJitterBackoffV2Specs | ||
{ | ||
private readonly ITestOutputHelper testOutputHelper; | ||
|
||
public DecorrelatedJitterBackoffV2Specs(ITestOutputHelper testOutputHelper) | ||
{ | ||
this.testOutputHelper = testOutputHelper; | ||
} | ||
|
||
[Fact] | ||
public void Backoff_WithMeanFirstDelayLessThanZero_ThrowsException() | ||
{ | ||
// Arrange | ||
var medianFirstDelay = new TimeSpan(-1); | ||
const int retryCount = 3; | ||
const bool fastFirst = false; | ||
const int seed = 1; | ||
|
||
// Act | ||
Action act = () => Backoff.DecorrelatedJitterBackoffV2(medianFirstDelay, retryCount, seed, fastFirst); | ||
|
||
// Assert | ||
act.Should().Throw<ArgumentOutOfRangeException>() | ||
.And.ParamName.Should().Be("medianFirstRetryDelay"); | ||
} | ||
|
||
[Fact] | ||
public void Backoff_WithRetryCountLessThanZero_ThrowsException() | ||
{ | ||
// Arrange | ||
var medianFirstDelay = TimeSpan.FromSeconds(1); | ||
const int retryCount = -1; | ||
const bool fastFirst = false; | ||
const int seed = 1; | ||
|
||
// Act | ||
Action act = () => Backoff.DecorrelatedJitterBackoffV2(medianFirstDelay, retryCount, seed, fastFirst); | ||
|
||
// Assert | ||
act.Should().Throw<ArgumentOutOfRangeException>() | ||
.And.ParamName.Should().Be("retryCount"); | ||
} | ||
|
||
[Fact] | ||
public void Backoff_WithRetryEqualToZero_ResultIsEmpty() | ||
{ | ||
// Arrange | ||
var medianFirstDelay = TimeSpan.FromSeconds(2); | ||
const int retryCount = 0; | ||
const bool fastFirst = false; | ||
const int seed = 1; | ||
|
||
// Act | ||
IEnumerable<TimeSpan> result = Backoff.DecorrelatedJitterBackoffV2(medianFirstDelay, retryCount, seed, fastFirst); | ||
|
||
// Assert | ||
result.Should().NotBeNull(); | ||
result.Should().BeEmpty(); | ||
} | ||
|
||
[Fact] | ||
public void Backoff_WithFastFirstEqualToTrue_ResultIsZero() | ||
{ | ||
// Arrange | ||
var medianFirstDelay = TimeSpan.FromSeconds(2); | ||
const int retryCount = 10; | ||
const bool fastFirst = true; | ||
const int seed = 1; | ||
|
||
// Act | ||
IEnumerable<TimeSpan> result = Backoff.DecorrelatedJitterBackoffV2(medianFirstDelay, retryCount, seed, fastFirst); | ||
|
||
// Assert | ||
result.Should().NotBeNull(); | ||
result = result.ToList(); | ||
result.Should().HaveCount(retryCount); | ||
|
||
bool first = true; | ||
int t = 0; | ||
foreach (TimeSpan timeSpan in result) | ||
{ | ||
if (first) | ||
{ | ||
timeSpan.Should().Be(TimeSpan.FromMilliseconds(0)); | ||
first = false; | ||
} | ||
else | ||
{ | ||
t++; | ||
AssertOnRetryDelayForTry(t, timeSpan, medianFirstDelay); | ||
} | ||
} | ||
} | ||
|
||
[Fact] | ||
public void Backoff_ResultIsInRange() | ||
{ | ||
// Arrange | ||
var medianFirstDelay = TimeSpan.FromSeconds(1); | ||
const int retryCount = 6; | ||
const bool fastFirst = false; | ||
const int seed = 23456; | ||
|
||
// Act | ||
IEnumerable<TimeSpan> result = Backoff.DecorrelatedJitterBackoffV2(medianFirstDelay, retryCount, seed, fastFirst); | ||
|
||
// Assert | ||
result.Should().NotBeNull(); | ||
result = result.ToList(); | ||
result.Should().HaveCount(retryCount); | ||
|
||
int t = 0; | ||
foreach (TimeSpan timeSpan in result) | ||
{ | ||
t++; | ||
AssertOnRetryDelayForTry(t, timeSpan, medianFirstDelay); | ||
} | ||
} | ||
|
||
public static IEnumerable<object[]> SeedRange => Enumerable.Range(0, 1000).Select(o => new object[] {o}).ToArray(); | ||
|
||
[Theory] | ||
[MemberData(nameof(SeedRange))] | ||
public void Backoff_ResultIsInRange_WideTest(int seed) | ||
{ | ||
// Arrange | ||
var medianFirstDelay = TimeSpan.FromSeconds(3); | ||
const int retryCount = 6; | ||
const bool fastFirst = false; | ||
|
||
// Act | ||
IEnumerable<TimeSpan> result = Backoff.DecorrelatedJitterBackoffV2(medianFirstDelay, retryCount, seed, fastFirst); | ||
|
||
// Assert | ||
result.Should().NotBeNull(); | ||
result = result.ToList(); | ||
result.Should().HaveCount(retryCount); | ||
|
||
int t = 0; | ||
foreach (TimeSpan timeSpan in result) | ||
{ | ||
t++; | ||
AssertOnRetryDelayForTry(t, timeSpan, medianFirstDelay); | ||
} | ||
} | ||
|
||
private void AssertOnRetryDelayForTry(int t, TimeSpan calculatedDelay, TimeSpan medianFirstDelay) | ||
{ | ||
/*testOutputHelper.WriteLine($"Try {t}, delay: {calculatedDelay.TotalSeconds} seconds; given median first delay {medianFirstDelay.TotalSeconds} seconds.");*/ | ||
|
||
calculatedDelay.Should().BeGreaterOrEqualTo(TimeSpan.Zero); | ||
|
||
int upperLimitFactor = t < 2 ? (int)Math.Pow(2, t + 1) : (int)(Math.Pow(2, t + 1) - Math.Pow(2, t - 1)); | ||
|
||
calculatedDelay.Should().BeLessOrEqualTo(TimeSpan.FromTicks(medianFirstDelay.Ticks * upperLimitFactor)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
src/Polly.Contrib.WaitAndRetry/Backoff.DecorrelatedJitterV2.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
|
||
namespace Polly.Contrib.WaitAndRetry | ||
{ | ||
partial class Backoff // .DecorrelatedJitterV2 | ||
{ | ||
/// <summary> | ||
/// Generates sleep durations in an exponentially backing-off, jittered manner, making sure to mitigate any correlations. | ||
/// For example: 850ms, 1455ms, 3060ms. | ||
/// Per discussion in Polly issue 530, the jitter of this implementation exhibits fewer spikes and a smoother distribution than the AWS jitter formula. | ||
/// </summary> | ||
/// <param name="medianFirstRetryDelay">The median delay to target before the first retry, call it f (= f * 2^0). | ||
/// Choose this value both to approximate the first delay, and to scale the remainder of the series. | ||
/// Subsequent retries will (over a large sample size) have a median approximating retries at time f * 2^1, f * 2^2 ... f * 2^t etc for try t. | ||
/// The actual amount of delay-before-retry for try t may be distributed between 0 and f * (2^(t+1) - 2^(t-1)) for t >= 2; | ||
/// or between 0 and f * 2^(t+1), for t is 0 or 1.</param> | ||
/// <param name="retryCount">The maximum number of retries to use, in addition to the original call.</param> | ||
/// <param name="seed">An optional <see cref="Random"/> seed to use. | ||
/// If not specified, will use a shared instance with a random seed, per Microsoft recommendation for maximum randomness.</param> | ||
/// <param name="fastFirst">Whether the first retry will be immediate or not.</param> | ||
public static IEnumerable<TimeSpan> DecorrelatedJitterBackoffV2(TimeSpan medianFirstRetryDelay, int retryCount, int? seed = null, bool fastFirst = false) | ||
{ | ||
if (medianFirstRetryDelay < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(medianFirstRetryDelay), medianFirstRetryDelay, "should be >= 0ms"); | ||
if (retryCount < 0) throw new ArgumentOutOfRangeException(nameof(retryCount), retryCount, "should be >= 0"); | ||
|
||
if (retryCount == 0) | ||
return Empty(); | ||
|
||
return Enumerate(medianFirstRetryDelay, retryCount, fastFirst, new ConcurrentRandom(seed)); | ||
|
||
// The original author/credit for this jitter formula is @george-polevoy . Jitter formula used with permission as described at https://github.com/App-vNext/Polly/issues/530#issuecomment-526555979 | ||
// Minor adaptations (pFactor = 4.0 and rpScalingFactor = 1 / 1.4d) by @reisenberger, to scale the formula output for easier parameterisation to users. | ||
|
||
IEnumerable<TimeSpan> Enumerate(TimeSpan scaleFirstTry, int maxRetries, bool fast, ConcurrentRandom random) | ||
{ | ||
// A factor used within the formula to help smooth the first calculated delay. | ||
const double pFactor = 4.0; | ||
|
||
// A factor used to scale the median values of the retry times generated by the formula to be _near_ whole seconds, to aid Polly user comprehension. | ||
// This factor allows the median values to fall approximately at 1, 2, 4 etc seconds, instead of 1.4, 2.8, 5.6, 11.2. | ||
const double rpScalingFactor = 1 / 1.4d; | ||
|
||
int i = 0; | ||
if (fast) | ||
{ | ||
i++; | ||
yield return TimeSpan.Zero; | ||
} | ||
|
||
long targetTicksFirstDelay = scaleFirstTry.Ticks; | ||
|
||
double prev = 0.0; | ||
for (; i < maxRetries; i++) | ||
{ | ||
double t = (double)i + random.NextDouble(); | ||
double next = Math.Pow(2, t) * Math.Tanh(Math.Sqrt(pFactor * t)); | ||
|
||
double formulaIntrinsicValue = next - prev; | ||
yield return TimeSpan.FromTicks((long)(formulaIntrinsicValue * rpScalingFactor * targetTicksFirstDelay)); | ||
|
||
prev = next; | ||
} | ||
|
||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters