Skip to content

Commit bf4ef8f

Browse files
committed
Fix Http 1.1's read-ahead task management
1 parent 7ffb9a4 commit bf4ef8f

File tree

2 files changed

+87
-20
lines changed

2 files changed

+87
-20
lines changed

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.Http1.cs

+35
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ private void ProcessHttp11RequestQueue(HttpConnection? connection)
114114
{
115115
if (connection is not null || _http11Connections.TryPop(out connection))
116116
{
117+
// If the connection is new, this check will always succeed as there is no scavenging task pending.
118+
if (!connection.TryOwnScavengingTaskCompletion())
119+
{
120+
goto DisposeConnection;
121+
}
122+
117123
// TryDequeueWaiter will prune completed requests from the head of the queue,
118124
// so it's possible for it to return false even though we checked that Count != 0.
119125
bool success = _http11RequestQueue.TryDequeueWaiter(this, out waiter);
@@ -144,10 +150,19 @@ private void ProcessHttp11RequestQueue(HttpConnection? connection)
144150
// and set the _http11RequestQueueIsEmptyAndNotDisposed flag to false, followed by multiple
145151
// returning connections observing the flag and calling into this method before we clear the flag.
146152
// This should be a relatively rare case, so the added contention should be minimal.
153+
154+
// We took ownership of the scavenging task completion.
155+
// If we can't return the completion (the task already completed), we must dispose the connection.
156+
if (!connection.TryReturnScavengingTaskCompletionOwnership())
157+
{
158+
goto DisposeConnection;
159+
}
160+
147161
_http11Connections.Push(connection);
148162
}
149163
else
150164
{
165+
// We may be out of available connections, check if we should inject a new one.
151166
CheckForHttp11ConnectionInjection();
152167
}
153168

@@ -163,11 +178,31 @@ private void ProcessHttp11RequestQueue(HttpConnection? connection)
163178
// before signaling the waiter. This is intentional, as the fact that
164179
// this method was called indicates that the connection is either new,
165180
// or was just returned to the pool and is still in a good state.
181+
//
182+
// We must, however, take ownership of the scavenging task completion as
183+
// there is a small chance that such a task was started if the connection
184+
// was briefly returned to the pool.
166185
return;
167186
}
168187

169188
// The request was already cancelled or handled by a different connection.
189+
190+
// We took ownership of the scavenging task completion.
191+
// If we can't return the completion (the task already completed), we must dispose the connection.
192+
if (!connection.TryReturnScavengingTaskCompletionOwnership())
193+
{
194+
goto DisposeConnection;
195+
}
196+
170197
// Loop again to try to find another request to signal, or return the connection.
198+
continue;
199+
200+
DisposeConnection:
201+
// The scavenging task completed before we assigned a request to the connection.
202+
// We've received EOF/erroneous data and the connection is not usable anymore.
203+
// Throw it away and try again.
204+
connection.Dispose();
205+
connection = null;
171206
}
172207

173208
if (_disposed)

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs

+52-20
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ internal sealed partial class HttpConnection : HttpConnectionBase
5757
private const int ReadAheadTask_NotStarted = 0;
5858
private const int ReadAheadTask_Started = 1;
5959
private const int ReadAheadTask_CompletionReserved = 2;
60+
private const int ReadAheadTask_Completed = 3;
6061
private int _readAheadTaskStatus;
6162
private ValueTask<int> _readAheadTask;
6263
private ArrayBuffer _readBuffer;
@@ -118,8 +119,11 @@ private void Dispose(bool disposing)
118119
}
119120
}
120121

122+
private bool ReadAheadTaskHasStarted =>
123+
_readAheadTaskStatus != ReadAheadTask_NotStarted;
124+
121125
/// <summary>Prepare an idle connection to be used for a new request.
122-
/// The caller MUST call SendAsync afterwards if this method returns true.</summary>
126+
/// The caller MUST call SendAsync afterwards if this method returns true, or dispose the connection if it returns false.</summary>
123127
/// <param name="async">Indicates whether the coming request will be sync or async.</param>
124128
/// <returns>True if connection can be used, false if it is invalid due to a timeout or receiving EOF or unexpected data.</returns>
125129
public bool PrepareForReuse(bool async)
@@ -133,7 +137,9 @@ public bool PrepareForReuse(bool async)
133137
// If the read-ahead task is completed, then we've received either EOF or erroneous data the connection, so it's not usable.
134138
if (ReadAheadTaskHasStarted)
135139
{
136-
return TryOwnReadAheadTaskCompletion();
140+
Debug.Assert(_readAheadTaskStatus is ReadAheadTask_Started or ReadAheadTask_Completed);
141+
142+
return Interlocked.Exchange(ref _readAheadTaskStatus, ReadAheadTask_CompletionReserved) == ReadAheadTask_Started;
137143
}
138144

139145
// Check to see if we've received anything on the connection; if we have, that's
@@ -177,6 +183,35 @@ public bool PrepareForReuse(bool async)
177183
}
178184
}
179185

