Skip to content

Commit 4a974f3

Browse files
Add retries for SocketExceptions (#256)
We observed SocketExceptions (closed from the remote end) in some not- obviously-error cases when uploading to registries. Wrap a simple retry loop with exponential backoff around the requests, being very specific to just that exception.
1 parent f183101 commit 4a974f3

File tree

1 file changed

+39
-14
lines changed

1 file changed

+39
-14
lines changed

Microsoft.NET.Build.Containers/AuthHandshakeMessageHandler.cs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Valleysoft.DockerCredsProvider;
1111

1212
using Microsoft.NET.Build.Containers.Credentials;
13+
using System.Net.Sockets;
1314

1415
namespace Microsoft.NET.Build.Containers;
1516

@@ -18,6 +19,8 @@ namespace Microsoft.NET.Build.Containers;
1819
/// </summary>
1920
public partial class AuthHandshakeMessageHandler : DelegatingHandler
2021
{
22+
private const int MaxRequestRetries = 5; // Arbitrary but seems to work ok for chunked uploads to ghcr.io
23+
2124
private record AuthInfo(Uri Realm, string Service, string? Scope);
2225

2326
/// <summary>
@@ -161,24 +164,46 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
161164
request.Headers.Authorization = cachedAuthentication;
162165
}
163166

164-
var response = await base.SendAsync(request, cancellationToken);
165-
if (response is { StatusCode: HttpStatusCode.OK })
166-
{
167-
return response;
168-
}
169-
else if (response is { StatusCode: HttpStatusCode.Unauthorized } && TryParseAuthenticationInfo(response, out string? scheme, out AuthInfo? authInfo))
167+
int retryCount = 0;
168+
169+
while (retryCount < MaxRequestRetries)
170170
{
171-
if (await GetAuthenticationAsync(scheme, authInfo.Realm, authInfo.Service, authInfo.Scope, cancellationToken) is AuthenticationHeaderValue authentication)
171+
try
172172
{
173-
request.Headers.Authorization = AuthHeaderCache.AddOrUpdate(request.RequestUri, authentication);
174-
return await base.SendAsync(request, cancellationToken);
173+
var response = await base.SendAsync(request, cancellationToken);
174+
if (response is { StatusCode: HttpStatusCode.OK })
175+
{
176+
return response;
177+
}
178+
else if (response is { StatusCode: HttpStatusCode.Unauthorized } && TryParseAuthenticationInfo(response, out string? scheme, out AuthInfo? authInfo))
179+
{
180+
if (await GetAuthenticationAsync(scheme, authInfo.Realm, authInfo.Service, authInfo.Scope, cancellationToken) is AuthenticationHeaderValue authentication)
181+
{
182+
request.Headers.Authorization = AuthHeaderCache.AddOrUpdate(request.RequestUri, authentication);
183+
return await base.SendAsync(request, cancellationToken);
184+
}
185+
return response;
186+
}
187+
else
188+
{
189+
return response;
190+
}
191+
}
192+
catch (HttpRequestException e) when (e.InnerException is IOException ioe && ioe.InnerException is SocketException se)
193+
{
194+
retryCount += 1;
195+
196+
// TODO: log in a way that is MSBuild-friendly
197+
Console.WriteLine($"Encountered a SocketException with message \"{se.Message}\". Pausing before retry.");
198+
199+
await Task.Delay(TimeSpan.FromSeconds(1.0 * Math.Pow(2, retryCount)), cancellationToken);
200+
201+
// retry
202+
continue;
175203
}
176-
return response;
177-
}
178-
else
179-
{
180-
return response;
181204
}
205+
206+
throw new ApplicationException("Too many retries, stopping");
182207
}
183208

184209
[GeneratedRegex("(?<key>\\w+)=\"(?<value>[^\"]*)\"(?:,|$)")]

0 commit comments

Comments
 (0)