diff --git a/Src/Support/Google.Apis.Core/Http/ConfigurableMessageHandler.cs b/Src/Support/Google.Apis.Core/Http/ConfigurableMessageHandler.cs
index 637785f4b85..cc12532d571 100644
--- a/Src/Support/Google.Apis.Core/Http/ConfigurableMessageHandler.cs
+++ b/Src/Support/Google.Apis.Core/Http/ConfigurableMessageHandler.cs
@@ -72,6 +72,11 @@ public class ConfigurableMessageHandler : DelegatingHandler
///
public const string CredentialKey = "__CredentialKey";
+ ///
+ /// Key for request specific max retries.
+ ///
+ public const string MaxRetriesKey = "__MaxRetriesKey";
+
/// The current API version of this client library.
private static readonly string ApiVersion = Google.Apis.Util.Utilities.GetLibraryVersion();
@@ -396,7 +401,8 @@ protected override async Task SendAsync(HttpRequestMessage
loggingRequestId = Interlocked.Increment(ref _loggingRequestId).ToString("X8");
}
- int triesRemaining = NumTries;
+ int maxRetries = GetEffectiveMaxRetries(request);
+ int triesRemaining = maxRetries;
int redirectRemaining = NumRedirects;
Exception lastException = null;
@@ -501,8 +507,8 @@ protected override async Task SendAsync(HttpRequestMessage
{
Request = request,
Exception = lastException,
- TotalTries = NumTries,
- CurrentFailedTry = NumTries - triesRemaining,
+ TotalTries = maxRetries,
+ CurrentFailedTry = maxRetries - triesRemaining,
CancellationToken = cancellationToken
}).ConfigureAwait(false);
}
@@ -561,8 +567,8 @@ protected override async Task SendAsync(HttpRequestMessage
{
Request = request,
Response = response,
- TotalTries = NumTries,
- CurrentFailedTry = NumTries - triesRemaining,
+ TotalTries = maxRetries,
+ CurrentFailedTry = maxRetries - triesRemaining,
CancellationToken = cancellationToken
};
@@ -667,6 +673,10 @@ private IHttpExecuteInterceptor GetEffectiveCredential(HttpRequestMessage reques
(request.Properties.TryGetValue(CredentialKey, out var cred) && cred is IHttpExecuteInterceptor callCredential)
? callCredential : Credential;
+ private int GetEffectiveMaxRetries(HttpRequestMessage request) =>
+ (request.Properties.TryGetValue(MaxRetriesKey, out var maxRetries) && maxRetries is int perRequestMaxRetries)
+ ? perRequestMaxRetries : NumTries;
+
///
/// Handles redirect if the response's status code is redirect, redirects are turned on, and the header has
/// a location.
diff --git a/Src/Support/Google.Apis.Tests/Apis/Http/ConfigurableMessageHandlerTest.cs b/Src/Support/Google.Apis.Tests/Apis/Http/ConfigurableMessageHandlerTest.cs
index 8acc28c11d3..3dffc27f0a7 100644
--- a/Src/Support/Google.Apis.Tests/Apis/Http/ConfigurableMessageHandlerTest.cs
+++ b/Src/Support/Google.Apis.Tests/Apis/Http/ConfigurableMessageHandlerTest.cs
@@ -259,9 +259,9 @@ public void SendAsync_ExecuteInterceptor_AbnormalResponse_UnsuccessfulResponseHa
{
var handler = new InterceptorMessageHandler();
handler.InjectedResponseMessage = new HttpResponseMessage()
- {
- StatusCode = HttpStatusCode.ServiceUnavailable
- };
+ {
+ StatusCode = HttpStatusCode.ServiceUnavailable
+ };
var configurableHanlder = new ConfigurableMessageHandler(handler);
var interceptor = new InterceptorMessageHandler.Interceptor();
@@ -278,6 +278,33 @@ public void SendAsync_ExecuteInterceptor_AbnormalResponse_UnsuccessfulResponseHa
}
}
+ [Fact]
+ public void SendAsync_ExecuteInterceptor_AbnormalResponse_UnsuccessfulResponseHandler_PerRequestMaxRetries()
+ {
+ var handler = new InterceptorMessageHandler();
+ handler.InjectedResponseMessage = new HttpResponseMessage()
+ {
+ StatusCode = HttpStatusCode.ServiceUnavailable
+ };
+
+ var configurableHandler = new ConfigurableMessageHandler(handler);
+ var interceptor = new InterceptorMessageHandler.Interceptor();
+ configurableHandler.AddExecuteInterceptor(interceptor);
+ configurableHandler.AddUnsuccessfulResponseHandler(new TrueUnsuccessfulResponseHandler());
+ // Let's have this request retry for a little longer than default.
+ int perRequestRetries = configurableHandler.NumTries + 2;
+
+ using (var client = new HttpClient(configurableHandler))
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://test-execute-interceptor");
+ request.Properties.Add(ConfigurableMessageHandler.MaxRetriesKey, perRequestRetries);
+
+ HttpResponseMessage response = client.SendAsync(request).Result;
+ Assert.Equal(perRequestRetries, interceptor.Calls);
+ Assert.Equal(perRequestRetries, handler.Calls);
+ }
+ }
+
#endregion
#region Unsuccessful reponse handler
@@ -326,8 +353,11 @@ public Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args)
}
}
- /// Test helper for testing unsuccessful response handlers.
- private void SubtestSendAsyncUnsuccessfulReponseHanlder(HttpStatusCode code)
+ [Theory]
+ [CombinatorialData]
+ public void SendAsyncUnsuccessfulReponseHanlder(
+ [CombinatorialValues(HttpStatusCode.OK, HttpStatusCode.BadGateway, HttpStatusCode.ServiceUnavailable)] HttpStatusCode code,
+ [CombinatorialValues(null, 5)] int? maxRetries)
{
var handler = new UnsuccessfulResponseMessageHandler { ResponseStatusCode = code };
@@ -338,6 +368,7 @@ private void SubtestSendAsyncUnsuccessfulReponseHanlder(HttpStatusCode code)
using (var client = new HttpClient(configurableHanlder))
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://test-unsuccessful-handler");
+ int expectedMaxRetries = MaybeSetMaxRetries(maxRetries, configurableHanlder.NumTries, request);
HttpResponseMessage response = client.SendAsync(request).Result;
Assert.Equal(code, response.StatusCode);
@@ -346,8 +377,8 @@ private void SubtestSendAsyncUnsuccessfulReponseHanlder(HttpStatusCode code)
// handles service unavailable responses
if (code == HttpStatusCode.ServiceUnavailable)
{
- Assert.Equal(configurableHanlder.NumTries, unsuccessfulHandler.Calls);
- Assert.Equal(configurableHanlder.NumTries, handler.Calls);
+ Assert.Equal(expectedMaxRetries, unsuccessfulHandler.Calls);
+ Assert.Equal(expectedMaxRetries, handler.Calls);
}
else
{
@@ -358,33 +389,6 @@ private void SubtestSendAsyncUnsuccessfulReponseHanlder(HttpStatusCode code)
}
}
- /// Tests that unsuccessful response handler isn't called when the response is successful.
- [Fact]
- public void SendAsync_UnsuccessfulReponseHanlder_SuccessfulReponse()
- {
- SubtestSendAsyncUnsuccessfulReponseHanlder(HttpStatusCode.OK);
- }
-
- ///
- /// Tests that unsuccessful response handler is called when the response is unsuccessful, but the handler can't
- /// handle the abnormal response (e.g. different status code).
- ///
- [Fact]
- public void SendAsync_UnsuccessfulReponseHanlder_AbnormalResponse_DifferentStatusCode()
- {
- SubtestSendAsyncUnsuccessfulReponseHanlder(HttpStatusCode.BadGateway);
- }
-
- ///
- /// Tests that unsuccessful response handler is called when the response is unsuccessful and the handler can
- /// handle the abnormal response (e.g. same status code).
- ///
- [Fact]
- public void SendAsync_UnsuccessfulReponseHanlder_AbnormalResponse_SameStatusCode()
- {
- SubtestSendAsyncUnsuccessfulReponseHanlder(HttpStatusCode.ServiceUnavailable);
- }
-
/// Tests abnormal response when unsuccessful response handler isn't plugged.
[Fact]
public void SendAsync_AbnormalResponse_WithoutUnsuccessfulReponseHandler()
@@ -464,8 +468,10 @@ public Task HandleExceptionAsync(HandleExceptionArgs args)
}
}
- /// Subtest for exception handler which tests that exception handler is invoked.
- private void SubtestSendAsyncExceptionHandler(bool throwException, bool handle)
+ [Theory]
+ [CombinatorialData]
+ public void SendAsyncExceptionHandler(bool throwException, bool handle,
+ [CombinatorialValues(null, 5)] int? maxRetries)
{
var handler = new ExceptionMessageHandler { ThrowException = throwException };
@@ -476,6 +482,8 @@ private void SubtestSendAsyncExceptionHandler(bool throwException, bool handle)
using (var client = new HttpClient(configurableHanlder))
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://test-exception-handler");
+ int expectedMaxRetries = MaybeSetMaxRetries(maxRetries, configurableHanlder.NumTries, request);
+
try
{
HttpResponseMessage response = client.SendAsync(request).Result;
@@ -493,7 +501,7 @@ private void SubtestSendAsyncExceptionHandler(bool throwException, bool handle)
// only 1
if (throwException)
{
- Assert.Equal(handle ? configurableHanlder.NumTries : 1, exceptionHandler.Calls);
+ Assert.Equal(handle ? expectedMaxRetries : 1, exceptionHandler.Calls);
}
// exception wasn't supposed to be thrown, so no call to exception handler should be made
else
@@ -501,38 +509,10 @@ private void SubtestSendAsyncExceptionHandler(bool throwException, bool handle)
Assert.Equal(0, exceptionHandler.Calls);
}
- Assert.Equal(throwException & handle ? configurableHanlder.NumTries : 1, handler.Calls);
+ Assert.Equal(throwException & handle ? expectedMaxRetries : 1, handler.Calls);
}
}
-
- /// Tests that the exception handler isn't called on successful response.
- [Fact]
- public void SendAsync_ExceptionHandler_SuccessReponse()
- {
- SubtestSendAsyncExceptionHandler(false, true);
- }
-
- ///
- /// Tests that the exception handler is called when exception is thrown on execute, but it can't handle the
- /// exception.
- ///
- [Fact]
- public void SendAsync_ExceptionHandler_ThrowException_DontHandle()
- {
- SubtestSendAsyncExceptionHandler(true, false);
- }
-
- ///
- /// Tests that the exception handler is called when exception is thrown on execute, and it handles the
- /// exception.
- ///
- [Fact]
- public void SendAsync_ExceptionHandler_ThrowException_Handle()
- {
- SubtestSendAsyncExceptionHandler(true, true);
- }
-
/// Tests an exception is thrown on execute and there is no exception handler.
[Fact]
public void SendAsync_ThrowException_WithoutExceptionHandler()
@@ -571,78 +551,88 @@ public void SendAsync_ThrowException_WithoutExceptionHandler()
/// Tests that back-off handler works as expected when exception is thrown.
/// Use default max time span (2 minutes).
///
- [Fact]
- public void SendAsync_BackOffExceptionHandler_Throw_Max2Minutes()
+ [Theory]
+ [InlineData(null)]
+ [InlineData(5)]
+ public void SendAsync_BackOffExceptionHandler_Throw_Max2Minutes(int? maxRetries)
{
// create exponential back-off without delta interval, so expected seconds are exactly 1, 2, 4, 8, etc.
var initializer = new BackOffHandler.Initializer(new ExponentialBackOff(TimeSpan.Zero));
- SubtestSendAsync_BackOffExceptionHandler(true, initializer);
+ SubtestSendAsync_BackOffExceptionHandler(true, initializer, maxRetries: maxRetries);
}
///
/// Tests that back-off handler works as expected when exception is thrown.
/// Max time span is set to 200 milliseconds (as a result the back-off handler can't handle the exception).
///
- [Fact]
- public void SendAsync_BackOffExceptionHandler_Throw_Max200Milliseconds()
+ [Theory]
+ [InlineData(null)]
+ [InlineData(5)]
+ public void SendAsync_BackOffExceptionHandler_Throw_Max200Milliseconds(int? maxRetries)
{
var initializer = new BackOffHandler.Initializer(new ExponentialBackOff(TimeSpan.Zero))
{
MaxTimeSpan = TimeSpan.FromMilliseconds(200)
};
- SubtestSendAsync_BackOffExceptionHandler(true, initializer);
+ SubtestSendAsync_BackOffExceptionHandler(true, initializer, maxRetries: maxRetries);
}
///
/// Tests that back-off handler works as expected when exception is thrown.
/// Max time span is set to 1 hour.
///
- [Fact]
- public void SendAsync_BackOffExceptionHandler_Throw_Max1Hour()
+ [Theory]
+ [InlineData(null)]
+ [InlineData(5)]
+ public void SendAsync_BackOffExceptionHandler_Throw_Max1Hour(int? maxRetries)
{
var initializer = new BackOffHandler.Initializer(new ExponentialBackOff(TimeSpan.Zero))
{
MaxTimeSpan = TimeSpan.FromHours(1)
};
- SubtestSendAsync_BackOffExceptionHandler(true, initializer);
+ SubtestSendAsync_BackOffExceptionHandler(true, initializer,maxRetries: maxRetries);
}
///
/// Tests that back-off handler works as expected when
/// > is thrown.
///
- [Fact]
- public void SendAsync_BackOffExceptionHandler_ThrowCanceledException()
+ [Theory]
+ [InlineData(null)]
+ [InlineData(5)]
+ public void SendAsync_BackOffExceptionHandler_ThrowCanceledException(int? maxRetries)
{
var initializer = new BackOffHandler.Initializer(new ExponentialBackOff(TimeSpan.Zero));
- SubtestSendAsync_BackOffExceptionHandler(true, initializer, new TaskCanceledException());
+ SubtestSendAsync_BackOffExceptionHandler(true, initializer, new TaskCanceledException(), maxRetries);
}
///
/// Tests that back-off handler works as expected with the not defaulted exception handler.
///
- [Fact]
- public void SendAsync_BackOffExceptionHandler_DifferentHandler()
+ [Theory]
+ [InlineData(null)]
+ [InlineData(5)]
+ public void SendAsync_BackOffExceptionHandler_DifferentHandler(int? maxRetries)
{
var initializer = new BackOffHandler.Initializer(new ExponentialBackOff(TimeSpan.Zero));
initializer.HandleExceptionFunc = e => (e is InvalidCastException);
- SubtestSendAsync_BackOffExceptionHandler(true, initializer, new InvalidCastException());
+ SubtestSendAsync_BackOffExceptionHandler(true, initializer, new InvalidCastException(), maxRetries);
initializer.HandleExceptionFunc = e => !(e is InvalidCastException);
- SubtestSendAsync_BackOffExceptionHandler(true, initializer, new InvalidCastException());
+ SubtestSendAsync_BackOffExceptionHandler(true, initializer, new InvalidCastException(), maxRetries);
}
/// Tests that back-off handler works as expected when exception isn't thrown.
- [Fact]
- public void SendAsync_BackOffExceptionHandler_DontThrow()
+ [Theory]
+ [InlineData(null)]
+ [InlineData(5)]
+ public void SendAsync_BackOffExceptionHandler_DontThrow(int? maxRetries)
{
var initializer = new BackOffHandler.Initializer(new ExponentialBackOff(TimeSpan.Zero));
- SubtestSendAsync_BackOffExceptionHandler(false, initializer);
+ SubtestSendAsync_BackOffExceptionHandler(false, initializer, maxRetries: maxRetries);
}
- /// Subtest that back-off handler works as expected when exception is or isn't thrown.
- private void SubtestSendAsync_BackOffExceptionHandler(bool throwException,
- BackOffHandler.Initializer initializer, Exception exceptionToThrow = null)
+ private void SubtestSendAsync_BackOffExceptionHandler(bool throwException, BackOffHandler.Initializer initializer, Exception exceptionToThrow = null, int? maxRetries = null)
{
var handler = new ExceptionMessageHandler { ThrowException = throwException };
if (exceptionToThrow != null)
@@ -654,19 +644,21 @@ private void SubtestSendAsync_BackOffExceptionHandler(bool throwException,
var boHandler = new MockBackOffHandler(initializer);
configurableHanlder.AddExceptionHandler(boHandler);
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://test-exception-handler");
+ int expectedMaxRetries = MaybeSetMaxRetries(maxRetries, configurableHanlder.NumTries, request);
+
int boHandleCount = 0;
// if an exception should be thrown and the handler can handle it then calculate the handle count by the
// lg(MaxTimeSpan)
if (throwException && initializer.HandleExceptionFunc(exceptionToThrow))
{
boHandleCount = Math.Min((int)Math.Floor(Math.Log(boHandler.MaxTimeSpan.TotalSeconds, 2)) + 1,
- configurableHanlder.NumTries - 1);
+ expectedMaxRetries - 1);
boHandleCount = boHandleCount >= 0 ? boHandleCount : 0;
}
using (var client = new HttpClient(configurableHanlder))
{
- var request = new HttpRequestMessage(HttpMethod.Get, "https://test-exception-handler");
try
{
HttpResponseMessage response = client.SendAsync(request).Result;
@@ -1216,5 +1208,16 @@ protected override Task SendAsync(HttpRequestMessage reques
return Task.FromResult(new HttpResponseMessage());
}
}
+
+ private int MaybeSetMaxRetries(int? perRequestMaxRetries, int defaultMaxRetries, HttpRequestMessage requestMessage)
+ {
+ if (perRequestMaxRetries == null)
+ {
+ return defaultMaxRetries;
+ }
+ int configuredRetries = perRequestMaxRetries.Value;
+ requestMessage.Properties.Add(ConfigurableMessageHandler.MaxRetriesKey, configuredRetries);
+ return configuredRetries;
+ }
}
}