Skip to content

Commit 40def61

Browse files
committed
factory
1 parent 633e298 commit 40def61

File tree

3 files changed

+531
-1
lines changed

3 files changed

+531
-1
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using Amazon.Runtime.Internal.Util;
17+
using System;
18+
using System.IO;
19+
using System.Net.Http;
20+
using System.Net.Sockets;
21+
using System.Threading;
22+
using System.Threading.Tasks;
23+
24+
namespace Amazon.Runtime.Pipeline.HttpHandler
25+
{
26+
/// <summary>
27+
/// A DelegatingHandler that automatically retries requests when they fail due to
28+
/// stale connections from the HTTP connection pool. This prevents dead pooled
29+
/// connections from counting against the SDK's retry limit.
30+
/// </summary>
31+
/// <remarks>
32+
/// When HttpClient reuses a connection from its pool, it may not immediately know
33+
/// if the server has closed that connection. The first write attempt will fail with
34+
/// errors like "Broken pipe" or "Connection reset". This handler catches these
35+
/// specific errors and retries the request once, allowing HttpClient to establish
36+
/// a fresh connection without consuming a retry from the SDK's retry policy.
37+
/// </remarks>
38+
public class PooledConnectionRetryHandler : DelegatingHandler
39+
{
40+
// Key used to mark requests that have already been retried by this handler
41+
private const string RetryAttemptedKey = "AmazonSDK_PooledConnectionRetryAttempted";
42+
43+
private readonly Logger _logger = Logger.GetLogger(typeof(PooledConnectionRetryHandler));
44+
45+
/// <summary>
46+
/// Initializes a new instance of the PooledConnectionRetryHandler class.
47+
/// </summary>
48+
/// <param name="innerHandler">The inner handler to delegate requests to.</param>
49+
public PooledConnectionRetryHandler(HttpMessageHandler innerHandler)
50+
: base(innerHandler)
51+
{
52+
}
53+
54+
/// <summary>
55+
/// Sends an HTTP request, automatically retrying once if the failure is due to
56+
/// a stale pooled connection.
57+
/// </summary>
58+
protected override async Task<HttpResponseMessage> SendAsync(
59+
HttpRequestMessage request,
60+
CancellationToken cancellationToken)
61+
{
62+
try
63+
{
64+
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
65+
}
66+
catch (Exception ex)
67+
{
68+
// Only retry if this is a stale connection error and we haven't already retried
69+
if (IsStaleConnectionError(ex) && !HasRetryBeenAttempted(request))
70+
{
71+
_logger.DebugFormat(
72+
"Detected stale pooled connection error: {0}. Automatically retrying request to {1}",
73+
GetErrorMessage(ex),
74+
request.RequestUri);
75+
76+
// Mark that we've attempted a retry to prevent infinite loops
77+
MarkRetryAttempted(request);
78+
79+
try
80+
{
81+
// Retry the request - HttpClient will use a fresh connection
82+
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
83+
84+
_logger.DebugFormat(
85+
"Automatic retry succeeded for request to {0}",
86+
request.RequestUri);
87+
88+
return response;
89+
}
90+
catch (Exception retryEx)
91+
{
92+
_logger.DebugFormat(
93+
"Automatic retry failed for request to {0}: {1}",
94+
request.RequestUri,
95+
GetErrorMessage(retryEx));
96+
97+
// Retry failed - throw the new exception
98+
throw;
99+
}
100+
}
101+
102+
// Not a stale connection error, or already retried - rethrow original exception
103+
throw;
104+
}
105+
}
106+
107+
/// <summary>
108+
/// Determines if an exception indicates a stale pooled connection.
109+
/// </summary>
110+
/// <remarks>
111+
/// This method relies on SocketException error codes rather than error messages,
112+
/// as error codes are stable across platforms and .NET versions, while error
113+
/// messages can vary and are subject to localization.
114+
/// </remarks>
115+
private static bool IsStaleConnectionError(Exception ex)
116+
{
117+
// Walk the exception chain looking for SocketException with known stale connection error codes
118+
var currentException = ex;
119+
while (currentException != null)
120+
{
121+
if (currentException is SocketException socketException)
122+
{
123+
// SocketError.Shutdown (32) = Broken pipe on Unix/Linux
124+
// SocketError.ConnectionReset (10054) = Connection reset by peer
125+
// SocketError.ConnectionAborted (10053) = Connection aborted
126+
if (socketException.SocketErrorCode == SocketError.Shutdown ||
127+
socketException.SocketErrorCode == SocketError.ConnectionReset ||
128+
socketException.SocketErrorCode == SocketError.ConnectionAborted)
129+
{
130+
return true;
131+
}
132+
}
133+
134+
currentException = currentException.InnerException;
135+
}
136+
137+
return false;
138+
}
139+
140+
/// <summary>
141+
/// Checks if a retry has already been attempted for this request.
142+
/// </summary>
143+
private static bool HasRetryBeenAttempted(HttpRequestMessage request)
144+
{
145+
#if NET8_0_OR_GREATER
146+
return request.Options.TryGetValue(new HttpRequestOptionsKey<bool>(RetryAttemptedKey), out var attempted) && attempted;
147+
#else
148+
return request.Properties.TryGetValue(RetryAttemptedKey, out var value) &&
149+
value is bool attempted &&
150+
attempted;
151+
#endif
152+
}
153+
154+
/// <summary>
155+
/// Marks that a retry has been attempted for this request.
156+
/// </summary>
157+
private static void MarkRetryAttempted(HttpRequestMessage request)
158+
{
159+
#if NET8_0_OR_GREATER
160+
request.Options.Set(new HttpRequestOptionsKey<bool>(RetryAttemptedKey), true);
161+
#else
162+
request.Properties[RetryAttemptedKey] = true;
163+
#endif
164+
}
165+
166+
/// <summary>
167+
/// Extracts a readable error message from an exception.
168+
/// </summary>
169+
private static string GetErrorMessage(Exception ex)
170+
{
171+
var currentException = ex;
172+
while (currentException != null)
173+
{
174+
if (currentException is IOException || currentException is SocketException)
175+
{
176+
return $"{currentException.GetType().Name}: {currentException.Message}";
177+
}
178+
currentException = currentException.InnerException;
179+
}
180+
return ex.Message;
181+
}
182+
}
183+
}

sdk/src/Core/Amazon.Runtime/Pipeline/HttpHandler/_netstandard/HttpRequestMessageFactory.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,11 @@ private static HttpClient CreateManagedHttpClient(IClientConfig clientConfig)
305305
Logger.GetLogger(typeof(HttpRequestMessageFactory)).Debug(pns, $"The current runtime does not support modifying proxy settings of HttpClient.");
306306
}
307307

308-
var httpClient = new HttpClient(httpMessageHandler);
308+
// Wrap the handler with pooled connection retry middleware to automatically
309+
// retry requests that fail due to stale connections from the connection pool.
310+
// This prevents dead pooled connections from consuming SDK retry attempts.
311+
var pooledConnectionRetryHandler = new PooledConnectionRetryHandler(httpMessageHandler);
312+
var httpClient = new HttpClient(pooledConnectionRetryHandler);
309313

310314
if (clientConfig.Timeout.HasValue)
311315
{

0 commit comments

Comments
 (0)