Adapt existing HttpClientMiniStress tests to support HTTP/2 (dotnet/corefx#38619)
authorStephen Toub <stoub@microsoft.com>
Tue, 18 Jun 2019 22:08:38 +0000 (18:08 -0400)
committerGitHub <noreply@github.com>
Tue, 18 Jun 2019 22:08:38 +0000 (18:08 -0400)
This adapts the existing HttpClientMiniStress tests to run with HTTP/2 as well as HTTP/1.1.  Some caveats, though:
- All of these existing tests spin up a new server per request, so they're not stressing arguably the most important aspect of the client's HTTP/2 implementation, that of sharing the single connection across all requests.  Our loopback server is not currently robust enough to handle concurrent requests; we should look at switching over to using Kestrel.
- The tests run really slowly, ~40x slower than for HTTP/1.1.  However, the HTTP/1.1 tests are non-SSL, these are of course SSL, and it looks like that's actually the difference.  In particular, something about how our test server gets its test certificate appears to be taking the vast majority of the time. (We should separately see if we could tweak that, not just for this purpose, but more so to help speed up the whole suite.)

Commit migrated from https://github.com/dotnet/corefx/commit/a7c893ab3d3e50b11c7fa2338882c8964e0a290d

src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs
src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTestBase.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientMiniStressTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/PlatformHandlerTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj

index 9c6f792..db9f225 100644 (file)
@@ -194,8 +194,7 @@ namespace System.Net.Test.Common
 
             if (_options.UseSsl)
             {
-                var sslStream = new SslStream(_connectionStream, false, delegate
-                { return true; });
+                var sslStream = new SslStream(_connectionStream, false, delegate { return true; });
                 using (var cert = Configuration.Certificates.GetServerCertificate())
                 {
                     SslServerAuthenticationOptions options = new SslServerAuthenticationOptions();
index d826d24..53a0c44 100644 (file)
@@ -93,8 +93,7 @@ namespace System.Net.Test.Common
                 Stream stream = new NetworkStream(s, ownsSocket: false);
                 if (_options.UseSsl)
                 {
-                    var sslStream = new SslStream(stream, false, delegate
-                    { return true; });
+                    var sslStream = new SslStream(stream, false, delegate { return true; });
                     using (var cert = Configuration.Certificates.GetServerCertificate())
                     {
                         await sslStream.AuthenticateAsServerAsync(
index 9669c56..0219973 100644 (file)
@@ -34,7 +34,7 @@ namespace System.Net.Http.Functional.Tests
 
         protected static Version GetVersion(bool http2) => http2 ? new Version(2, 0) : HttpVersion.Version11;
 
-        protected HttpClient CreateHttpClient() => CreateHttpClient(CreateHttpClientHandler());
+        protected virtual HttpClient CreateHttpClient() => CreateHttpClient(CreateHttpClientHandler());
 
         protected HttpClient CreateHttpClient(HttpMessageHandler handler)
         {
index 2394d65..b1eb70d 100644 (file)
@@ -4,8 +4,10 @@
 
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Security;
 using System.Net.Sockets;
 using System.Net.Test.Common;
+using System.Security.Cryptography.X509Certificates;
 using System.Threading;
 using System.Threading.Tasks;
 using Xunit;
@@ -13,31 +15,108 @@ using Xunit.Abstractions;
 
 namespace System.Net.Http.Functional.Tests
 {
+    public sealed class SocketsHttpHandler_HttpClientMiniStress_NoVersion : HttpClientMiniStress
+    {
+        public SocketsHttpHandler_HttpClientMiniStress_NoVersion(ITestOutputHelper output) : base(output) { }
+        protected override bool UseSocketsHttpHandler => true;
+
+        [ConditionalTheory(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled))]
+        [InlineData(1000000)]
+        public void CreateAndDestroyManyClients(int numClients)
+        {
+            for (int i = 0; i < numClients; i++)
+            {
+                CreateHttpClient().Dispose();
+            }
+        }
+    }
+
+    public sealed class SocketsHttpHandler_HttpClientMiniStress_Http2 : HttpClientMiniStress
+    {
+        public SocketsHttpHandler_HttpClientMiniStress_Http2(ITestOutputHelper output) : base(output) { }
+        protected override bool UseSocketsHttpHandler => true;
+        protected override bool UseHttp2 => true;
+    }
+
+    public sealed class SocketsHttpHandler_HttpClientMiniStress_Http11 : HttpClientMiniStress
+    {
+        public SocketsHttpHandler_HttpClientMiniStress_Http11(ITestOutputHelper output) : base(output) { }
+        protected override bool UseSocketsHttpHandler => true;
+
+        [ConditionalTheory(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled))]
+        [MemberData(nameof(PostStressOptions))]
+        public async Task ManyClients_ManyPosts_Async(int numRequests, int dop, int numBytes)
+        {
+            (List<HttpHeaderData> headers, string content) = CreateResponse("abcdefghijklmnopqrstuvwxyz");
+            await ForCountAsync(numRequests, dop, async i =>
+            {
+                using (HttpClient client = CreateHttpClient())
+                {
+                    await CreateServerAndPostAsync(client, numBytes, headers, content);
+                }
+            });
+        }
+
+        private async Task CreateServerAndPostAsync(HttpClient client, int numBytes, List<HttpHeaderData> headers, string content)
+        {
+            string responseText = "HTTP/1.1 200 OK\r\n" + string.Join("\r\n", headers.Select(h => h.Name + ": " + h.Value)) + "\r\n\r\n" + content;
+
+            await LoopbackServer.CreateServerAsync(async (server, url) =>
+            {
+                var content = new ByteArrayContent(new byte[numBytes]);
+                Task<HttpResponseMessage> postAsync = client.PostAsync(url, content);
+
+                await server.AcceptConnectionAsync(async connection =>
+                {
+                    byte[] postData = new byte[numBytes];
+                    while (!string.IsNullOrEmpty(await connection.ReadLineAsync().ConfigureAwait(false)));
+                    Assert.Equal(numBytes, await connection.ReadBlockAsync(postData, 0, numBytes));
+
+                    await connection.Writer.WriteAsync(responseText).ConfigureAwait(false);
+                    connection.Socket.Shutdown(SocketShutdown.Send);
+                });
+
+                (await postAsync.ConfigureAwait(false)).Dispose();
+            });
+        }
+    }
+
     public abstract class HttpClientMiniStress : HttpClientHandlerTestBase
     {
         public HttpClientMiniStress(ITestOutputHelper output) : base(output) { }
 
+        protected override HttpClient CreateHttpClient() =>
+            CreateHttpClient(
+                new SocketsHttpHandler()
+                {
+                    SslOptions = new SslClientAuthenticationOptions()
+                    {
+                        RemoteCertificateValidationCallback = delegate { return true; },
+                    }
+                });
+
         [ConditionalTheory(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled))]
         [MemberData(nameof(GetStressOptions))]
         public void SingleClient_ManyGets_Sync(int numRequests, int dop, HttpCompletionOption completionOption)
         {
-            string responseText = CreateResponse("abcdefghijklmnopqrstuvwxyz");
+            (List<HttpHeaderData> headers, string content) = CreateResponse("abcd");
             using (HttpClient client = CreateHttpClient())
             {
                 Parallel.For(0, numRequests, new ParallelOptions { MaxDegreeOfParallelism = dop, TaskScheduler = new ThreadPerTaskScheduler() }, _ =>
                 {
-                    CreateServerAndGet(client, completionOption, responseText);
+                    CreateServerAndGet(client, completionOption, headers, content);
                 });
             }
         }
 
         [ConditionalTheory(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled))]
+        [MemberData(nameof(GetStressOptions))]
         public async Task SingleClient_ManyGets_Async(int numRequests, int dop, HttpCompletionOption completionOption)
         {
-            string responseText = CreateResponse("abcdefghijklmnopqrstuvwxyz");
+            (List<HttpHeaderData> headers, string content) = CreateResponse("abcdefghijklmnopqrstuvwxyz");
             using (HttpClient client = CreateHttpClient())
             {
-                await ForCountAsync(numRequests, dop, i => CreateServerAndGetAsync(client, completionOption, responseText));
+                await ForCountAsync(numRequests, dop, i => CreateServerAndGetAsync(client, completionOption, headers, content));
             }
         }
 
@@ -45,41 +124,17 @@ namespace System.Net.Http.Functional.Tests
         [MemberData(nameof(GetStressOptions))]
         public void ManyClients_ManyGets(int numRequests, int dop, HttpCompletionOption completionOption)
         {
-            string responseText = CreateResponse("abcdefghijklmnopqrstuvwxyz");
+            (List<HttpHeaderData> headers, string content) = CreateResponse("abcdefghijklmnopqrstuvwxyz");
             Parallel.For(0, numRequests, new ParallelOptions { MaxDegreeOfParallelism = dop, TaskScheduler = new ThreadPerTaskScheduler() }, _ =>
             {
                 using (HttpClient client = CreateHttpClient())
                 {
-                    CreateServerAndGet(client, completionOption, responseText);
+                    CreateServerAndGet(client, completionOption, headers, content);
                 }
             });
         }
 
         [ConditionalTheory(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled))]
-        [MemberData(nameof(PostStressOptions))]
-        public async Task ManyClients_ManyPosts_Async(int numRequests, int dop, int numBytes)
-        {
-            string responseText = CreateResponse("");
-            await ForCountAsync(numRequests, dop, async i =>
-            {
-                using (HttpClient client = CreateHttpClient())
-                {
-                    await CreateServerAndPostAsync(client, numBytes, responseText);
-                }
-            });
-        }
-
-        [ConditionalTheory(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled))]
-        [InlineData(1000000)]
-        public void CreateAndDestroyManyClients(int numClients)
-        {
-            for (int i = 0; i < numClients; i++)
-            {
-                CreateHttpClient().Dispose();
-            }
-        }
-
-        [ConditionalTheory(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled))]
         [InlineData(5000)]
         public async Task MakeAndFaultManyRequests(int numRequests)
         {
@@ -115,42 +170,35 @@ namespace System.Net.Http.Functional.Tests
                         yield return new object[] { numRequests, dop, completionoption };
         }
 
-        private static void CreateServerAndGet(HttpClient client, HttpCompletionOption completionOption, string responseText)
+        private void CreateServerAndGet(HttpClient client, HttpCompletionOption completionOption, List<HttpHeaderData> headers, string content)
         {
-            LoopbackServer.CreateServerAsync((server, url) =>
+            LoopbackServerFactory.CreateServerAsync((server, url) =>
             {
                 Task<HttpResponseMessage> getAsync = client.GetAsync(url, completionOption);
 
-                server.AcceptConnectionAsync(connection => 
-                {
-                    while (!string.IsNullOrEmpty(connection.ReadLine())) ;
-
-                    connection.Writer.Write(responseText);
-                    connection.Socket.Shutdown(SocketShutdown.Send);
+                new Task[] {
+                    getAsync,
+                    server.HandleRequestAsync(HttpStatusCode.OK, headers, content)
+                }.WhenAllOrAnyFailed().GetAwaiter().GetResult();
 
-                    return Task.CompletedTask;
-                }).GetAwaiter().GetResult();
-
-                getAsync.GetAwaiter().GetResult().Dispose();
+                getAsync.Result.Dispose();
                 return Task.CompletedTask;
             }).GetAwaiter().GetResult();
         }
 
-        private static async Task CreateServerAndGetAsync(HttpClient client, HttpCompletionOption completionOption, string responseText)
+        private async Task CreateServerAndGetAsync(HttpClient client, HttpCompletionOption completionOption, List<HttpHeaderData> headers, string content)
         {
-            await LoopbackServer.CreateServerAsync(async (server, url) =>
+            await LoopbackServerFactory.CreateServerAsync(async (server, url) =>
             {
                 Task<HttpResponseMessage> getAsync = client.GetAsync(url, completionOption);
 
-                await server.AcceptConnectionAsync(async connection => 
+                await new Task[]
                 {
-                    while (!string.IsNullOrEmpty(await connection.ReadLineAsync().ConfigureAwait(false))) ;
+                    getAsync,
+                    server.HandleRequestAsync(HttpStatusCode.OK, headers, content),
+                }.WhenAllOrAnyFailed().ConfigureAwait(false);
 
-                    await connection.Writer.WriteAsync(responseText).ConfigureAwait(false);
-                    connection.Socket.Shutdown(SocketShutdown.Send);
-                });
-
-                (await getAsync.ConfigureAwait(false)).Dispose();
+                getAsync.Result.Dispose();
             });
         }
 
@@ -162,64 +210,41 @@ namespace System.Net.Http.Functional.Tests
                         yield return new object[] { numRequests, dop, numBytes };
         }
 
-        private static async Task CreateServerAndPostAsync(HttpClient client, int numBytes, string responseText)
-        {
-            await LoopbackServer.CreateServerAsync(async (server, url) =>
-            {
-                var content = new ByteArrayContent(new byte[numBytes]);
-                Task<HttpResponseMessage> postAsync = client.PostAsync(url, content);
-
-                await server.AcceptConnectionAsync(async connection => 
-                {
-                    byte[] postData = new byte[numBytes];
-                    while (!string.IsNullOrEmpty(await connection.ReadLineAsync().ConfigureAwait(false))) ;
-                    Assert.Equal(numBytes, await connection.ReadBlockAsync(postData, 0, numBytes));
-
-                    await connection.Writer.WriteAsync(responseText).ConfigureAwait(false);
-                    connection.Socket.Shutdown(SocketShutdown.Send);
-                });
-
-                (await postAsync.ConfigureAwait(false)).Dispose();
-            });
-        }
-
         [ConditionalFact(typeof(TestEnvironment), nameof(TestEnvironment.IsStressModeEnabled))]
         public async Task UnreadResponseMessage_Collectible()
         {
-            await LoopbackServer.CreateServerAsync(async (server, url) =>
+            await LoopbackServerFactory.CreateServerAsync(async (server, url) =>
             {
                 using (HttpClient client = CreateHttpClient())
                 {
                     Func<Task<WeakReference>> getAsync = () => client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ContinueWith(t => new WeakReference(t.Result));
                     Task<WeakReference> wrt = getAsync();
 
-                    await server.AcceptConnectionAsync(async connection =>
+                    await server.HandleRequestAsync(content: new string('a', 16 * 1024));
+
+                    WeakReference wr = wrt.GetAwaiter().GetResult();
+                    Assert.True(SpinWait.SpinUntil(() =>
                     {
-                        while (!string.IsNullOrEmpty(await connection.ReadLineAsync())) ;
-                        await connection.Writer.WriteAsync(CreateResponse(new string('a', 32 * 1024)));
-
-                        WeakReference wr = wrt.GetAwaiter().GetResult();
-                        Assert.True(SpinWait.SpinUntil(() =>
-                        {
-                            GC.Collect();
-                            GC.WaitForPendingFinalizers();
-                            GC.Collect();
-                            return !wr.IsAlive;
-                        }, 10 * 1000), "Response object should have been collected");
-                    });
+                        GC.Collect();
+                        GC.WaitForPendingFinalizers();
+                        GC.Collect();
+                        return !wr.IsAlive;
+                    }, 10 * 1000), "Response object should have been collected");
                 }
             });
         }
 
-        private static string CreateResponse(string asciiBody) =>
-            $"HTTP/1.1 200 OK\r\n" +
-            $"Date: {DateTimeOffset.UtcNow:R}\r\n" +
-            "Content-Type: text/plain\r\n" +
-            $"Content-Length: {asciiBody.Length}\r\n" +
-            "\r\n" +
-            $"{asciiBody}";
+        protected (List<HttpHeaderData> Headers, string Content) CreateResponse(string asciiBody)
+        {
+            var headers = new List<HttpHeaderData>();
+            if (!UseHttp2)
+            {
+                headers.Add(new HttpHeaderData("Content-Length", asciiBody.Length.ToString()));
+            }
+            return (headers, asciiBody);
+        }
 
-        private static Task ForCountAsync(int count, int dop, Func<int, Task> bodyAsync)
+        protected static Task ForCountAsync(int count, int dop, Func<int, Task> bodyAsync)
         {
             var sched = new ThreadPerTaskScheduler();
             int nextAvailableIndex = 0;
index 3904e90..fbf0234 100644 (file)
@@ -165,12 +165,6 @@ namespace System.Net.Http.Functional.Tests
         protected override bool UseSocketsHttpHandler => false;
     }
 
-    public sealed class PlatformHandler_HttpClientMiniStress : HttpClientMiniStress
-    {
-        public PlatformHandler_HttpClientMiniStress(ITestOutputHelper output) : base(output) { }
-        protected override bool UseSocketsHttpHandler => false;
-    }
-
     public sealed class PlatformHandler_HttpClientHandlerTest : HttpClientHandlerTest
     {
         public PlatformHandler_HttpClientHandlerTest(ITestOutputHelper output) : base(output) { }
index 92cb7ed..a231cf2 100644 (file)
@@ -941,12 +941,6 @@ namespace System.Net.Http.Functional.Tests
         protected override bool UseSocketsHttpHandler => true;
     }
 
-    public sealed class SocketsHttpHandler_HttpClientMiniStress : HttpClientMiniStress
-    {
-        public SocketsHttpHandler_HttpClientMiniStress(ITestOutputHelper output) : base(output) { }
-        protected override bool UseSocketsHttpHandler => true;
-    }
-
     public sealed class SocketsHttpHandler_HttpClientHandlerTest : HttpClientHandlerTest
     {
         public SocketsHttpHandler_HttpClientHandlerTest(ITestOutputHelper output) : base(output) { }
index 71741d7..61600e8 100644 (file)
     <Compile Include="HttpClientHandlerTestBase.cs" />
     <Compile Include="HttpClientTest.cs" />
     <Compile Include="HttpClientEKUTest.cs" />
-    <Compile Include="HttpClientMiniStressTest.cs" />
     <Compile Include="HttpClient.SelectedSitesTest.cs" />
     <Compile Include="HttpContentTest.cs" />
     <Compile Include="HttpMessageInvokerTest.cs" />
     <Compile Include="DefaultCredentialsTest.cs" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetGroup)' == 'netcoreapp'">
+    <Compile Include="CustomContent.netcore.cs" />
+    <Compile Include="HPackTest.cs" />
+    <Compile Include="HttpClientTest.netcoreapp.cs" />
+    <Compile Include="HttpClientHandlerTest.Http1.cs" />
+    <Compile Include="HttpClientHandlerTest.Http2.cs" />
     <Compile Include="HttpClientHandlerTest.AcceptAllCerts.cs" />
     <Compile Include="HttpClientHandlerTest.Decompression.cs" />
-    <Compile Include="HttpClientTest.netcoreapp.cs" />
+    <Compile Include="HttpClientMiniStressTest.cs" />
     <Compile Include="HttpMethodTest.netcoreapp.cs" />
     <Compile Include="HuffmanDecodingTests.cs" />
     <Compile Include="NtAuthTests.cs" />
     <Compile Include="ReadOnlyMemoryContentTest.cs" />
     <Compile Include="SocketsHttpHandlerTest.cs" />
-    <Compile Include="HttpClientHandlerTest.Http1.cs" />
-    <Compile Include="HttpClientHandlerTest.Http2.cs" />
-    <Compile Include="CustomContent.netcore.cs" />
-    <Compile Include="HPackTest.cs" />
     <Compile Include="$(CommonTestPath)\System\Net\Http\Http2Frames.cs">
       <Link>Common\System\Net\Http\Http2Frames.cs</Link>
     </Compile>