186+
/// <summary>Takes ownership of the scavenging task completion if it was started.
187+
/// The caller MUST call either SendAsync or return the completion ownership afterwards if this method returns true, or dispose the connection if it returns false.</summary>
188+
public bool TryOwnScavengingTaskCompletion()
189+
{
190+
Debug.Assert(_readAheadTaskStatus != ReadAheadTask_CompletionReserved);
191+
192+
return !ReadAheadTaskHasStarted
193+
|| Interlocked.Exchange(ref _readAheadTaskStatus, ReadAheadTask_CompletionReserved) == ReadAheadTask_Started;
194+
}
195+
196+
/// <summary>Returns ownership of the scavenging task completion if it was started.
197+
/// The caller MUST Dispose the connection afterwards if this method returns false.</summary>
198+
public bool TryReturnScavengingTaskCompletionOwnership()
199+
{
200+
Debug.Assert(_readAheadTaskStatus != ReadAheadTask_Started);
201+
202+
if (!ReadAheadTaskHasStarted ||
203+
Interlocked.Exchange(ref _readAheadTaskStatus, ReadAheadTask_Started) == ReadAheadTask_CompletionReserved)
204+
{
205+
return true;
206+
}
207+
208+
// The read-ahead task has started, and we failed to transition back to CompletionReserved.
209+
// This means that the read-ahead task has completed, and we can't reuse the connection. The caller must dispose it.
210+
// We're still responsible for observing potential exceptions thrown by the read-ahead task to avoid leaking unobserved exceptions.
211+
LogExceptions(_readAheadTask.AsTask());
212+
return false;
213+
}
214+
180215
/// <summary>Check whether a currently idle connection is still usable, or should be scavenged.</summary>
181216
/// <returns>True if connection can be used, false if it is invalid due to a timeout or receiving EOF or unexpected data.</returns>
182217
public override bool CheckUsabilityOnScavenge()
@@ -187,21 +222,7 @@ public override bool CheckUsabilityOnScavenge()
187222
}
188223

189224
// We may already have a read-ahead task if we did a previous scavenge and haven't used the connection since.
190-
EnsureReadAheadTaskHasStarted();
191-
192-
// If the read-ahead task is completed, then we've received either EOF or erroneous data the connection, so it's not usable.
193-
return !_readAheadTask.IsCompleted;
194-
}
195-
196-
private bool ReadAheadTaskHasStarted =>
197-
_readAheadTaskStatus != ReadAheadTask_NotStarted;
198-
199-
private bool TryOwnReadAheadTaskCompletion() =>
200-
Interlocked.CompareExchange(ref _readAheadTaskStatus, ReadAheadTask_CompletionReserved, ReadAheadTask_Started) == ReadAheadTask_Started;
201-
202-
private void EnsureReadAheadTaskHasStarted()
203-
{
204-
if (_readAheadTaskStatus == ReadAheadTask_NotStarted)
225+
if (!ReadAheadTaskHasStarted)
205226
{
206227
Debug.Assert(_readAheadTask == default);
207228

@@ -212,6 +233,9 @@ private void EnsureReadAheadTaskHasStarted()
212233
#pragma warning restore CA2012
213234
}
214235

236+
// If the read-ahead task is completed, then we've received either EOF or erroneous data the connection, so it's not usable.
237+
return !_readAheadTask.IsCompleted;
238+
215239
async ValueTask<int> ReadAheadWithZeroByteReadAsync()
216240
{
217241
Debug.Assert(_readAheadTask == default);
@@ -231,19 +255,26 @@ async ValueTask<int> ReadAheadWithZeroByteReadAsync()
231255
// PrepareForReuse will check TryOwnReadAheadTaskCompletion before calling into SendAsync.
232256
// If we can own the completion from within the read-ahead task, it means that PrepareForReuse hasn't been called yet.
233257
// In that case we've received EOF/erroneous data before we sent the request headers, and the connection can't be reused.
234-
if (TryOwnReadAheadTaskCompletion())
258+
if (TransitionToCompletedAndTryOwnCompletion())
235259
{
236260
if (NetEventSource.Log.IsEnabled()) Trace("Read-ahead task observed data before the request was sent.");
237261
}
238262

239263
return read;
240264
}
241-
catch (Exception error) when (TryOwnReadAheadTaskCompletion())
265+
catch (Exception error) when (TransitionToCompletedAndTryOwnCompletion())
242266
{
243267
if (NetEventSource.Log.IsEnabled()) Trace($"Error performing read ahead: {error}");
244268

245269
return 0;
246270
}
271+
272+
bool TransitionToCompletedAndTryOwnCompletion()
273+
{
274+
Debug.Assert(_readAheadTaskStatus is ReadAheadTask_Started or ReadAheadTask_CompletionReserved);
275+
276+
return Interlocked.Exchange(ref _readAheadTaskStatus, ReadAheadTask_Completed) == ReadAheadTask_Started;
277+
}
247278
}
248279
}
249280

@@ -497,7 +528,8 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, boo
497528
{
498529
Debug.Assert(_currentRequest == null, $"Expected null {nameof(_currentRequest)}.");
499530
Debug.Assert(_readBuffer.ActiveLength == 0, "Unexpected data in read buffer");
500-
Debug.Assert(_readAheadTaskStatus != ReadAheadTask_Started);
531+
Debug.Assert(_readAheadTaskStatus != ReadAheadTask_Started,
532+
"The caller should have called PrepareForReuse or TryOwnScavengingTaskCompletion if the connection was idle on the pool.");
501533

502534
MarkConnectionAsNotIdle();
503535

0 commit comments

Comments
 (0)