Skip to content

Fix HTTP/2 pings not working with zero connection lifetime #115735

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ public HttpConnectionPoolManager(HttpConnectionSettings settings)
// However, we can only do such optimizations if we're not also tracking
// connections per server, as we use data in the associated data structures
// to do that tracking.
// Additionally, we should not avoid storing connections if keep-alive ping is configured,
// as the heartbeat timer is needed for ping functionality.
bool avoidStoringConnections =
settings._maxConnectionsPerServer == int.MaxValue &&
(settings._pooledConnectionIdleTimeout == TimeSpan.Zero ||
settings._pooledConnectionLifetime == TimeSpan.Zero);
settings._pooledConnectionLifetime == TimeSpan.Zero) &&
settings._keepAlivePingDelay == Timeout.InfiniteTimeSpan;

// Start out with the timer not running, since we have no pools.
// When it does run, run it with a frequency based on the idle timeout.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,118 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
}, NoAutoPingResponseHttp2Options);
}

[OuterLoop("Runs long")]
[Fact]
public async Task KeepAlivePing_ZeroConnectionLifetime_PingsStillWork()
{
await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
{
SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
handler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
handler.KeepAlivePingDelay = TimeSpan.FromSeconds(1);
handler.PooledConnectionLifetime = TimeSpan.Zero; // Zero lifetime should still allow pings

using HttpClient client = new HttpClient(handler);
client.DefaultRequestVersion = HttpVersion.Version20;

// Warmup request to create connection:
HttpResponseMessage response0 = await client.GetAsync(uri);
Assert.Equal(HttpStatusCode.OK, response0.StatusCode);

// Actual request:
HttpResponseMessage response1 = await client.GetAsync(uri);
Assert.Equal(HttpStatusCode.OK, response1.StatusCode);

// Let connection live until server finishes:
await _serverFinished.Task.WaitAsync(TestTimeout);
},
async server =>
{
await EstablishConnectionAsync(server);

// Warmup the connection.
int streamId1 = await ReadRequestHeaderAsync();
await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));

// Request under the test scope.
int streamId2 = await ReadRequestHeaderAsync();
Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter

// Simulate inactive period:
await Task.Delay(5_000);

// Even with zero lifetime, we should still receive pings for active streams:
// We may receive one RTT PING in response to HEADERS.
// Upon that, we expect to receive at least 1 keep alive PING:
Assert.True(_pingCounter > 1);

// Finish the response:
await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));

await TerminateLoopbackConnectionAsync();

List<Frame> unexpectedFrames = new List<Frame>();
while (_framesChannel.Reader.Count > 0)
{
Frame unexpectedFrame = await _framesChannel.Reader.ReadAsync();
unexpectedFrames.Add(unexpectedFrame);
}

Assert.False(unexpectedFrames.Any(), "Received unexpected frames: \n" + string.Join('\n', unexpectedFrames.Select(f => f.ToString()).ToArray()));
}, NoAutoPingResponseHttp2Options);
}

[OuterLoop("Runs long")]
[Fact]
public async Task KeepAlivePing_ZeroConnectionLifetime_NoPingConfig_NoPingsSent()
{
await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
{
SocketsHttpHandler handler = CreateSocketsHttpHandler(allowAllCertificates: true);
// Don't configure KeepAlivePing settings
handler.PooledConnectionLifetime = TimeSpan.Zero; // Zero lifetime

using HttpClient client = new HttpClient(handler);
client.DefaultRequestVersion = HttpVersion.Version20;

// Warmup request to create connection:
HttpResponseMessage response0 = await client.GetAsync(uri);
Assert.Equal(HttpStatusCode.OK, response0.StatusCode);

// Actual request:
HttpResponseMessage response1 = await client.GetAsync(uri);
Assert.Equal(HttpStatusCode.OK, response1.StatusCode);

// Let connection live until server finishes:
await _serverFinished.Task.WaitAsync(TestTimeout);
},
async server =>
{
await EstablishConnectionAsync(server);

// Warmup the connection.
int streamId1 = await ReadRequestHeaderAsync();
await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));

// Request under the test scope.
int streamId2 = await ReadRequestHeaderAsync();
Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter

// Simulate inactive period:
await Task.Delay(5_000);

// With zero lifetime and no ping config, we should not receive keep alive pings
// We may have received one RTT PING in response to HEADERS, but should receive no KeepAlive PING
Assert.True(_pingCounter <= 1);

// Finish the response:
await GuardConnectionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));

await TerminateLoopbackConnectionAsync();
}, NoAutoPingResponseHttp2Options);
}

private async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
{
try
Expand Down