Skip to content

Commit 7ebc067

Browse files
geoffkizerGeoffrey Kizerstephentoub
authored
improve connection scavenge logic by doing zero-byte read (#50545)
* improve connection usability check in scavenge logic by doing zero-byte read * fix assert re read buffer state in ReadAheadWithZeroByteReadAsync * Update src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs Co-authored-by: Stephen Toub <stoub@microsoft.com> * Update src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs Co-authored-by: Stephen Toub <stoub@microsoft.com> * simplify waiter dequeue Co-authored-by: Geoffrey Kizer <geoffrek@windows.microsoft.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
1 parent e9e8bc7 commit 7ebc067

File tree

4 files changed

+137
-125
lines changed

4 files changed

+137
-125
lines changed

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,12 +1587,19 @@ public bool IsExpired(long nowTicks,
15871587
(_httpStreams.Count == 0) &&
15881588
((nowTicks - _idleSinceTickCount) > connectionIdleTimeout.TotalMilliseconds))
15891589
{
1590-
if (NetEventSource.Log.IsEnabled()) Trace($"Connection no longer usable. Idle {TimeSpan.FromMilliseconds(nowTicks - _idleSinceTickCount)} > {connectionIdleTimeout}.");
1590+
if (NetEventSource.Log.IsEnabled()) Trace($"HTTP/2 connection no longer usable. Idle {TimeSpan.FromMilliseconds(nowTicks - _idleSinceTickCount)} > {connectionIdleTimeout}.");
15911591

15921592
return true;
15931593
}
15941594

1595-
return LifetimeExpired(nowTicks, connectionLifetime);
1595+
if (LifetimeExpired(nowTicks, connectionLifetime))
1596+
{
1597+
if (NetEventSource.Log.IsEnabled()) Trace($"HTTP/2 connection no longer usable. Lifetime {TimeSpan.FromMilliseconds(nowTicks - CreationTickCount)} > {connectionLifetime}.");
1598+
1599+
return true;
1600+
}
1601+
1602+
return false;
15961603
}
15971604

15981605
private void AbortStreams(Exception abortException)

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

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -135,54 +135,84 @@ private void Dispose(bool disposing)
135135
}
136136
}
137137

138-
/// <summary>Do a non-blocking poll to see whether the connection has data available or has been closed.</summary>
139-
/// <remarks>If we don't have direct access to the underlying socket, we instead use a read-ahead task.</remarks>
140-
public bool PollRead()
138+
/// <summary>Prepare an idle connection to be used for a new request.</summary>
139+
/// <param name="async">Indicates whether the coming request will be sync or async.</param>
140+
/// <returns>True if connection can be used, false if it is invalid due to receiving EOF or unexpected data.</returns>
141+
public bool PrepareForReuse(bool async)
141142
{
142-
if (_socket != null) // may be null if we don't have direct access to the socket
143+
// We may already have a read-ahead task if we did a previous scavenge and haven't used the connection since.
144+
// If the read-ahead task is completed, then we've received either EOF or erroneous data the connection, so it's not usable.
145+
if (_readAheadTask is not null)
143146
{
147+
return !_readAheadTask.Value.IsCompleted;
148+
}
149+
150+
// Check to see if we've received anything on the connection; if we have, that's
151+
// either erroneous data (we shouldn't have received anything yet) or the connection
152+
// has been closed; either way, we can't use it.
153+
if (!async && _socket is not null)
154+
{
155+
// Directly poll the socket rather than doing an async read, so that we can
156+
// issue an appropriate sync read when we actually need it.
144157
try
145158
{
146-
return _socket.Poll(0, SelectMode.SelectRead);
159+
return !_socket.Poll(0, SelectMode.SelectRead);
147160
}
148161
catch (Exception e) when (e is SocketException || e is ObjectDisposedException)
149162
{
150163
// Poll can throw when used on a closed socket.
151-
return true;
164+
return false;
152165
}
153166
}
154167
else
155168
{
156-
return EnsureReadAheadAndPollRead();
169+
// Perform an async read on the stream, since we're going to need to read from it
170+
// anyway, and in doing so we can avoid the extra syscall.
171+
try
172+
{
173+
#pragma warning disable CA2012 // we're very careful to ensure the ValueTask is only consumed once, even though it's stored into a field
174+
_readAheadTask = _stream.ReadAsync(new Memory<byte>(_readBuffer));
175+
#pragma warning restore CA2012
176+
return !_readAheadTask.Value.IsCompleted;
177+
}
178+
catch (Exception error)
179+
{
180+
// If reading throws, eat the error and don't reuse the connection.
181+
if (NetEventSource.Log.IsEnabled()) Trace($"Error performing read ahead: {error}");
182+
return false;
183+
}
157184
}
158185
}
159186

