Skip to content

Commit 589f50b

Browse files
committed
Added SimpleRetryHandler
1 parent 3541117 commit 589f50b

3 files changed

Lines changed: 266 additions & 1 deletion

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net;
4+
using System.Threading.Tasks;
5+
using Moq;
6+
using NUnit.Framework;
7+
using SpotifyAPI.Web.Http;
8+
9+
namespace SpotifyAPI.Web
10+
{
11+
[TestFixture]
12+
public class SimpleRetryHandlerTest
13+
{
14+
[Test]
15+
public async Task HandleRetry_TooManyRequestsWithNoSuccess()
16+
{
17+
var sleep = new Mock<Func<int, Task>>();
18+
19+
var request = new Mock<IRequest>();
20+
var initialResponse = new Mock<IResponse>();
21+
initialResponse.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.TooManyRequests);
22+
initialResponse.SetupGet(r => r.Headers).Returns(new Dictionary<string, string> {
23+
{ "Retry-After", "50" }
24+
});
25+
26+
var retryCalled = 0;
27+
Task<IResponse> retry(IRequest request)
28+
{
29+
retryCalled++;
30+
return Task.FromResult(initialResponse.Object);
31+
}
32+
33+
var handler = new SimpleRetryHandler(sleep.Object)
34+
{
35+
TooManyRequestsConsumesARetry = true,
36+
RetryTimes = 2
37+
};
38+
var response = await handler.HandleRetry(request.Object, initialResponse.Object, retry);
39+
40+
Assert.AreEqual(2, retryCalled);
41+
Assert.AreEqual(initialResponse.Object, response);
42+
sleep.Verify(s => s(50000), Times.Exactly(2));
43+
44+
}
45+
46+
[Test]
47+
public async Task HandleRetry_TooManyRetriesWithSuccess()
48+
{
49+
var sleep = new Mock<Func<int, Task>>();
50+
51+
var request = new Mock<IRequest>();
52+
var initialResponse = new Mock<IResponse>();
53+
initialResponse.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.TooManyRequests);
54+
initialResponse.SetupGet(r => r.Headers).Returns(new Dictionary<string, string> {
55+
{ "Retry-After", "50" }
56+
});
57+
var successResponse = new Mock<IResponse>();
58+
successResponse.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.OK);
59+
60+
var retryCalled = 0;
61+
Task<IResponse> retry(IRequest request)
62+
{
63+
retryCalled++;
64+
return Task.FromResult(successResponse.Object);
65+
}
66+
67+
var handler = new SimpleRetryHandler(sleep.Object)
68+
{
69+
TooManyRequestsConsumesARetry = true,
70+
RetryTimes = 10
71+
};
72+
var response = await handler.HandleRetry(request.Object, initialResponse.Object, retry);
73+
74+
Assert.AreEqual(1, retryCalled);
75+
Assert.AreEqual(successResponse.Object, response);
76+
sleep.Verify(s => s(50000), Times.Once);
77+
}
78+
79+
[Test]
80+
public async Task HandleRetry_TooManyRetriesWithSuccessNoConsume()
81+
{
82+
var sleep = new Mock<Func<int, Task>>();
83+
84+
var request = new Mock<IRequest>();
85+
var initialResponse = new Mock<IResponse>();
86+
initialResponse.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.TooManyRequests);
87+
initialResponse.SetupGet(r => r.Headers).Returns(new Dictionary<string, string> {
88+
{ "Retry-After", "50" }
89+
});
90+
var successResponse = new Mock<IResponse>();
91+
successResponse.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.OK);
92+
93+
var retryCalled = 0;
94+
Task<IResponse> retry(IRequest request)
95+
{
96+
retryCalled++;
97+
return Task.FromResult(successResponse.Object);
98+
}
99+
100+
var handler = new SimpleRetryHandler(sleep.Object)
101+
{
102+
TooManyRequestsConsumesARetry = false,
103+
RetryTimes = 0
104+
};
105+
var response = await handler.HandleRetry(request.Object, initialResponse.Object, retry);
106+
107+
Assert.AreEqual(1, retryCalled);
108+
Assert.AreEqual(successResponse.Object, response);
109+
sleep.Verify(s => s(50000), Times.Once);
110+
}
111+
112+
[Test]
113+
public async Task HandleRetry_ServerErrors()
114+
{
115+
var sleep = new Mock<Func<int, Task>>();
116+
117+
var request = new Mock<IRequest>();
118+
var initialResponse = new Mock<IResponse>();
119+
initialResponse.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.BadGateway);
120+
121+
var retryCalled = 0;
122+
Task<IResponse> retry(IRequest request)
123+
{
124+
retryCalled++;
125+
return Task.FromResult(initialResponse.Object);
126+
}
127+
128+
var handler = new SimpleRetryHandler(sleep.Object)
129+
{
130+
TooManyRequestsConsumesARetry = true,
131+
RetryTimes = 10,
132+
RetryAfter = 50
133+
};
134+
var response = await handler.HandleRetry(request.Object, initialResponse.Object, retry);
135+
136+
Assert.AreEqual(10, retryCalled);
137+
Assert.AreEqual(initialResponse.Object, response);
138+
sleep.Verify(s => s(50), Times.Exactly(10));
139+
}
140+
141+
[Test]
142+
public async Task HandleRetry_DirectSuccess()
143+
{
144+
var sleep = new Mock<Func<int, Task>>();
145+
146+
var request = new Mock<IRequest>();
147+
var initialResponse = new Mock<IResponse>();
148+
initialResponse.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.OK);
149+
150+
var retryCalled = 0;
151+
Task<IResponse> retry(IRequest request)
152+
{
153+
retryCalled++;
154+
return Task.FromResult(initialResponse.Object);
155+
}
156+
157+
var handler = new SimpleRetryHandler(sleep.Object)
158+
{
159+
TooManyRequestsConsumesARetry = true,
160+
RetryTimes = 10,
161+
RetryAfter = 50
162+
};
163+
var response = await handler.HandleRetry(request.Object, initialResponse.Object, retry);
164+
165+
Assert.AreEqual(0, retryCalled);
166+
Assert.AreEqual(initialResponse.Object, response);
167+
sleep.Verify(s => s(50), Times.Exactly(0));
168+
}
169+
}
170+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System.Linq;
2+
using System.Net;
3+
using System;
4+
using System.Threading.Tasks;
5+
using System.Collections.Generic;
6+
7+
namespace SpotifyAPI.Web.Http
8+
{
9+
public class SimpleRetryHandler : IRetryHandler
10+
{
11+
private readonly Func<int, Task> _sleep;
12+
13+
/// <summary>
14+
/// Specifies after how many miliseconds should a failed request be retried.
15+
/// </summary>
16+
public int RetryAfter { get; set; }
17+
18+
/// <summary>
19+
/// Maximum number of tries for one failed request.
20+
/// </summary>
21+
public int RetryTimes { get; set; }
22+
23+
/// <summary>
24+
/// Whether a failure of type "Too Many Requests" should use up one of the allocated retry attempts.
25+
/// </summary>
26+
public bool TooManyRequestsConsumesARetry { get; set; }
27+
28+
/// <summary>
29+
/// Error codes that will trigger auto-retry
30+
/// </summary>
31+
public IEnumerable<HttpStatusCode> RetryErrorCodes { get; set; }
32+
33+
/// <summary>
34+
/// A simple retry handler which retries a request based on status codes with a fixed sleep interval.
35+
/// It also supports Retry-After headers sent by spotify. The execution will be delayed by the amount in
36+
/// the Retry-After header
37+
/// </summary>
38+
/// <returns></returns>
39+
public SimpleRetryHandler() : this(Task.Delay) { }
40+
public SimpleRetryHandler(Func<int, Task> sleep)
41+
{
42+
_sleep = sleep;
43+
RetryAfter = 50;
44+
RetryTimes = 10;
45+
TooManyRequestsConsumesARetry = false;
46+
RetryErrorCodes = new[] {
47+
HttpStatusCode.InternalServerError,
48+
HttpStatusCode.BadGateway,
49+
HttpStatusCode.ServiceUnavailable
50+
};
51+
}
52+
53+
private static int? ParseTooManyRetriesToMs(IResponse response)
54+
{
55+
if (response.StatusCode != HttpStatusCode.TooManyRequests)
56+
{
57+
return null;
58+
}
59+
if (int.TryParse(response.Headers["Retry-After"], out int secondsToWait))
60+
{
61+
return secondsToWait * 1000;
62+
}
63+
64+
throw new APIException("429 received, but unable to parse Retry-After Header. This should not happen!");
65+
}
66+
67+
public Task<IResponse> HandleRetry(IRequest request, IResponse response, Func<IRequest, Task<IResponse>> retry)
68+
{
69+
Ensure.ArgumentNotNull(response, nameof(response));
70+
71+
return HandleRetryInternally(request, response, retry, RetryTimes);
72+
}
73+
74+
private async Task<IResponse> HandleRetryInternally(IRequest request, IResponse response, Func<IRequest, Task<IResponse>> retry, int triesLeft)
75+
{
76+
var secondsToWait = ParseTooManyRetriesToMs(response);
77+
if (secondsToWait != null && (!TooManyRequestsConsumesARetry || triesLeft > 0))
78+
{
79+
await _sleep(secondsToWait.Value).ConfigureAwait(false);
80+
response = await retry(request).ConfigureAwait(false);
81+
var newTriesLeft = TooManyRequestsConsumesARetry ? triesLeft - 1 : triesLeft;
82+
return await HandleRetryInternally(request, response, retry, newTriesLeft).ConfigureAwait(false);
83+
}
84+
85+
while (RetryErrorCodes.Contains(response.StatusCode) && triesLeft > 0)
86+
{
87+
await _sleep(RetryAfter).ConfigureAwait(false);
88+
response = await retry(request).ConfigureAwait(false);
89+
return await HandleRetryInternally(request, response, retry, triesLeft - 1).ConfigureAwait(false);
90+
}
91+
92+
return response;
93+
}
94+
}
95+
}

‎SpotifyAPI.Web/Util/Ensure.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static void ArgumentNotNullOrEmptyString(string value, string name)
3939
throw new ArgumentException("String is empty or null", name);
4040
}
4141

42-
public static void ArgumentNotNullOrEmptyList<T>(IList<T> value, string name)
42+
public static void ArgumentNotNullOrEmptyList<T>(IEnumerable<T> value, string name)
4343
{
4444
if (value != null && value.Any())
4545
{

0 commit comments

Comments
 (0)