Redisign HTTP/2 KeepAlive PING tests (#56736)
authorAnton Firszov <Anton.Firszov@microsoft.com>
Fri, 6 Aug 2021 12:41:04 +0000 (14:41 +0200)
committerGitHub <noreply@github.com>
Fri, 6 Aug 2021 12:41:04 +0000 (14:41 +0200)
Completely redesign tests for HTTP/2 KeepAlive PING, so they:
- Work well with RTT pings introduced in Implement dynamic HTTP/2 window scaling #54755
- Run sequentially, reducing the chance of failing because of timing issues caused by parallel workloads
- Are better organized: multiple test cases for different scenarios, instead of one theory with complex branches on parameters

Instead of reading / reacting to frames inline, there is a separate Task for processing incoming frames, responding to PING immediately and pushing other frames to a Channel<Frame>.

Fixes #41929

src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2FlowControl.cs
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs [new file with mode: 0644]
src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj

index 4840ad7..b2c5d70 100644 (file)
@@ -24,7 +24,6 @@ namespace System.Net.Test.Common
         private Stream _connectionStream;
         private TaskCompletionSource<bool> _ignoredSettingsAckPromise;
         private bool _ignoreWindowUpdates;
-        private TaskCompletionSource<PingFrame> _expectPingFrame;
         private bool _transparentPingResponse;
         private readonly TimeSpan _timeout;
         private int _lastStreamId;
@@ -201,7 +200,7 @@ namespace System.Net.Test.Common
                 return await ReadFrameAsync(cancellationToken).ConfigureAwait(false);
             }
 
