Skip to content
This repository was archived by the owner on Apr 24, 2024. It is now read-only.

Commit 0b377e0

Browse files
authored
Merge pull request #158 from North-Two-Five/issue#154-Catch-PostAsync-Network-exceptions-and-Retry
Issue#154 catch post async network exceptions and retry
2 parents a9670f1 + daeb441 commit 0b377e0

File tree

9 files changed

+469
-21
lines changed

9 files changed

+469
-21
lines changed

Analytics/Request/BlockingRequestHandler.cs

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected override WebRequest GetWebRequest(Uri address)
3434
{
3535
WebRequest w = base.GetWebRequest(address);
3636
if (Timeout.Milliseconds != 0)
37-
w.Timeout = Timeout.Milliseconds;
37+
w.Timeout = Convert.ToInt32(Timeout.Milliseconds);
3838
return w;
3939
}
4040
}
@@ -220,24 +220,22 @@ public async Task MakeRequest(Batch batch)
220220
watch.Stop();
221221

222222
var response = (HttpWebResponse)ex.Response;
223-
if (response != null)
223+
statusCode = (response != null) ? (int)response.StatusCode : 0;
224+
if ((statusCode >= 500 && statusCode <= 600) || statusCode == 429 || statusCode == 0)
224225
{
225-
statusCode = (int)response.StatusCode;
226-
if ((statusCode >= 500 && statusCode <= 600) || statusCode == 429)
227-
{
228-
// If status code is greater than 500 and less than 600, it indicates server error
229-
// Error code 429 indicates rate limited.
230-
// Retry uploading in these cases.
231-
Thread.Sleep(_backo.AttemptTime());
232-
continue;
233-
}
234-
else if (statusCode >= 400)
235-
{
236-
responseStr = String.Format("Status Code {0}. ", statusCode);
237-
responseStr += ex.Message;
238-
break;
239-
}
226+
// If status code is greater than 500 and less than 600, it indicates server error
227+
// Error code 429 indicates rate limited.
228+
// Retry uploading in these cases.
229+
Thread.Sleep(_backo.AttemptTime());
230+
continue;
240231
}
232+
else if (statusCode >= 400)
233+
{
234+
responseStr = String.Format("Status Code {0}. ", statusCode);
235+
responseStr += ex.Message;
236+
break;
237+
}
238+
241239
}
242240

243241
#else
@@ -248,19 +246,32 @@ public async Task MakeRequest(Batch batch)
248246
if (_client.Config.Gzip)
249247
content.Headers.ContentEncoding.Add("gzip");
250248

251-
var response = await _httpClient.PostAsync(uri, content).ConfigureAwait(false);
249+
HttpResponseMessage response = null;
250+
bool retry = false;
251+
try
252+
{
253+
response = await _httpClient.PostAsync(uri, content).ConfigureAwait(false);
254+
}
255+
catch (TaskCanceledException)
256+
{
257+
retry = true;
258+
}
259+
catch (HttpRequestException)
260+
{
261+
retry = true;
262+
}
252263

253264
watch.Stop();
254-
statusCode = (int)response.StatusCode;
265+
statusCode = response != null ? (int)response.StatusCode : 0;
255266

