Use docker-compose for running stress tests (dotnet/corefx#42186)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Tue, 29 Oct 2019 16:46:09 +0000 (16:46 +0000)
committerGitHub <noreply@github.com>
Tue, 29 Oct 2019 16:46:09 +0000 (16:46 +0000)
HttpStress: add docker-compose scenario

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

eng/pipelines/libraries/stress/http-linux.yml
src/libraries/System.Net.Http/tests/StressTests/HttpStress/Configuration.cs
src/libraries/System.Net.Http/tests/StressTests/HttpStress/Dockerfile
src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs
src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressClient.cs
src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressServer.cs
src/libraries/System.Net.Http/tests/StressTests/HttpStress/corefx.Dockerfile
src/libraries/System.Net.Http/tests/StressTests/HttpStress/docker-compose.yml [new file with mode: 0644]

index e1936fd..7a15fc9 100644 (file)
@@ -15,14 +15,16 @@ steps:
   lfs: false
 
 - bash: |
-    docker build -t $(sdkBaseImage) -f $(Build.SourcesDirectory)/$(HttpStressProject)/corefx.Dockerfile .
+    docker build -t $(sdkBaseImage) --build-arg CONFIGURATION=$(BUILD_CONFIGURATION) -f $(Build.SourcesDirectory)/$(HttpStressProject)/corefx.Dockerfile .
   displayName: Build Corefx
 
 - bash: |
     cd '$(Build.SourcesDirectory)/$(HttpStressProject)'
-    docker build -t $(httpStressImage) --build-arg SDK_BASE_IMAGE=$(sdkBaseImage) .
+    docker build -t $(httpStressImage) --build-arg SDK_BASE_IMAGE=$(sdkBaseImage) --build-arg CONFIGURATION=$(BUILD_CONFIGURATION) .
   displayName: Build HttpStress
 
 - bash: |
-    docker run --rm -e HTTPSTRESS_ARGS='$(HTTPSTRESS_ARGS)' $(httpStressImage)
+    cd '$(Build.SourcesDirectory)/$(HttpStressProject)'
+    export HTTPSTRESS_ARGS='$(HTTPSTRESS_ARGS)'
+    docker-compose up --abort-on-container-exit --no-color
   displayName: Run HttpStress
index b27bfc7..f92ceb0 100644 (file)
@@ -22,7 +22,7 @@ namespace HttpStress
 
     public class Configuration
     {
-        public Uri ServerUri { get; set; } = new Uri("http://placeholder");
+        public string ServerUri { get; set; } = "";
         public RunMode RunMode { get; set; }
         public bool ListOperations { get; set; }
 
index 8f6571a..d4513ef 100644 (file)
@@ -4,5 +4,11 @@ FROM $SDK_BASE_IMAGE
 WORKDIR /app
 COPY . .
 
+ARG CONFIGURATION=Release
+RUN dotnet build -c $CONFIGURATION
+
+EXPOSE 5001
+
+ENV CONFIGURATION=$CONFIGURATION
 ENV HTTPSTRESS_ARGS='-maxExecutionTime 30 -displayInterval 60'
-CMD dotnet run -c Release -- $HTTPSTRESS_ARGS
+CMD dotnet run --no-build -c $CONFIGURATION -- $HTTPSTRESS_ARGS
index 5c4d40a..0922c86 100644 (file)
@@ -36,7 +36,7 @@ public static class Program
     {
         var cmd = new RootCommand();
         cmd.AddOption(new Option("-n", "Max number of requests to make concurrently.") { Argument = new Argument<int>("numWorkers", Environment.ProcessorCount) });
-        cmd.AddOption(new Option("-serverUri", "Stress suite server uri.") { Argument = new Argument<Uri>("serverUri", new Uri("https://localhost:5001")) });
+        cmd.AddOption(new Option("-serverUri", "Stress suite server uri.") { Argument = new Argument<string>("serverUri", "https://localhost:5001") });
         cmd.AddOption(new Option("-runMode", "Stress suite execution mode. Defaults to Both.") { Argument = new Argument<RunMode>("runMode", RunMode.both) });
         cmd.AddOption(new Option("-maxExecutionTime", "Maximum stress execution time, in minutes. Defaults to infinity.") { Argument = new Argument<double?>("minutes", null) });
         cmd.AddOption(new Option("-maxContentLength", "Max content length for request and response bodies.") { Argument = new Argument<int>("numBytes", 1000) });
@@ -78,7 +78,7 @@ public static class Program
         config = new Configuration()
         {
             RunMode = cmdline.ValueForOption<RunMode>("-runMode"),
-            ServerUri = cmdline.ValueForOption<Uri>("-serverUri"),
+            ServerUri = cmdline.ValueForOption<string>("-serverUri"),
             ListOperations = cmdline.ValueForOption<bool>("-listOps"),
 
             HttpVersion = cmdline.ValueForOption<Version>("-http"),
@@ -124,7 +124,7 @@ public static class Program
             return ExitCode.CliError;
         }
 
-        if (!config.ServerUri.Scheme.StartsWith("http"))
+        if (!config.ServerUri.StartsWith("http"))
         {
             Console.Error.WriteLine("Invalid server uri");
             return ExitCode.CliError;
index 9f21aca..a7030de 100644 (file)
@@ -21,6 +21,7 @@ namespace HttpStress
         private const string UNENCRYPTED_HTTP2_ENV_VAR = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2UNENCRYPTEDSUPPORT";
 
         private readonly (string name, Func<RequestContext, Task> operation)[] _clientOperations;
+        private readonly Uri _baseAddress;
         private readonly Configuration _config;
         private readonly StressResultAggregator _aggregator;
         private readonly Stopwatch _stopwatch = new Stopwatch();
@@ -33,6 +34,7 @@ namespace HttpStress
         {
             _clientOperations = clientOperations;
             _config = configuration;
+            _baseAddress = new Uri(configuration.ServerUri);
             _aggregator = new StressResultAggregator(clientOperations);
         }
 
@@ -80,7 +82,7 @@ namespace HttpStress
 
         private async Task StartCore()
         {
-            if (_config.ServerUri.Scheme == "http")
+            if (_baseAddress.Scheme == "http")
             {
                 Environment.SetEnvironmentVariable(UNENCRYPTED_HTTP2_ENV_VAR, "1");
             }
@@ -107,7 +109,34 @@ namespace HttpStress
                 }
             }
 
-            using var client = new HttpClient(CreateHttpHandler()) { BaseAddress = _config.ServerUri, Timeout = _config.DefaultTimeout };
+            HttpClient CreateHttpClient() => 
+                new HttpClient(CreateHttpHandler()) 
+                { 
+                    BaseAddress = _baseAddress,
+                    Timeout = _config.DefaultTimeout,
+                    DefaultRequestVersion = _config.HttpVersion,
+                };
+
+            using HttpClient client = CreateHttpClient();
+
+            // Before starting the full-blown test, make sure can communicate with the server
+            // Needed for scenaria where we're deploying server & client in separate containers, simultaneously.
+            await SendTestRequestToServer(maxRetries: 10);
+
+            // Spin up a thread dedicated to outputting stats for each defined interval
+            new Thread(() =>
+            {
+                while (!_cts.IsCancellationRequested)
+                {
+                    Thread.Sleep(_config.DisplayInterval);
+                    lock (Console.Out) { _aggregator.PrintCurrentResults(_stopwatch.Elapsed); }
+                }
+            })
+            { IsBackground = true }.Start();
+
+            // Start N workers, each of which sits in a loop making requests.
+            Task[] tasks = Enumerable.Range(0, _config.ConcurrentRequests).Select(RunWorker).ToArray();
+            await Task.WhenAll(tasks);
 
             async Task RunWorker(int taskNum)
             {
@@ -147,21 +176,24 @@ namespace HttpStress
                     return ((int)rol5 + h1) ^ h2;
                 }
             }
-
-            // Spin up a thread dedicated to outputting stats for each defined interval
-            new Thread(() =>
+            
+            async Task SendTestRequestToServer(int maxRetries)
             {
-                while (!_cts.IsCancellationRequested)
+                using HttpClient client = CreateHttpClient();
+                for (int remainingRetries = maxRetries; ; remainingRetries--)
                 {
-                    Thread.Sleep(_config.DisplayInterval);
-                    lock (Console.Out) { _aggregator.PrintCurrentResults(_stopwatch.Elapsed); }
+                    try
+                    {
+                        await client.GetAsync("/");
+                        break;
+                    }
+                    catch (HttpRequestException) when (remainingRetries > 0)
+                    {
+                        Console.WriteLine($"Stress client could not connect to host {_baseAddress}, {remainingRetries} attempts remaining");
+                        await Task.Delay(millisecondsDelay: 1000);
+                    }
                 }
-            })
-            { IsBackground = true }.Start();
-
-            // Start N workers, each of which sits in a loop making requests.
-            Task[] tasks = Enumerable.Range(0, _config.ConcurrentRequests).Select(RunWorker).ToArray();
-            await Task.WhenAll(tasks);
+            }
         }
 
         /// <summary>Aggregate view of a particular stress failure type</summary>
