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 9c6f79223f511caaa69c48cf1019032a411426ae..db9f2253f7af6a80c17266fcedcfe94713959185 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 d826d247c29d72e7e7fd73e3636c05734e0f213e..53a0c44824bea84a2d3be1d19a60f22a5fead3f5 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 9669c56eebfe31850b23ec6adec0cf0f4b9359d8..02199731eae608b0eda8ac60e4f36c7515ce249f 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 2394d6575b8f050e43f76ae2205a9298ed3d35e1..b1eb70da343677c3deab95f6272d5bfe03eebd3c 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,40 +124,16 @@ 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 3904e9017f81035e656cc56bf90b3458f8b519bf..fbf023456f9cdbf00f6d83ac4df3a8fd78cda33d 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 92cb7ed524306585f0b1459ad73eae273fd14f57..a231cf2f1b27718e2961cb1d3afa78e0fda2550b 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 71741d7857d90db1ba98228cbf9368d1e8c284c8..61600e83c5f8c160a64fc3fc255dea1b2186388b 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>