Skip to content

Commit 0ad410c

Browse files
author
Simon Ejsing
committed
Created class to implement retry with exponential backoff with unit tests
1 parent 6f4c245 commit 0ad410c

File tree

7 files changed

+225
-0
lines changed

7 files changed

+225
-0
lines changed

src/GitVersionCore.Tests/GitVersionCore.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@
113113
<Compile Include="ConfigProviderTests.cs" />
114114
<Compile Include="GitVersionContextTests.cs" />
115115
<Compile Include="Helpers\DirectoryHelper.cs" />
116+
<Compile Include="Mocks\MockThreadSleep.cs" />
117+
<Compile Include="RetryOperationExponentialBackoffTests.cs" />
116118
<Compile Include="Init\InitScenarios.cs" />
117119
<Compile Include="Init\InitStepsDefaultResponsesDoNotThrow.cs" />
118120
<Compile Include="Init\TestConsole.cs" />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using GitVersion.Helpers;
3+
4+
public class MockThreadSleep : IThreadSleep
5+
{
6+
private Action<int> Validator;
7+
8+
public MockThreadSleep(Action<int> validator = null)
9+
{
10+
this.Validator = validator;
11+
}
12+
13+
public void Sleep(int milliseconds)
14+
{
15+
if (Validator != null)
16+
{
17+
Validator(milliseconds);
18+
}
19+
}
20+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using System;
2+
using System.IO;
3+
using GitVersion.Helpers;
4+
using NUnit.Framework;
5+
using Shouldly;
6+
7+
[TestFixture]
8+
public class RetryOperationExponentialBackoffTests
9+
{
10+
[Test]
11+
public void RetryOperationThrowsWhenNegativeMaxRetries()
12+
{
13+
Action action = () => new RetryOperationExponentialBackoff<IOException>(new MockThreadSleep(), () => { }, -1);
14+
action.ShouldThrow<ArgumentOutOfRangeException>();
15+
}
16+
17+
[Test]
18+
public void RetryOperationThrowsWhenThreadSleepIsNull()
19+
{
20+
Action action = () => new RetryOperationExponentialBackoff<IOException>(null, () => { });
21+
action.ShouldThrow<ArgumentNullException>();
22+
}
23+
24+
[Test]
25+
public void OperationIsNotRetriedOnInvalidException()
26+
{
27+
Action operation = () =>
28+
{
29+
throw new Exception();
30+
};
31+
32+
var retryOperation = new RetryOperationExponentialBackoff<IOException>(new MockThreadSleep(), operation);
33+
Action action = () => retryOperation.Execute();
34+
action.ShouldThrow<Exception>();
35+
}
36+
37+
[Test]
38+
public void OperationIsRetriedOnIOException()
39+
{
40+
var operationCount = 0;
41+
42+
Action operation = () =>
43+
{
44+
operationCount++;
45+
if (operationCount < 2)
46+
{
47+
throw new IOException();
48+
}
49+
};
50+
51+
var retryOperation = new RetryOperationExponentialBackoff<IOException>(new MockThreadSleep(), operation);
52+
retryOperation.Execute();
53+
54+
operationCount.ShouldBe(2);
55+
}
56+
57+
[Test]
58+
public void OperationIsRetriedAMaximumNumberOfTimes()
59+
{
60+
const int numberOfRetries = 3;
61+
var operationCount = 0;
62+
63+
Action operation = () =>
64+
{
65+
operationCount++;
66+
throw new IOException();
67+
};
68+
69+
var retryOperation = new RetryOperationExponentialBackoff<IOException>(new MockThreadSleep(), operation, numberOfRetries);
70+
Action action = () => retryOperation.Execute();
71+
action.ShouldThrow<AggregateException>();
72+
73+
operationCount.ShouldBe(numberOfRetries + 1);
74+
}
75+
76+
[Test]
77+
public void OperationDelayDoublesBetweenRetries()
78+
{
79+
const int numberOfRetries = 3;
80+
var expectedSleepMSec = 500;
81+
var sleepCount = 0;
82+
83+
Action operation = () =>
84+
{
85+
throw new IOException();
86+
};
87+
88+
Action<int> validator = u =>
89+
{
90+
sleepCount++;
91+
u.ShouldBe(expectedSleepMSec);
92+
expectedSleepMSec *= 2;
93+
};
94+
95+
var retryOperation = new RetryOperationExponentialBackoff<IOException>(new MockThreadSleep(validator), operation, numberOfRetries);
96+
Action action = () => retryOperation.Execute();
97+
action.ShouldThrow<AggregateException>();
98+
99+
sleepCount.ShouldBe(numberOfRetries);
100+
}
101+
102+
[Test]
103+
public void TotalSleepTimeForSixRetriesIsAboutThirtySeconds()
104+
{
105+
const int numberOfRetries = 6;
106+
int totalSleep = 0;
107+
108+
Action operation = () =>
109+
{
110+
throw new IOException();
111+
};
112+
113+
Action<int> validator = u =>
114+
{
115+
totalSleep += u;
116+
};
117+
118+
var retryOperation = new RetryOperationExponentialBackoff<IOException>(new MockThreadSleep(validator), operation, numberOfRetries);
119+
Action action = () => retryOperation.Execute();
120+
action.ShouldThrow<AggregateException>();
121+
122+
// Exact number is 31,5 seconds
123+
totalSleep.ShouldBe(31500);
124+
}
125+
}

src/GitVersionCore/GitVersionCore.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@
122122
<Compile Include="GitVersionCache.cs" />
123123
<Compile Include="Helpers\FileSystem.cs" />
124124
<Compile Include="Helpers\IFileSystem.cs" />
125+
<Compile Include="Helpers\IThreadSleep.cs" />
126+
<Compile Include="Helpers\RetryOperationExponentialBackoff.cs" />
125127
<Compile Include="Helpers\ServiceMessageEscapeHelper.cs" />
128+
<Compile Include="Helpers\ThreadSleep.cs" />
126129
<Compile Include="IncrementStrategyFinder.cs" />
127130
<Compile Include="OutputVariables\VersionVariables.cs" />
128131
<Compile Include="SemanticVersionExtensions.cs" />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace GitVersion.Helpers
2+
{
3+
public interface IThreadSleep
4+
{
5+
void Sleep(int milliseconds);
6+
}
7+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace GitVersion.Helpers
7+
{
8+
internal class RetryOperationExponentialBackoff<T> where T : Exception
9+
{
10+
private IThreadSleep ThreadSleep;
11+
private Action Operation;
12+
private int MaxRetries;
13+
14+
public RetryOperationExponentialBackoff(IThreadSleep threadSleep, Action operation, int maxRetries = 5)
15+
{
16+
if (threadSleep == null)
17+
throw new ArgumentNullException("threadSleep");
18+
if (maxRetries < 0)
19+
throw new ArgumentOutOfRangeException("maxRetries");
20+
21+
this.ThreadSleep = threadSleep;
22+
this.Operation = operation;
23+
this.MaxRetries = maxRetries;
24+
}
25+
26+
public void Execute()
27+
{
28+
var exceptions = new List<Exception>();
29+
30+
int tries = 0;
31+
int sleepMSec = 500;
32+
33+
while (tries <= MaxRetries)
34+
{
35+
tries++;
36+
37+
try
38+
{
39+
Operation();
40+
break;
41+
}
42+
catch (T e)
43+
{
44+
exceptions.Add(e);
45+
if (tries > MaxRetries)
46+
{
47+
throw new AggregateException("Operation failed after maximum number of retries were exceeded.", exceptions);
48+
}
49+
}
50+
51+
ThreadSleep.Sleep(sleepMSec);
52+
sleepMSec *= 2;
53+
}
54+
}
55+
}
56+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace GitVersion.Helpers
2+
{
3+
using System.Threading;
4+
5+
internal class ThreadSleep : IThreadSleep
6+
{
7+
public void Sleep(int milliseconds)
8+
{
9+
Thread.Sleep(milliseconds);
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)