index f81a71e..a722b44 100644 (file)
@@ -9,6 +9,7 @@ using System.Diagnostics.Tracing;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Text.RegularExpressions;
 using System.Runtime.InteropServices;
 using System.Security.Cryptography;
 using System.Security.Cryptography.X509Certificates;
@@ -37,11 +38,12 @@ namespace HttpStress
         private EventListener? _eventListener;
         private readonly IWebHost _webHost;
 
-        public Uri ServerUri { get; }
+        public string ServerUri { get; }
 
         public StressServer(Configuration configuration)
         {
             ServerUri = configuration.ServerUri;
+            (string scheme, string hostname, int port) = ParseServerUri(configuration.ServerUri);
             IWebHostBuilder host = WebHost.CreateDefaultBuilder();
 
             if (configuration.UseHttpSys)
@@ -54,7 +56,7 @@ namespace HttpStress
                 // 3. Register the cert, e.g. netsh http add sslcert ipport=[::1]:5001 certhash=THUMBPRINTFROMABOVE appid="{some-guid}"
                 host = host.UseHttpSys(hso =>
                 {
-                    hso.UrlPrefixes.Add(ServerUri.ToString());
+                    hso.UrlPrefixes.Add(ServerUri);
                     hso.Authentication.Schemes = Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes.None;
                     hso.Authentication.AllowAnonymous = true;
                     hso.MaxConnections = null;
@@ -76,16 +78,27 @@ namespace HttpStress
                     ko.Limits.Http2.InitialConnectionWindowSize = configuration.ServerInitialConnectionWindowSize ?? ko.Limits.Http2.InitialConnectionWindowSize;
                     ko.Limits.Http2.MaxRequestHeaderFieldSize = configuration.ServerMaxRequestHeaderFieldSize ?? ko.Limits.Http2.MaxRequestHeaderFieldSize;
 
-                    IPAddress iPAddress = Dns.GetHostAddresses(configuration.ServerUri.Host).First();
+                    switch (hostname)
+                    {
+                        case "+":
+                        case "*":
+                            ko.ListenAnyIP(port, ConfigureListenOptions);
+                            break;
+                        default:
+                            IPAddress iPAddress = Dns.GetHostAddresses(hostname).First();
+                            ko.Listen(iPAddress, port, ConfigureListenOptions);
+                            break;
+
+                    }
 
-                    ko.Listen(iPAddress, configuration.ServerUri.Port, listenOptions =>
+                    void ConfigureListenOptions(ListenOptions listenOptions)
                     {
-                        if (configuration.ServerUri.Scheme == "https")
+                        if (scheme == "https")
                         {
                             // Create self-signed cert for server.
                             using (RSA rsa = RSA.Create())
                             {
-                                var certReq = new CertificateRequest($"CN={ServerUri.Host}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+                                var certReq = new CertificateRequest("CN=contoso.com", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
                                 certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
                                 certReq.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
                                 certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));
@@ -104,7 +117,7 @@ namespace HttpStress
                                 HttpProtocols.Http2 :
                                 HttpProtocols.Http1 ;
                         }
-                    });
+                    }
                 });
             };
 
