This repository has been archived by the owner on Jul 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Retry once on certain WebException codes (#785)
Address NuGet/Engineering#3212 Only added retry for WebException codes that were clearly transient in logs
- Loading branch information
1 parent
725e891
commit 4ee90dc
Showing
9 changed files
with
317 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
src/NuGet.Services.AzureSearch/WebExceptionRetryDelegatingHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace NuGet.Services.AzureSearch | ||
{ | ||
public class WebExceptionRetryDelegatingHandler : DelegatingHandler | ||
{ | ||
private static readonly HashSet<WebExceptionStatus> _transientWebExceptionStatuses = new HashSet<WebExceptionStatus>(new[] | ||
{ | ||
WebExceptionStatus.ConnectFailure, // Unable to connect to the remote server | ||
WebExceptionStatus.ConnectionClosed, // The underlying connection was closed | ||
WebExceptionStatus.KeepAliveFailure, // A connection that was expected to be kept alive was closed by the server | ||
WebExceptionStatus.ReceiveFailure, // An unexpected error occurred on a receive | ||
}); | ||
|
||
private readonly ILogger<WebExceptionRetryDelegatingHandler> _logger; | ||
|
||
public WebExceptionRetryDelegatingHandler(ILogger<WebExceptionRetryDelegatingHandler> logger) | ||
{ | ||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
} | ||
|
||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||
{ | ||
try | ||
{ | ||
return await base.SendAsync(request, cancellationToken); | ||
} | ||
catch (Exception ex) when (ex is HttpRequestException hre && hre.InnerException is WebException we) | ||
{ | ||
if (_transientWebExceptionStatuses.Contains(we.Status)) | ||
{ | ||
// Retry only a single time since some of these transient exceptions take a while (~20 seconds) to be | ||
// thrown and we don't want to make the user wait too long even to see a failure. | ||
_logger.LogWarning(ex, "Transient web exception encountered, status {Status}. Attempting a single retry.", we.Status); | ||
return await base.SendAsync(request, cancellationToken); | ||
} | ||
else | ||
{ | ||
_logger.LogError(ex, "Non-transient web exception encountered, status {Status}.", we.Status); | ||
throw; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
161 changes: 161 additions & 0 deletions
161
tests/NuGet.Services.AzureSearch.Tests/DependencyInjectionExtensionsFacts.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Azure.Search; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Rest.TransientFaultHandling; | ||
using Moq; | ||
using Moq.Language.Flow; | ||
using NuGet.Services.AzureSearch.Wrappers; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace NuGet.Services.AzureSearch | ||
{ | ||
public class DependencyInjectionExtensionsFacts | ||
{ | ||
public class TheGetRetryPolicyMethod | ||
{ | ||
public TheGetRetryPolicyMethod(ITestOutputHelper output) | ||
{ | ||
LoggerFactory = new LoggerFactory().AddXunit(output); | ||
HttpClientHandler = new Mock<TestHttpClientHandler> { CallBase = true }; | ||
|
||
SearchServiceClient = new SearchServiceClient( | ||
"test-search-service", | ||
new SearchCredentials("api-key"), | ||
HttpClientHandler.Object, | ||
DependencyInjectionExtensions.GetSearchDelegatingHandlers(LoggerFactory)); | ||
SearchServiceClient.SetRetryPolicy(SingleRetry); | ||
|
||
IndexesOperationsWrapper = new IndexesOperationsWrapper( | ||
SearchServiceClient.Indexes, | ||
DependencyInjectionExtensions.GetSearchDelegatingHandlers(LoggerFactory), | ||
SingleRetry, | ||
LoggerFactory.CreateLogger<DocumentsOperationsWrapper>()); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(NonTransientTestData))] | ||
public async Task DoesNotRetryNonTransientErrorsForIndexOperations(Action<ISetup<TestHttpClientHandler, Task<HttpResponseMessage>>> setup) | ||
{ | ||
setup(HttpClientHandler.Setup(x => x.OnSendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))); | ||
|
||
await Assert.ThrowsAnyAsync<Exception>(() => SearchServiceClient.Indexes.ListAsync()); | ||
|
||
VerifyAttemptCount(1); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(NonTransientTestData))] | ||
public async Task DoesNotRetryNonTransientErrorsForDocumentOperations(Action<ISetup<TestHttpClientHandler, Task<HttpResponseMessage>>> setup) | ||
{ | ||
setup(HttpClientHandler.Setup(x => x.OnSendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))); | ||
|
||
await Assert.ThrowsAnyAsync<Exception>(() => IndexesOperationsWrapper.GetClient("test-index").Documents.CountAsync()); | ||
|
||
VerifyAttemptCount(1); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(TransientTestData))] | ||
public async Task RetriesTransientErrorsForIndexOperations(Action<ISetup<TestHttpClientHandler, Task<HttpResponseMessage>>> setup) | ||
{ | ||
setup(HttpClientHandler.Setup(x => x.OnSendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))); | ||
|
||
await Assert.ThrowsAnyAsync<Exception>(() => SearchServiceClient.Indexes.ListAsync()); | ||
|
||
VerifyAttemptCount(2); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(TransientTestData))] | ||
public async Task RetriesTransientErrorsForDocumentOperations(Action<ISetup<TestHttpClientHandler, Task<HttpResponseMessage>>> setup) | ||
{ | ||
setup(HttpClientHandler.Setup(x => x.OnSendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))); | ||
|
||
await Assert.ThrowsAnyAsync<Exception>(() => IndexesOperationsWrapper.GetClient("test-index").Documents.CountAsync()); | ||
|
||
VerifyAttemptCount(2); | ||
} | ||
|
||
private void VerifyAttemptCount(int count) | ||
{ | ||
HttpClientHandler.Verify( | ||
x => x.OnSendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()), | ||
Times.Exactly(count)); | ||
} | ||
|
||
public static IEnumerable<HttpStatusCode> TransientHttpStatusCodes => new[] | ||
{ | ||
HttpStatusCode.RequestTimeout, | ||
(HttpStatusCode)429, | ||
HttpStatusCode.InternalServerError, | ||
HttpStatusCode.BadGateway, | ||
HttpStatusCode.InternalServerError, | ||
HttpStatusCode.GatewayTimeout, | ||
}; | ||
|
||
public static IEnumerable<WebExceptionStatus> TransientWebExceptionStatuses => new[] | ||
{ | ||
WebExceptionStatus.ConnectFailure, | ||
WebExceptionStatus.ConnectionClosed, | ||
WebExceptionStatus.KeepAliveFailure, | ||
WebExceptionStatus.ReceiveFailure, | ||
}; | ||
|
||
public static IEnumerable<object[]> TransientTestData => GetTestData(TransientHttpStatusCodes, TransientWebExceptionStatuses); | ||
|
||
public static IEnumerable<HttpStatusCode> NonTransientHttpStatusCodes => new[] | ||
{ | ||
HttpStatusCode.BadRequest, | ||
HttpStatusCode.Unauthorized, | ||
HttpStatusCode.Forbidden, | ||
HttpStatusCode.NotFound, | ||
HttpStatusCode.Conflict, | ||
HttpStatusCode.NotImplemented, | ||
}; | ||
|
||
public static IEnumerable<WebExceptionStatus> NonTransientWebExceptionStatuses => new[] | ||
{ | ||
WebExceptionStatus.TrustFailure, | ||
WebExceptionStatus.NameResolutionFailure, | ||
}; | ||
|
||
public static IEnumerable<object[]> NonTransientTestData => GetTestData(NonTransientHttpStatusCodes, NonTransientWebExceptionStatuses); | ||
|
||
private static IEnumerable<object[]> GetTestData(IEnumerable<HttpStatusCode> statusCodes, IEnumerable<WebExceptionStatus> webExceptionStatuses) | ||
{ | ||
var setups = new List<Action<ISetup<TestHttpClientHandler, Task<HttpResponseMessage>>>>(); | ||
|
||
foreach (var statusCode in statusCodes) | ||
{ | ||
setups.Add(s => s.ReturnsAsync(() => new HttpResponseMessage(statusCode) { Content = new StringContent(string.Empty) })); | ||
} | ||
|
||
foreach (var webExceptionStatus in webExceptionStatuses) | ||
{ | ||
setups.Add(s => s.ThrowsAsync(new HttpRequestException("Fail.", new WebException("Inner fail.", webExceptionStatus)))); | ||
} | ||
|
||
return setups.Select(x => new object[] { x }); | ||
} | ||
|
||
public RetryPolicy SingleRetry => new RetryPolicy( | ||
new HttpStatusCodeErrorDetectionStrategy(), | ||
new FixedIntervalRetryStrategy(retryCount: 1, retryInterval: TimeSpan.Zero)); | ||
|
||
public ILoggerFactory LoggerFactory { get; } | ||
public Mock<TestHttpClientHandler> HttpClientHandler { get; } | ||
public SearchServiceClient SearchServiceClient { get; } | ||
public IndexesOperationsWrapper IndexesOperationsWrapper { get; } | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.