Skip to content

Commit 842f5dd

Browse files
committed
fix
1 parent af874f0 commit 842f5dd

File tree

6 files changed

+167
-2
lines changed

6 files changed

+167
-2
lines changed

Eocron.DependencyInjection.Interceptors/DecoratorChainExtensions.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
2+
using System.Reflection;
23
using Castle.DynamicProxy;
4+
using Eocron.DependencyInjection.Interceptors.Retry;
5+
using Microsoft.Extensions.Caching.Memory;
36
using Microsoft.Extensions.DependencyInjection;
47
using Microsoft.Extensions.Logging;
58

@@ -41,5 +44,54 @@ public static DecoratorChain AddRetry(this DecoratorChain decoratorChain,
4144
sp.GetService<ILoggerFactory>()?.CreateLogger(decoratorChain.ServiceType.Name)));
4245
return decoratorChain;
4346
}
47+
48+
public static DecoratorChain AddConstantBackoff(this DecoratorChain decoratorChain,
49+
int maxAttempts,
50+
TimeSpan retryInterval,
51+
bool jittered = false,
52+
Func<Exception, bool> isRetryable = null)
53+
{
54+
return decoratorChain.AddRetry(
55+
(c, ex) => c <= maxAttempts && (isRetryable?.Invoke(ex) ?? true),
56+
(c, _) => ConstantBackoff.Calculate(StaticRandom.Value, retryInterval, jittered));
57+
}
58+
59+
public static DecoratorChain AddExponentialBackoff(this DecoratorChain decoratorChain,
60+
int maxAttempts,
61+
TimeSpan minPropagationDuration,
62+
TimeSpan maxPropagationDuration,
63+
bool jittered = true,
64+
Func<Exception, bool> isRetryable = null)
65+
{
66+
return decoratorChain.AddRetry(
67+
(c, ex) => c <= maxAttempts && (isRetryable?.Invoke(ex) ?? true),
68+
(c, _) => CorrelatedExponentialBackoff.Calculate(StaticRandom.Value, c, minPropagationDuration, maxPropagationDuration, jittered));
69+
}
70+
71+
public static DecoratorChain AddSlidingTimeoutCache(this DecoratorChain decoratorChain,
72+
Func<MethodInfo, object[], object> keyProvider,
73+
TimeSpan cacheDuration)
74+
{
75+
if (cacheDuration <= TimeSpan.Zero)
76+
return decoratorChain;
77+
78+
decoratorChain.AddInterceptor((sp) => new MemoryCacheAsyncInterceptor(sp.GetRequiredService<IMemoryCache>(),
79+
keyProvider,
80+
(_,_,ce)=> ce.SetSlidingExpiration(cacheDuration)));
81+
return decoratorChain;
82+
}
83+
84+
public static DecoratorChain AddTimeoutCache(this DecoratorChain decoratorChain,
85+
Func<MethodInfo, object[], object> keyProvider,
86+
TimeSpan cacheDuration)
87+
{
88+
if (cacheDuration <= TimeSpan.Zero)
89+
return decoratorChain;
90+
91+
decoratorChain.AddInterceptor((sp) => new MemoryCacheAsyncInterceptor(sp.GetRequiredService<IMemoryCache>(),
92+
keyProvider,
93+
(_,_,ce)=> ce.SetAbsoluteExpiration(cacheDuration)));
94+
return decoratorChain;
95+
}
4496
}
4597
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
3+
namespace Eocron.DependencyInjection.Interceptors.Retry
4+
{
5+
public static class ConstantBackoff
6+
{
7+
public static TimeSpan Calculate(Random random, TimeSpan interval, bool jittered)
8+
{
9+
if(interval <= TimeSpan.Zero)
10+
return TimeSpan.Zero;
11+
if (!jittered)
12+
return interval;
13+
var stepMs = random.Next((int)interval.TotalMilliseconds);
14+
return TimeSpan.FromMilliseconds(stepMs);
15+
}
16+
}
17+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
3+
namespace Eocron.DependencyInjection.Interceptors.Retry
4+
{
5+
public static class CorrelatedExponentialBackoff
6+
{
7+
public static TimeSpan Calculate(Random random, int attempt, TimeSpan propagationDuration,
8+
TimeSpan maxPropagationDuration, bool jittered)
9+
{
10+
if(attempt < 0)
11+
throw new ArgumentOutOfRangeException(nameof(attempt));
12+
if(propagationDuration >= maxPropagationDuration)
13+
throw new ArgumentOutOfRangeException(nameof(propagationDuration), "Minimum propagation duration must be less than max propagation duration.");
14+
if (maxPropagationDuration <= TimeSpan.Zero)
15+
throw new ArgumentOutOfRangeException(nameof(maxPropagationDuration), "Maximum propagation duration must be greater than zero.");
16+
17+
var minPropagationMs = Math.Max((int)propagationDuration.TotalMilliseconds, 5); //min time it takes to process single request
18+
var maxPropagationMs = Math.Max(minPropagationMs, (int)maxPropagationDuration.TotalMilliseconds); //max time it takes to process single request
19+
var maxAttemptExp = (int)Math.Floor(Math.Log2((maxPropagationMs - minPropagationMs) / minPropagationMs));
20+
if (maxAttemptExp >= attempt)
21+
{
22+
var duration = minPropagationMs * (1 << attempt);
23+
return TimeSpan.FromMilliseconds(jittered ? random.Next(duration) : duration);
24+
}
25+
else
26+
{
27+
return ConstantBackoff.Calculate(random, maxPropagationDuration, jittered);
28+
}
29+
}
30+
}
31+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace Eocron.DependencyInjection.Interceptors.Retry
5+
{
6+
public static class StaticRandom
7+
{
8+
private static int _seed = Environment.TickCount;
9+
10+
private static readonly ThreadLocal<Random> Random = new(() => new Random(Interlocked.Increment(ref _seed)));
11+
public static Random Value => Random.Value;
12+
13+
public static double NextDouble()
14+
{
15+
return Random.Value.NextDouble();
16+
}
17+
18+
public static int Next(int maxValue)
19+
{
20+
return Random.Value.Next(maxValue);
21+
}
22+
23+
public static int Next(int minValue, int maxValue)
24+
{
25+
return Random.Value.Next(minValue, maxValue);
26+
}
27+
}
28+
}

Eocron.DependencyInjection.Tests/RetryUntilConditionInterceptorTests.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
using System;
2+
using System.Linq;
23
using System.Threading;
34
using System.Threading.Tasks;
45
using Castle.DynamicProxy;
56
using Eocron.DependencyInjection.Interceptors;
7+
using Eocron.DependencyInjection.Interceptors.Retry;
68
using FluentAssertions;
79
using Moq;
810
using NUnit.Framework;
911

1012
namespace Eocron.DependencyInjection.Tests
1113
{
1214
[TestFixture]
13-
[Ignore("Not yet tested")]
1415
public class RetryUntilConditionInterceptorTests
1516
{
1617
private IAsyncInterceptor _interceptor;
@@ -22,6 +23,43 @@ public void Setup()
2223
_interceptor = new RetryUntilConditionAsyncInterceptor((_, _) => true, (_, _) => TimeSpan.Zero, TestConsoleLogger.Instance);
2324
_interceptorWithDelay = new RetryUntilConditionAsyncInterceptor((_, _) => true, (_, _) => TimeSpan.FromSeconds(10), TestConsoleLogger.Instance);
2425
}
26+
27+
[Test]
28+
public void CorrelatedExponentialBackoff_Check()
29+
{
30+
var rnd = new Random(42);
31+
var expectedMs = new[] {5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480, 40960, 60000, 60000, 60000, 60000, 60000, 60000};
32+
var actualMs = Enumerable.Range(0, 20).Select(x=> (int)CorrelatedExponentialBackoff.Calculate(rnd, x, TimeSpan.Zero, TimeSpan.FromSeconds(60), false).TotalMilliseconds).ToArray();
33+
actualMs.Should().Equal(expectedMs);
34+
}
35+
36+
[Test]
37+
public void CorrelatedExponentialBackoffJittered_Check()
38+
{
39+
var rnd = new Random(42);
40+
var expectedMs = new[] {3, 1, 2, 20, 13, 42, 231, 328, 222, 1948, 1201, 2634, 10354, 13116, 22858, 15614, 31047, 2119, 48848, 34631};
41+
var actualMs = Enumerable.Range(0, 20).Select(x=> (int)CorrelatedExponentialBackoff.Calculate(rnd, x, TimeSpan.Zero, TimeSpan.FromSeconds(60), true).TotalMilliseconds).ToArray();
42+
actualMs.Should().Equal(expectedMs);
43+
}
44+
45+
[Test]
46+
public void ConstantBackoff_Check()
47+
{
48+
var rnd = new Random(42);
49+
var expectedMs = new[] {60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000};
50+
var actualMs = Enumerable.Range(0, 20).Select(x=> (int)ConstantBackoff.Calculate(rnd, TimeSpan.FromSeconds(60), false).TotalMilliseconds).ToArray();
51+
actualMs.Should().Equal(expectedMs);
52+
}
53+
54+
[Test]
55+
public void ConstantBackoffJittered_Check()
56+
{
57+
var rnd = new Random(42);
58+
var expectedMs = new[] {40086, 8454, 7531, 31365, 10106, 15755, 43464, 30775, 10419, 45675, 14075, 15439, 30336, 19213, 22858, 15614, 31047, 2119, 48848, 34631};
59+
var actualMs = Enumerable.Range(0, 20).Select(x=> (int)ConstantBackoff.Calculate(rnd, TimeSpan.FromSeconds(60), true).TotalMilliseconds).ToArray();
60+
actualMs.Should().Equal(expectedMs);
61+
}
62+
2563
[Test]
2664
public async Task WorkAsync()
2765
{

Eocron.DependencyInjection.Tests/TimeoutInterceptorTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
namespace Eocron.DependencyInjection.Tests
1212
{
1313
[TestFixture]
14-
[Ignore("Not yet tested")]
1514
public class TimeoutInterceptorTests
1615
{
1716
private IAsyncInterceptor _interceptor;

0 commit comments

Comments
 (0)