160-
/// <summary>
161-
/// Issues a read-ahead on the connection, which will serve both as the first read on the
162-
/// response as well as a polling indication of whether the connection is usable.
163-
/// </summary>
164-
/// <returns>true if there's data available on the connection or it's been closed; otherwise, false.</returns>
165-
public bool EnsureReadAheadAndPollRead()
187+
/// <summary>Check whether a currently idle connection is still usable, or should be scavenged.</summary>
188+
/// <returns>True if connection can be used, false if it is invalid due to receiving EOF or unexpected data.</returns>
189+
public bool CheckUsabilityOnScavenge()
166190
{
167-
try
191+
// We may already have a read-ahead task if we did a previous scavenge and haven't used the connection since.
192+
if (_readAheadTask is null)
168193
{
169-
Debug.Assert(_readAheadTask == null || _socket == null, "Should only already have a read-ahead task if we don't have a socket to poll");
170-
if (_readAheadTask == null)
171-
{
172194
#pragma warning disable CA2012 // we're very careful to ensure the ValueTask is only consumed once, even though it's stored into a field
173-
_readAheadTask = _stream.ReadAsync(new Memory<byte>(_readBuffer));
195+
_readAheadTask = ReadAheadWithZeroByteReadAsync();
174196
#pragma warning restore CA2012
175-
}
176197
}
177-
catch (Exception error)
198+
199+
// If the read-ahead task is completed, then we've received either EOF or erroneous data the connection, so it's not usable.
200+
return !_readAheadTask.Value.IsCompleted;
201+
202+
async ValueTask<int> ReadAheadWithZeroByteReadAsync()
178203
{
179-
// If reading throws, eat the error and don't pool the connection.
180-
if (NetEventSource.Log.IsEnabled()) Trace($"Error performing read ahead: {error}");
181-
Dispose();
182-
_readAheadTask = new ValueTask<int>(0);
183-
}
204+
Debug.Assert(_readAheadTask is null);
205+
Debug.Assert(RemainingBuffer.Length == 0);
206+
207+
// Issue a zero-byte read.
208+
// If the underlying stream supports it, this will not complete until the stream has data available,
209+
// which will avoid pinning the connection's read buffer (and possibly allow us to release it to the buffer pool in the future, if desired).
210+
// If not, it will complete immediately.
211+
await _stream.ReadAsync(Memory<byte>.Empty).ConfigureAwait(false);
184212

185-
return _readAheadTask.Value.IsCompleted; // equivalent to polling
213+
// We don't know for sure that the stream actually has data available, so we need to issue a real read now.
214+
return await _stream.ReadAsync(new Memory<byte>(_readBuffer)).ConfigureAwait(false);
215+
}
186216
}
187217

188218
private ValueTask<int>? ConsumeReadAheadTask()

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,13 @@ protected void TraceConnection(Stream stream)
6060
}
6161
}
6262

63-
private long CreationTickCount { get; } = Environment.TickCount64;
63+
public long CreationTickCount { get; } = Environment.TickCount64;
6464

6565
// Check if lifetime expired on connection.
6666
public bool LifetimeExpired(long nowTicks, TimeSpan lifetime)
6767
{
68-
bool expired =
69-
lifetime != Timeout.InfiniteTimeSpan &&
68+
return lifetime != Timeout.InfiniteTimeSpan &&
7069
(lifetime == TimeSpan.Zero || (nowTicks - CreationTickCount) > lifetime.TotalMilliseconds);
71-
72-
if (expired && NetEventSource.Log.IsEnabled()) Trace($"Connection no longer usable. Alive {TimeSpan.FromMilliseconds((nowTicks - CreationTickCount))} > {lifetime}.");
73-
return expired;
7470
}
7571

7672
internal static bool IsDigit(byte c) => (uint)(c - '0') <= '9' - '0';

0 commit comments

Comments
 (0)