From ba0fc008b3b2bd90067ac5157a45547e28a2fb0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 May 2025 18:10:22 +0000 Subject: [PATCH 1/5] Initial plan for issue From 94b010698924f5947ab82f35ff1267f8aa613617 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 May 2025 18:17:37 +0000 Subject: [PATCH 2/5] Fix HTTP/2 pings with zero connection lifetime Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../HttpConnectionPoolManager.cs | 5 +- ...dlerTest.Http2KeepAlivePingZeroLifetime.cs | 197 ++++++++++++++++++ 2 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index 27181c7b5b8a95..8424d1b26cebe2 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -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. diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs new file mode 100644 index 00000000000000..09c5c3702a3bab --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Test.Common; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Http.Functional.Tests +{ + [Collection(nameof(DisableParallelization))] + [ConditionalClass(typeof(SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test), nameof(IsSupported))] + public sealed class SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test : HttpClientHandlerTestBase + { + public static readonly bool IsSupported = PlatformDetection.SupportsAlpn && PlatformDetection.IsNotBrowser; + + protected override Version UseVersion => HttpVersion20.Value; + + private int _pingCounter; + private Http2LoopbackConnection _connection; + private SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1); + private Channel _framesChannel = Channel.CreateUnbounded(); + private CancellationTokenSource _incomingFramesCts = new CancellationTokenSource(); + private Task _incomingFramesTask; + private TaskCompletionSource _serverFinished = new TaskCompletionSource(); + private bool _sendPingResponse = true; + + private static Http2Options NoAutoPingResponseHttp2Options => new Http2Options() { EnableTransparentPingResponse = false }; + + private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60); + + public SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test(ITestOutputHelper output) : base(output) + { + } + + [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 unexpectedFrames = new List(); + 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); + } + + private async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + Frame frame = await _connection.ReadFrameAsync(cancellationToken); + + if (frame is null) + { + break; + } + + if (frame is PingFrame pingFrame) + { + if (pingFrame.AckFlag) + { + _output?.WriteLine($"Received unexpected PING ACK ({pingFrame.Data})"); + await _framesChannel.Writer.WriteAsync(frame, cancellationToken); + } + else + { + _output?.WriteLine($"Received PING ({pingFrame.Data})"); + Interlocked.Increment(ref _pingCounter); + + if (_sendPingResponse) + { + await GuardConnectionWriteAsync(() => _connection.SendPingAckAsync(pingFrame.Data, cancellationToken), cancellationToken); + } + } + } + else if (frame is WindowUpdateFrame windowUpdateFrame) + { + _output?.WriteLine($"Received WINDOW_UPDATE"); + } + else + { + //_output?.WriteLine($"Received {frame}"); + await _framesChannel.Writer.WriteAsync(frame, cancellationToken); + } + } + } + catch (OperationCanceledException) + { + } + + _output?.WriteLine("ProcessIncomingFramesAsync finished"); + await _connection.DisposeAsync(); + } + + private async Task EstablishConnectionAsync(Http2LoopbackServer server) + { + _connection = await server.EstablishConnectionAsync(); + _incomingFramesTask = ProcessIncomingFramesAsync(_incomingFramesCts.Token); + } + + private async Task TerminateLoopbackConnectionAsync() + { + _serverFinished.SetResult(); + _incomingFramesCts.Cancel(); + await _incomingFramesTask; + } + + private async Task GuardConnectionWriteAsync(Func action, CancellationToken cancellationToken = default) + { + await _writeSemaphore.WaitAsync(cancellationToken); + await action(); + _writeSemaphore.Release(); + } + + private async Task ReadRequestHeaderFrameAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default) + { + // Receive HEADERS frame for request. + Frame frame = await _framesChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (frame == null) + { + throw new IOException("Failed to read Headers frame."); + } + + Assert.Equal(FrameType.Headers, frame.Type); + Assert.Equal(FrameFlags.EndHeaders, frame.Flags & FrameFlags.EndHeaders); + if (expectEndOfStream) + { + Assert.Equal(FrameFlags.EndStream, frame.Flags & FrameFlags.EndStream); + } + return (HeadersFrame)frame; + } + + private async Task ReadRequestHeaderAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default) + { + HeadersFrame frame = await ReadRequestHeaderFrameAsync(expectEndOfStream, cancellationToken); + return frame.StreamId; + } + } +} \ No newline at end of file From 2c238adc494e6b0c33917a2861f2477fbbb84e88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 May 2025 18:19:09 +0000 Subject: [PATCH 3/5] Add additional test case for zero lifetime without ping config Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- ...dlerTest.Http2KeepAlivePingZeroLifetime.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs index 09c5c3702a3bab..d698bcfd7b0048 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs @@ -100,6 +100,56 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => }, 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 From f1088d8fc56de51d6869696d02d573d67453465d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 May 2025 18:26:44 +0000 Subject: [PATCH 4/5] Add new test file to csproj for test inclusion Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../FunctionalTests/System.Net.Http.Functional.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index 340819fc688edb..74811f21bc9926 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -179,6 +179,7 @@ + From 2fc0eb778564bc51229f25a0a36513812ae69d47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:49:59 +0000 Subject: [PATCH 5/5] Consolidate zero lifetime ping tests into existing test file Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- ...cketsHttpHandlerTest.Http2KeepAlivePing.cs | 112 ++++++++ ...dlerTest.Http2KeepAlivePingZeroLifetime.cs | 247 ------------------ .../System.Net.Http.Functional.Tests.csproj | 1 - 3 files changed, 112 insertions(+), 248 deletions(-) delete mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs index 0f1c1b8d58c326..b60211bc21e56f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs @@ -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 unexpectedFrames = new List(); + 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 diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs deleted file mode 100644 index d698bcfd7b0048..00000000000000 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePingZeroLifetime.cs +++ /dev/null @@ -1,247 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Test.Common; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; - -namespace System.Net.Http.Functional.Tests -{ - [Collection(nameof(DisableParallelization))] - [ConditionalClass(typeof(SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test), nameof(IsSupported))] - public sealed class SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test : HttpClientHandlerTestBase - { - public static readonly bool IsSupported = PlatformDetection.SupportsAlpn && PlatformDetection.IsNotBrowser; - - protected override Version UseVersion => HttpVersion20.Value; - - private int _pingCounter; - private Http2LoopbackConnection _connection; - private SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1); - private Channel _framesChannel = Channel.CreateUnbounded(); - private CancellationTokenSource _incomingFramesCts = new CancellationTokenSource(); - private Task _incomingFramesTask; - private TaskCompletionSource _serverFinished = new TaskCompletionSource(); - private bool _sendPingResponse = true; - - private static Http2Options NoAutoPingResponseHttp2Options => new Http2Options() { EnableTransparentPingResponse = false }; - - private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60); - - public SocketsHttpHandler_Http2KeepAlivePingZeroLifetime_Test(ITestOutputHelper output) : base(output) - { - } - - [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 unexpectedFrames = new List(); - 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 - { - while (!cancellationToken.IsCancellationRequested) - { - Frame frame = await _connection.ReadFrameAsync(cancellationToken); - - if (frame is null) - { - break; - } - - if (frame is PingFrame pingFrame) - { - if (pingFrame.AckFlag) - { - _output?.WriteLine($"Received unexpected PING ACK ({pingFrame.Data})"); - await _framesChannel.Writer.WriteAsync(frame, cancellationToken); - } - else - { - _output?.WriteLine($"Received PING ({pingFrame.Data})"); - Interlocked.Increment(ref _pingCounter); - - if (_sendPingResponse) - { - await GuardConnectionWriteAsync(() => _connection.SendPingAckAsync(pingFrame.Data, cancellationToken), cancellationToken); - } - } - } - else if (frame is WindowUpdateFrame windowUpdateFrame) - { - _output?.WriteLine($"Received WINDOW_UPDATE"); - } - else - { - //_output?.WriteLine($"Received {frame}"); - await _framesChannel.Writer.WriteAsync(frame, cancellationToken); - } - } - } - catch (OperationCanceledException) - { - } - - _output?.WriteLine("ProcessIncomingFramesAsync finished"); - await _connection.DisposeAsync(); - } - - private async Task EstablishConnectionAsync(Http2LoopbackServer server) - { - _connection = await server.EstablishConnectionAsync(); - _incomingFramesTask = ProcessIncomingFramesAsync(_incomingFramesCts.Token); - } - - private async Task TerminateLoopbackConnectionAsync() - { - _serverFinished.SetResult(); - _incomingFramesCts.Cancel(); - await _incomingFramesTask; - } - - private async Task GuardConnectionWriteAsync(Func action, CancellationToken cancellationToken = default) - { - await _writeSemaphore.WaitAsync(cancellationToken); - await action(); - _writeSemaphore.Release(); - } - - private async Task ReadRequestHeaderFrameAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default) - { - // Receive HEADERS frame for request. - Frame frame = await _framesChannel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - if (frame == null) - { - throw new IOException("Failed to read Headers frame."); - } - - Assert.Equal(FrameType.Headers, frame.Type); - Assert.Equal(FrameFlags.EndHeaders, frame.Flags & FrameFlags.EndHeaders); - if (expectEndOfStream) - { - Assert.Equal(FrameFlags.EndStream, frame.Flags & FrameFlags.EndStream); - } - return (HeadersFrame)frame; - } - - private async Task ReadRequestHeaderAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default) - { - HeadersFrame frame = await ReadRequestHeaderFrameAsync(expectEndOfStream, cancellationToken); - return frame.StreamId; - } - } -} \ No newline at end of file diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index 74811f21bc9926..340819fc688edb 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -179,7 +179,6 @@ -