256-
if (statusCode == (int)HttpStatusCode.OK)
267+
if (response != null && response.StatusCode == HttpStatusCode.OK)
257268
{
258269
Succeed(batch, watch.ElapsedMilliseconds);
259270
break;
260271
}
261272
else
262273
{
263-
if ((statusCode >= 500 && statusCode <= 600) || statusCode == 429)
274+
if ((statusCode >= 500 && statusCode <= 600) || statusCode == 429 || retry)
264275
{
265276
// If status code is greater than 500 and less than 600, it indicates server error
266277
// Error code 429 indicates rate limited.
@@ -281,6 +292,10 @@ public async Task MakeRequest(Batch batch)
281292
if (_backo.HasReachedMax || statusCode != (int)HttpStatusCode.OK)
282293
{
283294
Fail(batch, new APIException("Unexpected Status Code", responseStr), watch.ElapsedMilliseconds);
295+
if (_backo.HasReachedMax)
296+
{
297+
_backo.Reset();
298+
}
284299
}
285300
}
286301
catch (System.Exception e)

Test.Net35/ConnectionTests.cs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Net;
35
using System.Text;
46
using Moq;
57
using NUnit.Framework;
@@ -13,6 +15,14 @@ public class ConnectionTests
1315
{
1416
private Mock<IRequestHandler> _mockRequestHandler;
1517

18+
class RetryErrorTestCase
19+
{
20+
public HttpStatusCode ResponseCode;
21+
public string ErrorMessage;
22+
public int Timeout;
23+
public bool ShouldRetry;
24+
}
25+
1626
[SetUp]
1727
public void Init()
1828
{
@@ -34,6 +44,113 @@ public void CleanUp()
3444
Logger.Handlers -= LoggingHandler;
3545
}
3646

47+
[Test()]
48+
public void RetryErrorTestNet35()
49+
{
50+
Stopwatch watch = new Stopwatch();
51+
52+
// Set invalid host address and make timeout to 1s
53+
var config = new Config().SetAsync(false);
54+
config.SetHost("https://fake.segment-server.com");
55+
config.SetTimeout(new TimeSpan(0, 0, 1));
56+
Analytics.Initialize(Constants.WRITE_KEY, config);
57+
58+
// Calculate working time for Identiy message with invalid host address
59+
watch.Start();
60+
Actions.Identify(Analytics.Client);
61+
watch.Stop();
62+
63+
Assert.AreEqual(1, Analytics.Client.Statistics.Submitted);
64+
Assert.AreEqual(0, Analytics.Client.Statistics.Succeeded);
65+
Assert.AreEqual(1, Analytics.Client.Statistics.Failed);
66+
67+
// Handling Identify message will take more than 10s even though the timeout is 1s.
68+
// That's because it retries submit when it's failed.
69+
Assert.AreEqual(true, watch.ElapsedMilliseconds > 10000);
70+
}
71+
72+
[Test()]
73+
public void RetryServerErrorTestNet35()
74+
{
75+
Stopwatch watch = new Stopwatch();
76+
77+
string DummyServerUrl = "http://localhost:9696";
78+
using (var DummyServer = new WebServer(DummyServerUrl))
79+
{
80+
DummyServer.RunAsync();
81+
82+
// Set invalid host address and make timeout to 1s
83+
var config = new Config().SetAsync(false);
84+
config.SetHost(DummyServerUrl);
85+
config.SetTimeout(new TimeSpan(0, 0, 1));
86+
Analytics.Initialize(Constants.WRITE_KEY, config);
87+
88+
var TestCases = new RetryErrorTestCase[]
89+
{
90+
// The errors (500 > code >= 400) doesn't require retry
91+
new RetryErrorTestCase()
92+
{
93+
ErrorMessage = "Server Gone",
94+
ResponseCode = HttpStatusCode.Gone,
95+
ShouldRetry = false,
96+
Timeout = 10000
97+
},
98+
// 429 error requires retry
99+
new RetryErrorTestCase()
100+
{
101+
ErrorMessage = "Too many requests",
102+
ResponseCode = (HttpStatusCode)429,
103+
ShouldRetry = true,
104+
Timeout = 10000
105+
},
106+
// Server errors require retry
107+
new RetryErrorTestCase()
108+
{
109+
ErrorMessage = "Bad Gateway",
110+
ResponseCode = HttpStatusCode.BadGateway,
111+
ShouldRetry = true,
112+
Timeout = 10000
113+
}
114+
};
115+
116+
foreach (var testCase in TestCases)
117+
{
118+
// Setup fallback module which returns error code
119+
DummyServer.RequestHandler = ((req, res) =>
120+
{
121+
string pageData = "{ ErrorMessage: '" + testCase.ErrorMessage + "' }";
122+
byte[] data = Encoding.UTF8.GetBytes(pageData);
123+
124+
res.StatusCode = (int)testCase.ResponseCode;
125+
res.ContentType = "application/json";
126+
res.ContentEncoding = Encoding.UTF8;
127+
res.ContentLength64 = data.LongLength;
128+
129+
res.OutputStream.Write(data, 0, data.Length);
130+
res.Close();
131+
});
132+
133+
// Calculate working time for Identiy message with invalid host address
134+
watch.Start();
135+
Actions.Identify(Analytics.Client);
136+
watch.Stop();
137+
138+
DummyServer.RequestHandler = null;
139+
140+
Assert.AreEqual(0, Analytics.Client.Statistics.Succeeded);
141+
142+
// Handling Identify message will less than 10s because the server returns GONE message.
143+
// That's because it retries submit when it's failed.
144+
if (testCase.ShouldRetry)
145+
Assert.IsTrue(watch.ElapsedMilliseconds > testCase.Timeout);
146+
else
147+
Assert.IsFalse(watch.ElapsedMilliseconds > testCase.Timeout);
148+
}
149+
}
150+
}
151+
152+
153+
37154
[Test()]
38155
public void ProxyTestNet35()
39156
{

Test.Net35/Test.Net35.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
<Compile Include="RequestHandlerTest.cs" />
6868
<Compile Include="Request\BlockingRequestHandlerTests.cs" />
6969
<Compile Include="Stats\StatisticsTests.cs" />
70+
<Compile Include="WebServer.cs" />
7071
</ItemGroup>
7172
<ItemGroup>
7273
<None Include="packages.config" />

Test.Net35/WebServer.cs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Text;
6+
using System.Threading;
7+
8+
namespace Segment.Test
9+
{
10+
class WebServer :IDisposable
11+
{
12+
private HttpListener listener;
13+
private Thread runningThread;
14+
private bool runServer;
15+
private string address;
16+
17+
public delegate void HandleRequest(HttpListenerRequest req, HttpListenerResponse res);
18+
public HandleRequest RequestHandler = null;
19+
20+
public WebServer(string url)
21+
{
22+
// Only string ends with '/' is acceptable for web server url
23+
address = url;
24+
if (!address.EndsWith("/"))
25+
address += "/";
26+
}
27+
28+
public void HandleIncomingConnections()
29+
{
30+
while (runServer)
31+
{
32+
try
33+
{
34+
// Will wait here until we hear from a connection
35+
HttpListenerContext ctx = listener.GetContext();
36+
37+
// Peel out the requests and response objects
38+
HttpListenerRequest req = ctx.Request;
39+
HttpListenerResponse res = ctx.Response;
40+
41+
if (RequestHandler != null)
42+
{
43+
RequestHandler(req, res);
44+
}
45+
else
46+
{
47+
// Write the response info
48+
string pageData = "{}";
49+
byte[] data = Encoding.UTF8.GetBytes(pageData);
50+
res.ContentType = "application/json";
51+
res.ContentEncoding = Encoding.UTF8;
52+
res.ContentLength64 = data.LongLength;
53+
54+
// Write out to the response stream (asynchronously), then close it
55+
res.OutputStream.Write(data, 0, data.Length);
56+
res.Close();
57+
}
58+
}
59+
catch (System.Exception)
60+
{
61+
runServer = false;
62+
return;
63+
}
64+
}
65+
}
66+
67+
public void RunAsync()
68+
{
69+
// Stop already running server
70+
Stop();
71+
72+
// Create new listener
73+
listener = new HttpListener();
74+
listener.Prefixes.Add(address);
75+
listener.Start();
76+
77+
// Start listening requests
78+
runServer = true;
79+
runningThread = new Thread(HandleIncomingConnections);
80+
runningThread.Start();
81+
}
82+
83+
public void Stop()
84+
{
85+
runServer = false;
86+
87+
if (listener != null)
88+
{
89+
listener.Close();
90+
listener = null;
91+
}
92+
if (runningThread != null)
93+
{
94+
runningThread.Join();
95+
runningThread = null;
96+
}
97+
}
98+
99+
#region IDisposable Support
100+
private bool disposedValue = false; // To detect redundant calls
101+
102+
protected virtual void Dispose(bool disposing)
103+
{
104+
if (!disposedValue)
105+
{
106+
if (disposing)
107+
{
108+
Stop();
109+
}
110+
111+
disposedValue = true;
112+
}
113+
}
114+
115+
public void Dispose()
116+
{
117+
Dispose(true);
118+
}
119+
#endregion
120+
}
121+
}

0 commit comments

Comments
 (0)