-            if (header.Type == FrameType.Ping && (_expectPingFrame != null || _transparentPingResponse))
+            if (header.Type == FrameType.Ping && _transparentPingResponse)
             {
                 PingFrame pingFrame = PingFrame.ReadFrom(header, data);
 
@@ -237,13 +236,7 @@ namespace System.Net.Test.Common
 
         private async Task<bool> TryProcessExpectedPingFrameAsync(PingFrame pingFrame)
         {
-            if (_expectPingFrame != null)
-            {
-                _expectPingFrame.SetResult(pingFrame);
-                _expectPingFrame = null;
-                return true;
-            }
-            else if (_transparentPingResponse && !pingFrame.AckFlag)
+            if (_transparentPingResponse && !pingFrame.AckFlag)
             {
                 try
                 {
@@ -293,22 +286,6 @@ namespace System.Net.Test.Common
             _ignoreWindowUpdates = true;
         }
 
-        // Set up loopback server to expect a PING frame among other frames.
-        // Once PING frame is read in ReadFrameAsync, the returned task is completed.
-        // The returned task is canceled in ReadPingAsync if no PING frame has been read so far.
-        // Does not work when Http2Options.EnableTransparentPingResponse == true
-        public Task<PingFrame> ExpectPingFrameAsync()
-        {
-            if (_transparentPingResponse)
-            {
-                throw new InvalidOperationException(
-                    $"{nameof(Http2LoopbackConnection)}.{nameof(ExpectPingFrameAsync)} can not be used when transparent PING response is enabled.");
-            }
-
-            _expectPingFrame ??= new TaskCompletionSource<PingFrame>();
-            return _expectPingFrame.Task;
-        }
-
         public async Task ReadRstStreamAsync(int streamId)
         {
             Frame frame = await ReadFrameAsync(_timeout);
@@ -772,9 +749,6 @@ namespace System.Net.Test.Common
 
         public async Task<PingFrame> ReadPingAsync(TimeSpan timeout)
         {
-            _expectPingFrame?.TrySetCanceled();
-            _expectPingFrame = null;
-
             Frame frame = await ReadFrameAsync(timeout).ConfigureAwait(false);
             Assert.NotNull(frame);
             Assert.Equal(FrameType.Ping, frame.Type);
index ce524c4..b6a33b2 100644 (file)
@@ -1206,7 +1206,7 @@ namespace System.Net.Http
         private Task SendPingAsync(long pingContent, bool isAck = false) =>
             PerformWriteAsync(FrameHeader.Size + FrameHeader.PingLength, (thisRef: this, pingContent, isAck), static (state, writeBuffer) =>
             {
-                if (NetEventSource.Log.IsEnabled()) state.thisRef.Trace("Started writing.");
+                if (NetEventSource.Log.IsEnabled()) state.thisRef.Trace($"Started writing. {nameof(pingContent)}={state.pingContent}");
 
                 Debug.Assert(sizeof(long) == FrameHeader.PingLength);
 
index a66c33b..fc94b54 100644 (file)
@@ -1783,149 +1783,6 @@ namespace System.Net.Http.Functional.Tests
             }
         }
 
-        public static IEnumerable<object[]> KeepAliveTestDataSource()
-        {
-            yield return new object[] { Timeout.InfiniteTimeSpan, HttpKeepAlivePingPolicy.Always, false };
-            yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.WithActiveRequests, false };
-            yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.Always, false };
-            yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.WithActiveRequests, true };
-        }
-
-        [OuterLoop("Significant delay.")]
-        [MemberData(nameof(KeepAliveTestDataSource))]
-        [ConditionalTheory(nameof(SupportsAlpn))]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/41929")]
-        public void Http2_PingKeepAlive(TimeSpan keepAlivePingDelay, HttpKeepAlivePingPolicy keepAlivePingPolicy, bool expectRequestFail)
-        {
-            RemoteExecutor.Invoke(RunTest, keepAlivePingDelay.Ticks.ToString(), keepAlivePingPolicy.ToString(), expectRequestFail.ToString()).Dispose();
-
-            static async Task RunTest(string keepAlivePingDelayString, string keepAlivePingPolicyString, string expectRequestFailString)
-            {
-                // We should refactor this test so it can react to RTT PINGs.
-                // For now, avoid interference by disabling them:
-                AppContext.SetSwitch("System.Net.SocketsHttpHandler.Http2FlowControl.DisableDynamicWindowSizing", true);
-
-                bool expectRequestFail = bool.Parse(expectRequestFailString);
-                TimeSpan keepAlivePingDelay = TimeSpan.FromTicks(long.Parse(keepAlivePingDelayString));
-                HttpKeepAlivePingPolicy keepAlivePingPolicy = Enum.Parse<HttpKeepAlivePingPolicy>(keepAlivePingPolicyString);
-
-                TimeSpan pingTimeout = TimeSpan.FromSeconds(5);
-                // Simulate failure by delaying the pong, otherwise send it immediately.
-                TimeSpan pongDelay = expectRequestFail ? pingTimeout * 2 : TimeSpan.Zero;
-                // Pings are send only if KeepAlivePingDelay is not infinite.
-                bool expectStreamPing = keepAlivePingDelay != Timeout.InfiniteTimeSpan;
-                // Pings (regardless ongoing communication) are send only if sending is on and policy is set to always.
-                bool expectPingWithoutStream = expectStreamPing && keepAlivePingPolicy == HttpKeepAlivePingPolicy.Always;
-
-                TaskCompletionSource serverFinished = new TaskCompletionSource();
-
-                await Http2LoopbackServer.CreateClientAndServerAsync(
-                    async uri =>
-                    {
-                        SocketsHttpHandler handler = new SocketsHttpHandler()
-                        {
-                            KeepAlivePingTimeout = pingTimeout,
-                            KeepAlivePingPolicy = keepAlivePingPolicy,
-                            KeepAlivePingDelay = keepAlivePingDelay
-                        };
-                        handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
-
-                        using HttpClient client = new HttpClient(handler);
-                        client.DefaultRequestVersion = HttpVersion.Version20;
-
-                        // Warmup request to create connection.
-                        await client.GetStringAsync(uri);
-                        // Request under the test scope.
-                        if (expectRequestFail)
-                        {
-                            await Assert.ThrowsAsync<HttpRequestException>(() => client.GetStringAsync(uri));
-                            // As stream is closed we don't want to continue with sending data.
-                            return;
-                        }
-                        else
-                        {
-                            await client.GetStringAsync(uri);
-                        }
-
-                        // Let connection live until server finishes.
-                        try
-                        {
-                            await serverFinished.Task.WaitAsync(pingTimeout * 3);
-                        }
-                        catch (TimeoutException) { }
-                    },
-                    async server =>
-                    {
-                        using Http2LoopbackConnection connection = await server.EstablishConnectionAsync();
-
-                        Task<PingFrame> receivePingTask = expectStreamPing ? connection.ExpectPingFrameAsync() : null;
-
-                        // Warmup the connection.
-                        int streamId1 = await connection.ReadRequestHeaderAsync();
-                        await connection.SendDefaultResponseAsync(streamId1);
-
-                        // Request under the test scope.
-                        int streamId2 = await connection.ReadRequestHeaderAsync();
-
-                        // Test ping with active stream.
-                        if (!expectStreamPing)
-                        {
-                            await Assert.ThrowsAsync<OperationCanceledException>(() => connection.ReadPingAsync(pingTimeout));
-                        }
-                        else
-                        {
-                            PingFrame ping;
-                            if (receivePingTask != null && receivePingTask.IsCompleted)
-                            {
-                                ping = await receivePingTask;
-                            }
-                            else
-                            {
-                                ping = await connection.ReadPingAsync(pingTimeout);
-                            }
-                            if (pongDelay > TimeSpan.Zero)
-                            {
-                                await Task.Delay(pongDelay);
-                            }
-
-                            await connection.SendPingAckAsync(ping.Data);
-                        }
-
-                        // Send response and close the stream.
-                        if (expectRequestFail)
-                        {
-                            await Assert.ThrowsAsync<IOException>(() => connection.SendDefaultResponseAsync(streamId2));
-                            // As stream is closed we don't want to continue with sending data.
-                            return;
-                        }
-                        await connection.SendDefaultResponseAsync(streamId2);
-                        // Test ping with no active stream.
-                        if (expectPingWithoutStream)
-                        {
-                            PingFrame ping = await connection.ReadPingAsync(pingTimeout);
-                            await connection.SendPingAckAsync(ping.Data);
-                        }
-                        else
-                        {
-                            // If the pings were recently coming, just give the connection time to clear up streams
-                            // and still accept one stray ping.
-                            if (expectStreamPing)
-                            {
-                                try
-                                {
-                                    await connection.ReadPingAsync(pingTimeout);
-                                }
-                                catch (OperationCanceledException) { } // if it failed once, it will fail again
-                            }
-                            await Assert.ThrowsAsync<OperationCanceledException>(() => connection.ReadPingAsync(pingTimeout));
-                        }
-                        serverFinished.SetResult();
-                        await connection.WaitForClientDisconnectAsync(true);
-                    },
-                    new Http2Options() { EnableTransparentPingResponse = false });
-            }
-        }
-
         [OuterLoop("Uses Task.Delay")]
         [ConditionalFact(nameof(SupportsAlpn))]
         public async Task Http2_MaxConcurrentStreams_LimitEnforced()
index 36bb2c9..84ea142 100644 (file)
@@ -1,7 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Diagnostics;
 using System.Linq;
 using System.Net.Test.Common;
 using System.Threading;
@@ -236,7 +235,6 @@ namespace System.Net.Http.Functional.Tests
 
                 int nextRemainingBytes = remainingBytes - bytesToSend;
                 bool endStream = nextRemainingBytes == 0;
-
                 await writeSemaphore.WaitAsync();
                 Interlocked.Add(ref credit, -bytesToSend);
                 await connection.SendResponseDataAsync(streamId, responseData, endStream);
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2KeepAlivePing.cs
new file mode 100644 (file)
index 0000000..aad7f5d
--- /dev/null
@@ -0,0 +1,371 @@
+// 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(NonParallelTestCollection))]
+    [ConditionalClass(typeof(SocketsHttpHandler_Http2KeepAlivePing_Test), nameof(IsSupported))]
+    public sealed class SocketsHttpHandler_Http2KeepAlivePing_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<Frame> _framesChannel = Channel.CreateUnbounded<Frame>();
+        private CancellationTokenSource _incomingFramesCts = new CancellationTokenSource();
+        private Task _incomingFramesTask;
+        private TaskCompletionSource _serverFinished = new TaskCompletionSource();
+        private int _sendPingResponse = 1;
+
+        private static Http2Options NoAutoPingResponseHttp2Options => new Http2Options() { EnableTransparentPingResponse = false };
+
+        private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
+
+        public SocketsHttpHandler_Http2KeepAlivePing_Test(ITestOutputHelper output) : base(output)
+        {
+        }
+
+        [OuterLoop("Runs long")]
+        [Fact]
+        public async Task KeepAlivePingDelay_Infinite_NoKeepAlivePingIsSent()
+        {
+            await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
+            {
+                SocketsHttpHandler handler = new SocketsHttpHandler()
+                {
+                    KeepAlivePingTimeout = TimeSpan.FromSeconds(1),
+                    KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
+                    KeepAlivePingDelay = Timeout.InfiniteTimeSpan
+                };
+                handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+
+                using HttpClient client = new HttpClient(handler);
+                client.DefaultRequestVersion = HttpVersion.Version20;
+
+                // Warmup request to create connection:
+                await client.GetStringAsync(uri);
+
+                // Actual request:
+                await client.GetStringAsync(uri);
+
+                // Let connection live until server finishes:
+                await _serverFinished.Task.WaitAsync(TestTimeout);
+            },
+            async server =>
+            {
+                await EstablishConnectionAsync(server);
+
+                // Warmup the connection.
+                int streamId1 = await ReadRequestHeaderAsync();
+                await GuardConnetionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
+
+                Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter
+                // Request under the test scope.
+                int streamId2 = await ReadRequestHeaderAsync();
+
+                // Simulate inactive period:
+                await Task.Delay(5_000);
+
+                // We may have received one RTT PING in response to HEADERS, but should receive no KeepAlive PING
+                Assert.True(_pingCounter <= 1);
+                Interlocked.Exchange(ref _pingCounter, 0); // reset the counter
+
+                // Finish the response:
+                await GuardConnetionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
+
+                // Simulate inactive period:
+                await Task.Delay(5_000);
+
+                // We may have received one RTT PING in response to HEADERS, but should receive no KeepAlive PING
+                Assert.True(_pingCounter <= 1);
+
+                await TerminateLoopbackConnectionAsync();
+            }).WaitAsync(TestTimeout);
+        }
+
+        [OuterLoop("Runs long")]
+        [Theory]
+        [InlineData(HttpKeepAlivePingPolicy.Always)]
+        [InlineData(HttpKeepAlivePingPolicy.WithActiveRequests)]
+        public async Task KeepAliveConfigured_KeepAlivePingsAreSentAccordingToPolicy(HttpKeepAlivePingPolicy policy)
+        {
+            await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
+            {
+                SocketsHttpHandler handler = new SocketsHttpHandler()
+                {
+                    KeepAlivePingTimeout = TimeSpan.FromSeconds(10),
+                    KeepAlivePingPolicy = policy,
+                    KeepAlivePingDelay = TimeSpan.FromSeconds(1)
+                };
+                handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+
+                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 GuardConnetionWriteAsync(() => _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);
+
+                // 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 GuardConnetionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
+                Interlocked.Exchange(ref _pingCounter, 0); // reset the PING counter
+
+                if (policy == HttpKeepAlivePingPolicy.Always)
+                {
+                    // Simulate inactive period:
+                    await Task.Delay(5_000);
+
+                    // 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);
+                }
+                else
+                {
+                    // We should receive no more KeepAlive PINGs
+                    Assert.True(_pingCounter <= 1);
+                }
+
+                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).WaitAsync(TestTimeout);
+        }
+
+        [OuterLoop("Runs long")]
+        [Fact]
+        public async Task KeepAliveConfigured_NoPingResponseDuringActiveStream_RequestShouldFail()
+        {
+            await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
+            {
+                SocketsHttpHandler handler = new SocketsHttpHandler()
+                {
+                    KeepAlivePingTimeout = TimeSpan.FromSeconds(1.5),
+                    KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests,
+                    KeepAlivePingDelay = TimeSpan.FromSeconds(1)
+                };
+                handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+
+                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:
+                await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync(uri));
+
+                // Let connection live until server finishes:
+                await _serverFinished.Task;
+            },
+            async server =>
+            {
+                await EstablishConnectionAsync(server);
+
+                // Warmup the connection.
+                int streamId1 = await ReadRequestHeaderAsync();
+                await GuardConnetionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
+
+                // Request under the test scope.
+                int streamId2 = await ReadRequestHeaderAsync();
+
+                DisablePingResponse();
+
+                // Simulate inactive period:
+                await Task.Delay(6_000);
+
+                // Finish the response:
+                await GuardConnetionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
+
+                await TerminateLoopbackConnectionAsync();
+            }, NoAutoPingResponseHttp2Options).WaitAsync(TestTimeout);
+        }
+
+        [OuterLoop("Runs long")]
+        [Fact]
+        public async Task HttpKeepAlivePingPolicy_Always_NoPingResponseBetweenStreams_SecondRequestShouldFail()
+        {
+            await Http2LoopbackServer.CreateClientAndServerAsync(async uri =>
+            {
+                SocketsHttpHandler handler = new SocketsHttpHandler()
+                {
+                    KeepAlivePingTimeout = TimeSpan.FromSeconds(1.5),
+                    KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
+                    KeepAlivePingDelay = TimeSpan.FromSeconds(1)
+                };
+                handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
+
+                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);
+
+                // Second request should fail:
+                await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync(uri));
+
+                // Let connection live until server finishes:
+                await _serverFinished.Task;
+            },
+            async server =>
+            {
+                await EstablishConnectionAsync(server);
+
+                // Warmup the connection.
+                int streamId1 = await ReadRequestHeaderAsync();
+                await GuardConnetionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId1));
+
+                DisablePingResponse();
+
+                // Simulate inactive period:
+                await Task.Delay(6_000);
+
+                // Request under the test scope.
+                int streamId2 = await ReadRequestHeaderAsync();
+
+                // Finish the response:
+                await GuardConnetionWriteAsync(() => _connection.SendDefaultResponseAsync(streamId2));
+
+                await TerminateLoopbackConnectionAsync();
+            }, NoAutoPingResponseHttp2Options).WaitAsync(TestTimeout);
+        }
+
+        private async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    Frame frame = await _connection.ReadFrameAsync(cancellationToken);
+
+                    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 > 0)
+                            {
+                                await GuardConnetionWriteAsync(() => _connection.SendPingAckAsync(pingFrame.Data, cancellationToken), cancellationToken);
+                            }
+                        }
+                    }
+                    else if (frame is WindowUpdateFrame windowUpdateFrame)
+                    {
+                        _output?.WriteLine($"Received WINDOW_UPDATE");
+                    }
+                    else if (frame is not null)
+                    {
+                        //_output?.WriteLine($"Received {frame}");
+                        await _framesChannel.Writer.WriteAsync(frame, cancellationToken);
+                    }
+                }
+            }
+            catch (OperationCanceledException)
+            {
+            }
+
+            _output?.WriteLine("ProcessIncomingFramesAsync finished");
+            _connection.Dispose();
+        }
+
+        private void DisablePingResponse() => Interlocked.Exchange(ref _sendPingResponse, 0);
+
+        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 GuardConnetionWriteAsync(Func<Task> action, CancellationToken cancellationToken = default)
+        {
+            await _writeSemaphore.WaitAsync(cancellationToken);
+            await action();
+            _writeSemaphore.Release();
+        }
+
+        private async Task<HeadersFrame> 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<int> ReadRequestHeaderAsync(bool expectEndOfStream = true, CancellationToken cancellationToken = default)
+        {
+            HeadersFrame frame = await ReadRequestHeaderFrameAsync(expectEndOfStream, cancellationToken);
+            return frame.StreamId;
+        }
+    }
+}
index 667b35e..5c17759 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <StringResourcesPath>../../src/Resources/Strings.resx</StringResourcesPath>
     <DefineConstants Condition="'$(TargetsWindows)'=='true'">$(DefineConstants);TargetsWindows</DefineConstants>
     <Compile Include="HttpClientHandlerTest.AltSvc.cs" />
     <Compile Include="SocketsHttpHandlerTest.Cancellation.cs" />
     <Compile Include="SocketsHttpHandlerTest.Http2FlowControl.cs" />
+    <Compile Include="SocketsHttpHandlerTest.Http2KeepAlivePing.cs" />
     <Compile Include="HttpClientHandlerTest.Connect.cs" />
     <Compile Include="HttpClientHandlerTest.Finalization.cs" />
     <Compile Include="HttpClientHandlerTest.Headers.cs" />