@@ -309,6 +322,28 @@ namespace HttpStress
             }
         }
 
+        private static (string scheme, string hostname, int port) ParseServerUri(string serverUri)
+        {
+            try
+            {
+                var uri = new Uri(serverUri);
+                return (uri.Scheme, uri.Host, uri.Port);
+            } 
+            catch (UriFormatException)
+            {
+                // Simple uri parser: used to parse values valid in Kestrel
+                // but not representable by the System.Uri class, e.g. https://+:5050
+                Match m = Regex.Match(serverUri, "^(?<scheme>https?)://(?<host>[^:/]+)(:(?<port>[0-9]+))?");
+
+                if (!m.Success) throw;
+
+                string scheme = m.Groups["scheme"].Value;
+                string hostname = m.Groups["host"].Value;
+                int port = m.Groups["port"].Success ? int.Parse(m.Groups["port"].Value) : (scheme == "https" ? 443 : 80);
+                return (scheme, hostname, port);
+            }
+        }
+
         /// <summary>EventListener that dumps HTTP events out to either the console or a stream writer.</summary>
         private sealed class HttpEventListener : EventListener
         {
@@ -396,6 +431,5 @@ namespace HttpStress
 
             return true;
         }
-
     }
 }
index be62e71..4039e02 100644 (file)
@@ -7,8 +7,8 @@ FROM $BUILD_BASE_IMAGE as corefxbuild
 WORKDIR /repo
 COPY . .
 
-ARG CONFIG=Release
-RUN ./build.sh -c $CONFIG
+ARG CONFIGURATION=Release
+RUN ./build.sh -c $CONFIGURATION
 
 FROM $SDK_BASE_IMAGE as target
 
@@ -16,7 +16,7 @@ ARG TESTHOST_LOCATION=/repo/artifacts/bin/testhost
 ARG TFM=netcoreapp
 ARG OS=Linux
 ARG ARCH=x64
-ARG CONFIG=Release
+ARG CONFIGURATION=Release
 
 ARG COREFX_SHARED_FRAMEWORK_NAME=Microsoft.NETCore.App
 ARG SOURCE_COREFX_VERSION=5.0.0
@@ -24,5 +24,5 @@ ARG TARGET_SHARED_FRAMEWORK=/usr/share/dotnet/shared
 ARG TARGET_COREFX_VERSION=3.0.0
 
 COPY --from=corefxbuild \
-    $TESTHOST_LOCATION/$TFM-$OS-$CONFIG-$ARCH/shared/$COREFX_SHARED_FRAMEWORK_NAME/$SOURCE_COREFX_VERSION/* \
+    $TESTHOST_LOCATION/$TFM-$OS-$CONFIGURATION-$ARCH/shared/$COREFX_SHARED_FRAMEWORK_NAME/$SOURCE_COREFX_VERSION/* \
     $TARGET_SHARED_FRAMEWORK/$COREFX_SHARED_FRAMEWORK_NAME/$TARGET_COREFX_VERSION/
diff --git a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/docker-compose.yml b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/docker-compose.yml
new file mode 100644 (file)
index 0000000..6393a13
--- /dev/null
@@ -0,0 +1,14 @@
+version: '3'
+services:
+  client:
+    image: httpstress
+    links:
+      - server
+    environment:
+      - HTTPSTRESS_ARGS=-runMode client -serverUri https://server:5001 ${HTTPSTRESS_ARGS}
+  server:
+    image: httpstress
+    ports:
+      - "5001:5001"
+    environment:
+      - HTTPSTRESS_ARGS=-runMode server -serverUri https://+:5001