Skip to content

Commit a8baca1

Browse files
author
Anton Firszov
authored
Redisign HTTP/2 KeepAlive PING tests (#56736)
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
1 parent 6f19f67 commit a8baca1

6 files changed

Lines changed: 376 additions & 175 deletions

File tree

src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public class Http2LoopbackConnection : GenericLoopbackConnection
2424
private Stream _connectionStream;
2525
private TaskCompletionSource<bool> _ignoredSettingsAckPromise;
2626
private bool _ignoreWindowUpdates;
27-
private TaskCompletionSource<PingFrame> _expectPingFrame;
2827
private bool _transparentPingResponse;
2928
private readonly TimeSpan _timeout;
3029
private int _lastStreamId;
@@ -201,7 +200,7 @@ public async Task<Frame> ReadFrameAsync(CancellationToken cancellationToken)
201200
return await ReadFrameAsync(cancellationToken).ConfigureAwait(false);
202201
}
203202

204-
if (header.Type == FrameType.Ping && (_expectPingFrame != null || _transparentPingResponse))
203+
if (header.Type == FrameType.Ping && _transparentPingResponse)
205204
{
206205
PingFrame pingFrame = PingFrame.ReadFrom(header, data);
207206

@@ -237,13 +236,7 @@ public async Task<Frame> ReadFrameAsync(CancellationToken cancellationToken)
237236

238237
private async Task<bool> TryProcessExpectedPingFrameAsync(PingFrame pingFrame)
239238
{
240-
if (_expectPingFrame != null)
241-
{
242-
_expectPingFrame.SetResult(pingFrame);
243-
_expectPingFrame = null;
244-
return true;
245-
}
246-
else if (_transparentPingResponse && !pingFrame.AckFlag)
239+
if (_transparentPingResponse && !pingFrame.AckFlag)
247240
{
248241
try
249242
{
@@ -293,22 +286,6 @@ public void IgnoreWindowUpdates()
293286
_ignoreWindowUpdates = true;
294287
}
295288

296-
// Set up loopback server to expect a PING frame among other frames.
297-
// Once PING frame is read in ReadFrameAsync, the returned task is completed.
298-
// The returned task is canceled in ReadPingAsync if no PING frame has been read so far.
299-
// Does not work when Http2Options.EnableTransparentPingResponse == true
300-
public Task<PingFrame> ExpectPingFrameAsync()
301-
{
302-
if (_transparentPingResponse)
303-
{
304-
throw new InvalidOperationException(
305-
$"{nameof(Http2LoopbackConnection)}.{nameof(ExpectPingFrameAsync)} can not be used when transparent PING response is enabled.");
306-
}
307-
308-
_expectPingFrame ??= new TaskCompletionSource<PingFrame>();
309-
return _expectPingFrame.Task;
310-
}
311-
312289
public async Task ReadRstStreamAsync(int streamId)
313290
{
314291
Frame frame = await ReadFrameAsync(_timeout);
@@ -772,9 +749,6 @@ public async Task PingPong()
772749

773750
public async Task<PingFrame> ReadPingAsync(TimeSpan timeout)
774751
{
775-
_expectPingFrame?.TrySetCanceled();
776-
_expectPingFrame = null;
777-
778752
Frame frame = await ReadFrameAsync(timeout).ConfigureAwait(false);
779753
Assert.NotNull(frame);
780754
Assert.Equal(FrameType.Ping, frame.Type);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1206,7 +1206,7 @@ private Task SendSettingsAckAsync() =>
12061206
private Task SendPingAsync(long pingContent, bool isAck = false) =>
12071207
PerformWriteAsync(FrameHeader.Size + FrameHeader.PingLength, (thisRef: this, pingContent, isAck), static (state, writeBuffer) =>
12081208
{
1209-
if (NetEventSource.Log.IsEnabled()) state.thisRef.Trace("Started writing.");
1209+
if (NetEventSource.Log.IsEnabled()) state.thisRef.Trace($"Started writing. {nameof(pingContent)}={state.pingContent}");
12101210

12111211
Debug.Assert(sizeof(long) == FrameHeader.PingLength);
12121212

src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs

Lines changed: 0 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,149 +1783,6 @@ public async Task Http2_SendOverConnectionWindowSizeWithoutExplicitFlush_ClientS
17831783
}
17841784
}
17851785

1786-
public static IEnumerable<object[]> KeepAliveTestDataSource()
1787-
{
1788-
yield return new object[] { Timeout.InfiniteTimeSpan, HttpKeepAlivePingPolicy.Always, false };
1789-
yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.WithActiveRequests, false };
1790-
yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.Always, false };
1791-
yield return new object[] { TimeSpan.FromSeconds(1), HttpKeepAlivePingPolicy.WithActiveRequests, true };
1792-
}
1793-
1794-
[OuterLoop("Significant delay.")]
1795-
[MemberData(nameof(KeepAliveTestDataSource))]
1796-
[ConditionalTheory(nameof(SupportsAlpn))]
1797-
[ActiveIssue("https://github.com/dotnet/runtime/issues/41929")]
1798-
public void Http2_PingKeepAlive(TimeSpan keepAlivePingDelay, HttpKeepAlivePingPolicy keepAlivePingPolicy, bool expectRequestFail)
1799-
{
1800-
RemoteExecutor.Invoke(RunTest, keepAlivePingDelay.Ticks.ToString(), keepAlivePingPolicy.ToString(), expectRequestFail.ToString()).Dispose();
1801-
1802-
static async Task RunTest(string keepAlivePingDelayString, string keepAlivePingPolicyString, string expectRequestFailString)
1803-
{
1804-
// We should refactor this test so it can react to RTT PINGs.
1805-
// For now, avoid interference by disabling them:
1806-
AppContext.SetSwitch("System.Net.SocketsHttpHandler.Http2FlowControl.DisableDynamicWindowSizing", true);
1807-
1808-
bool expectRequestFail = bool.Parse(expectRequestFailString);
1809-
TimeSpan keepAlivePingDelay = TimeSpan.FromTicks(long.Parse(keepAlivePingDelayString));
1810-
HttpKeepAlivePingPolicy keepAlivePingPolicy = Enum.Parse<HttpKeepAlivePingPolicy>(keepAlivePingPolicyString);
1811-
1812-
TimeSpan pingTimeout = TimeSpan.FromSeconds(5);
1813-
// Simulate failure by delaying the pong, otherwise send it immediately.
1814-
TimeSpan pongDelay = expectRequestFail ? pingTimeout * 2 : TimeSpan.Zero;
1815-
// Pings are send only if KeepAlivePingDelay is not infinite.
1816-
bool expectStreamPing = keepAlivePingDelay != Timeout.InfiniteTimeSpan;
1817-
// Pings (regardless ongoing communication) are send only if sending is on and policy is set to always.
1818-
bool expectPingWithoutStream = expectStreamPing && keepAlivePingPolicy == HttpKeepAlivePingPolicy.Always;
1819-
1820-
TaskCompletionSource serverFinished = new TaskCompletionSource();
1821-
1822-
await Http2LoopbackServer.CreateClientAndServerAsync(
1823-
async uri =>
1824-
{
1825-
SocketsHttpHandler handler = new SocketsHttpHandler()
1826-
{
1827-
KeepAlivePingTimeout = pingTimeout,
1828-
KeepAlivePingPolicy = keepAlivePingPolicy,
1829-
KeepAlivePingDelay = keepAlivePingDelay
1830-
};
1831-
handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
1832-
1833-
using HttpClient client = new HttpClient(handler);
1834-
client.DefaultRequestVersion = HttpVersion.Version20;
1835-
1836-
// Warmup request to create connection.
1837-
await client.GetStringAsync(uri);
1838-
// Request under the test scope.
1839-
if (expectRequestFail)
1840-
{
1841-
await Assert.ThrowsAsync<HttpRequestException>(() => client.GetStringAsync(uri));
1842-
// As stream is closed we don't want to continue with sending data.
1843-
return;
1844-
}
1845-
else
1846-
{
1847-
await client.GetStringAsync(uri);
1848-
}
1849-
1850-
// Let connection live until server finishes.
1851-
try
1852-
{
1853-
await serverFinished.Task.WaitAsync(pingTimeout * 3);
1854-
}
1855-
catch (TimeoutException) { }
1856-
},
1857-
async server =>
1858-
{
1859-
using Http2LoopbackConnection connection = await server.EstablishConnectionAsync();
1860-
1861-
Task<PingFrame> receivePingTask = expectStreamPing ? connection.ExpectPingFrameAsync() : null;
1862-
1863-
// Warmup the connection.
1864-
int streamId1 = await connection.ReadRequestHeaderAsync();
1865-
await connection.SendDefaultResponseAsync(streamId1);
1866-
1867-
// Request under the test scope.
1868-
int streamId2 = await connection.ReadRequestHeaderAsync();
1869-
1870-
// Test ping with active stream.
1871-
if (!expectStreamPing)
1872-
{
1873-
await Assert.ThrowsAsync<OperationCanceledException>(() => connection.ReadPingAsync(pingTimeout));
1874-
}
1875-
else
1876-
{
1877-
PingFrame ping;
1878-
if (receivePingTask != null && receivePingTask.IsCompleted)
1879-
{
1880-
ping = await receivePingTask;
1881-
}
1882-
else
1883-
{
1884-
ping = await connection.ReadPingAsync(pingTimeout);
1885-
}
1886-
if (pongDelay > TimeSpan.Zero)
1887-
{
1888-
await Task.Delay(pongDelay);
1889-
}
1890-
1891-
await connection.SendPingAckAsync(ping.Data);
1892-
}
1893-
1894-
// Send response and close the stream.
1895-
if (expectRequestFail)
1896-
{
1897-
await Assert.ThrowsAsync<IOException>(() => connection.SendDefaultResponseAsync(streamId2));
1898-
// As stream is closed we don't want to continue with sending data.
1899-
return;
1900-
}
1901-
await connection.SendDefaultResponseAsync(streamId2);
1902-
// Test ping with no active stream.
1903-
if (expectPingWithoutStream)
1904-
{
1905-
PingFrame ping = await connection.ReadPingAsync(pingTimeout);
1906-
await connection.SendPingAckAsync(ping.Data);
1907-
}
1908-
else
1909-
{
1910-
// If the pings were recently coming, just give the connection time to clear up streams
1911-
// and still accept one stray ping.
1912-
if (expectStreamPing)
1913-
{
1914-
try
1915-
{
1916-
await connection.ReadPingAsync(pingTimeout);
1917-
}
1918-
catch (OperationCanceledException) { } // if it failed once, it will fail again
1919-
}
1920-
await Assert.ThrowsAsync<OperationCanceledException>(() => connection.ReadPingAsync(pingTimeout));
1921-
}
1922-
serverFinished.SetResult();
1923-
await connection.WaitForClientDisconnectAsync(true);
1924-
},
1925-
new Http2Options() { EnableTransparentPingResponse = false });
1926-
}
1927-
}
1928-
19291786
[OuterLoop("Uses Task.Delay")]
19301787
[ConditionalFact(nameof(SupportsAlpn))]
19311788
public async Task Http2_MaxConcurrentStreams_LimitEnforced()

src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2FlowControl.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics;
54
using System.Linq;
65
using System.Net.Test.Common;
76
using System.Threading;
@@ -236,7 +235,6 @@ private static async Task<int> TestClientWindowScalingAsync(
236235

237236
int nextRemainingBytes = remainingBytes - bytesToSend;
238237
bool endStream = nextRemainingBytes == 0;
239-
240238
await writeSemaphore.WaitAsync();
241239
Interlocked.Add(ref credit, -bytesToSend);
242240
await connection.SendResponseDataAsync(streamId, responseData, endStream);

0 commit comments

Comments
 (0)