feat(pipeline): add ExponentialBackoff policy
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
This commit is contained in:
parent
aecbda674d
commit
798a5abd64
2 changed files with 77 additions and 0 deletions
46
src/TeamsISO.Engine/Pipeline/ExponentialBackoff.cs
Normal file
46
src/TeamsISO.Engine/Pipeline/ExponentialBackoff.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
namespace TeamsISO.Engine.Pipeline;
|
||||
|
||||
/// <summary>
|
||||
/// Exponential backoff policy used by <see cref="IsoPipeline"/>'s restart supervisor.
|
||||
/// Doubles the delay on each successive attempt and caps it at <c>cap</c>. After
|
||||
/// <c>maxAttempts</c> consecutive failures, <see cref="ShouldGiveUp"/> returns true.
|
||||
/// </summary>
|
||||
public sealed class ExponentialBackoff
|
||||
{
|
||||
private readonly int _maxAttempts;
|
||||
private readonly TimeSpan _initial;
|
||||
private readonly TimeSpan _cap;
|
||||
|
||||
public ExponentialBackoff(int maxAttempts, TimeSpan initial, TimeSpan cap)
|
||||
{
|
||||
if (maxAttempts <= 0) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
|
||||
if (initial <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(initial));
|
||||
if (cap < initial) throw new ArgumentOutOfRangeException(nameof(cap));
|
||||
_maxAttempts = maxAttempts;
|
||||
_initial = initial;
|
||||
_cap = cap;
|
||||
}
|
||||
|
||||
/// <summary>Default policy: 5 attempts, 1s initial, 30s cap.</summary>
|
||||
public static ExponentialBackoff Default { get; } =
|
||||
new(maxAttempts: 5, initial: TimeSpan.FromSeconds(1), cap: TimeSpan.FromSeconds(30));
|
||||
|
||||
public int MaxAttempts => _maxAttempts;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the delay before the <paramref name="attempt"/>-th retry.
|
||||
/// <paramref name="attempt"/> is 1-based.
|
||||
/// </summary>
|
||||
public TimeSpan NextDelay(int attempt)
|
||||
{
|
||||
if (attempt <= 0) throw new ArgumentOutOfRangeException(nameof(attempt));
|
||||
var multiplier = Math.Pow(2, attempt - 1);
|
||||
var delaySeconds = Math.Min(_initial.TotalSeconds * multiplier, _cap.TotalSeconds);
|
||||
return TimeSpan.FromSeconds(delaySeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true once <paramref name="attemptCount"/> exceeds <c>maxAttempts</c>.
|
||||
/// </summary>
|
||||
public bool ShouldGiveUp(int attemptCount) => attemptCount > _maxAttempts;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using TeamsISO.Engine.Pipeline;
|
||||
|
||||
namespace TeamsISO.Engine.Tests.Pipeline;
|
||||
|
||||
public class ExponentialBackoffTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1, 1)]
|
||||
[InlineData(2, 2)]
|
||||
[InlineData(3, 4)]
|
||||
[InlineData(4, 8)]
|
||||
[InlineData(5, 16)]
|
||||
[InlineData(6, 30)] // capped at 30s
|
||||
[InlineData(7, 30)]
|
||||
public void NextDelay_FollowsExponentialSequenceWithCap(int attempt, int expectedSeconds)
|
||||
{
|
||||
var policy = new ExponentialBackoff(maxAttempts: 5, initial: TimeSpan.FromSeconds(1), cap: TimeSpan.FromSeconds(30));
|
||||
policy.NextDelay(attempt).Should().Be(TimeSpan.FromSeconds(expectedSeconds));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, false)]
|
||||
[InlineData(4, false)]
|
||||
[InlineData(5, false)] // 5 attempts allowed
|
||||
[InlineData(6, true)] // give up after the 5th
|
||||
public void ShouldGiveUp_AfterMaxAttempts(int attemptCount, bool expectGiveUp)
|
||||
{
|
||||
var policy = new ExponentialBackoff(maxAttempts: 5, initial: TimeSpan.FromSeconds(1), cap: TimeSpan.FromSeconds(30));
|
||||
policy.ShouldGiveUp(attemptCount).Should().Be(expectGiveUp);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue