Httpstress: add max execution time argument (dotnet/corefx#41643)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 9 Oct 2019 10:53:42 +0000 (11:53 +0100)
committerGitHub <noreply@github.com>
Wed, 9 Oct 2019 10:53:42 +0000 (11:53 +0100)
HttpStress: add max execution time arg

Adds -maxExecutionTime cli argument.
Enables nullable annotations.

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

src/libraries/System.Net.Http/tests/StressTests/HttpStress/ChecksumHelpers.cs
src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs
src/libraries/System.Net.Http/tests/StressTests/HttpStress/Configuration.cs
src/libraries/System.Net.Http/tests/StressTests/HttpStress/HttpStress.csproj
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

index e99879c..aecf288 100644 (file)
@@ -57,7 +57,7 @@ namespace HttpStress
             return c;
         }
 
-        public static ulong update_crc(ulong crc, string text, Encoding encoding = null)
+        public static ulong update_crc(ulong crc, string text, Encoding? encoding = null)
         {
             encoding = encoding ?? Encoding.ASCII;
             byte[] bytes = encoding.GetBytes(text);
@@ -65,9 +65,9 @@ namespace HttpStress
         }
 
         public static ulong CalculateCRC(byte[] buf) => update_crc(InitialCrc, buf, buf.Length) ^ InitialCrc;
-        public static ulong CalculateCRC(string text, Encoding encoding = null) => update_crc(InitialCrc, text, encoding) ^ InitialCrc;
+        public static ulong CalculateCRC(string text, Encoding? encoding = null) => update_crc(InitialCrc, text, encoding) ^ InitialCrc;
 
-        public static ulong CalculateHeaderCrc<T>(IEnumerable<(string name, T)> headers, Encoding encoding = null) where T : IEnumerable<string>
+        public static ulong CalculateHeaderCrc<T>(IEnumerable<(string name, T)> headers, Encoding? encoding = null) where T : IEnumerable<string>
         {
             ulong checksum = InitialCrc;
 
@@ -83,4 +83,4 @@ namespace HttpStress
             return checksum ^ InitialCrc;
         }
     }
-}
\ No newline at end of file
+}
index 0724dc1..61fdd90 100644 (file)
@@ -300,14 +300,14 @@ namespace HttpStress
                                 return;
                             }
 
-                            string name = e.InnerException?.GetType().Name;
+                            string? name = e.InnerException?.GetType().Name;
                             switch (name)
                             {
                                 case "Http2ProtocolException":
                                 case "Http2ConnectionException":
                                 case "Http2StreamException":
-                                    if (e.InnerException.Message.Contains("INTERNAL_ERROR") || // UseKestrel (https://github.com/aspnet/AspNetCore/issues/12256)
-                                        e.InnerException.Message.Contains("CANCEL")) // UseHttpSys
+                                    if ((e.InnerException?.Message?.Contains("INTERNAL_ERROR") ?? false) || // UseKestrel (https://github.com/aspnet/AspNetCore/issues/12256)
+                                        (e.InnerException?.Message?.Contains("CANCEL") ?? false)) // UseHttpSys
                                     {
                                         return;
                                     }
@@ -482,7 +482,7 @@ namespace HttpStress
             }
         }
 
-        private static void ValidateContent(string expectedContent, string actualContent, string details = null)
+        private static void ValidateContent(string expectedContent, string actualContent, string? details = null)
         {
             if (actualContent != expectedContent)
             {
index 178d4cd..b27bfc7 100644 (file)
@@ -22,11 +22,11 @@ namespace HttpStress
 
     public class Configuration
     {
-        public Uri ServerUri { get; set; }
+        public Uri ServerUri { get; set; } = new Uri("http://placeholder");
         public RunMode RunMode { get; set; }
         public bool ListOperations { get; set; }
 
-        public Version HttpVersion { get; set; }
+        public Version HttpVersion { get; set; } = new Version();
         public bool UseWinHttpHandler { get; set; }
         public int ConcurrentRequests { get; set; }
         public int RandomSeed { get; set; }
@@ -35,15 +35,16 @@ namespace HttpStress
         public int MaxRequestHeaderCount { get; set; }
         public int MaxRequestHeaderTotalSize { get; set; }
         public int MaxParameters { get; set; }
-        public int[] OpIndices { get; set; }
-        public int[] ExcludedOpIndices { get; set; }
+        public int[]? OpIndices { get; set; }
+        public int[]? ExcludedOpIndices { get; set; }
         public TimeSpan DisplayInterval { get; set; }
         public TimeSpan DefaultTimeout { get; set; }
         public TimeSpan? ConnectionLifetime { get; set; }
+        public TimeSpan? MaximumExecutionTime { get; set; }
         public double CancellationProbability { get; set; }
 
         public bool UseHttpSys { get; set; }
-        public string LogPath { get; set; }
+        public string? LogPath { get; set; }
         public bool LogAspNet { get; set; }
         public int? ServerMaxConcurrentStreams { get; set; }
         public int? ServerMaxFrameSize { get; set; }
index 9eacb86..40a4a32 100644 (file)
@@ -4,6 +4,7 @@
     <OutputType>Exe</OutputType>
     <TargetFramework>netcoreapp3.0</TargetFramework>
     <LangVersion>preview</LangVersion>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
 
   <ItemGroup>
index c8ab594..595f5cf 100644 (file)
@@ -7,6 +7,7 @@ using System;
 using System.Collections.Generic;
 using System.CommandLine;
 using System.IO;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Net;
@@ -18,30 +19,33 @@ using HttpStress;
 public static class Program
 {
 
-    public static async Task Main(string[] args)
+    public enum ExitCode { Success = 0, StressError = 1, CliError = 2 };
+
+    public static async Task<int> Main(string[] args)
     {
-        if (!TryParseCli(args, out Configuration config))
+        if (!TryParseCli(args, out Configuration? config))
         {
-            return;
+            return (int) ExitCode.CliError;
         }
 
-        await Run(config);
+        return (int) await Run(config);
     }
 
-    private static bool TryParseCli(string[] args, out Configuration config)
+    private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configuration? config)
     {
         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("-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) });
         cmd.AddOption(new Option("-maxRequestUriSize", "Max query string length support by the server.") { Argument = new Argument<int>("numChars", 5000) });
         cmd.AddOption(new Option("-maxRequestHeaderCount", "Maximum number of headers to place in request") { Argument = new Argument<int>("numHeaders", 90) });
         cmd.AddOption(new Option("-maxRequestHeaderTotalSize", "Max request header total size.") { Argument = new Argument<int>("numBytes", 1000) });
         cmd.AddOption(new Option("-http", "HTTP version (1.1 or 2.0)") { Argument = new Argument<Version>("version", HttpVersion.Version20) });
         cmd.AddOption(new Option("-connectionLifetime", "Max connection lifetime length (milliseconds).") { Argument = new Argument<int?>("connectionLifetime", null) });
-        cmd.AddOption(new Option("-ops", "Indices of the operations to use") { Argument = new Argument<int[]>("space-delimited indices", null) });
-        cmd.AddOption(new Option("-xops", "Indices of the operations to exclude") { Argument = new Argument<int[]>("space-delimited indices", null) });
+        cmd.AddOption(new Option("-ops", "Indices of the operations to use") { Argument = new Argument<int[]?>("space-delimited indices", null) });
+        cmd.AddOption(new Option("-xops", "Indices of the operations to exclude") { Argument = new Argument<int[]?>("space-delimited indices", null) });
         cmd.AddOption(new Option("-trace", "Enable Microsoft-System-Net-Http tracing.") { Argument = new Argument<string>("\"console\" or path") });
         cmd.AddOption(new Option("-aspnetlog", "Enable ASP.NET warning and error logging.") { Argument = new Argument<bool>("enable", false) });
         cmd.AddOption(new Option("-listOps", "List available options.") { Argument = new Argument<bool>("enable", false) });
@@ -91,6 +95,7 @@ public static class Program
             DefaultTimeout = TimeSpan.FromSeconds(cmdline.ValueForOption<int>("-clientTimeout")),
             ConnectionLifetime = cmdline.ValueForOption<double?>("-connectionLifetime").Select(TimeSpan.FromMilliseconds),
             CancellationProbability = Math.Max(0, Math.Min(1, cmdline.ValueForOption<double>("-cancelRate"))),
+            MaximumExecutionTime = cmdline.ValueForOption<double?>("-maxExecutionTime").Select(TimeSpan.FromMinutes),
 
             UseHttpSys = cmdline.ValueForOption<bool>("-httpSys"),
             LogAspNet = cmdline.ValueForOption<bool>("-aspnetlog"),
@@ -104,7 +109,7 @@ public static class Program
         return true;
     }
 
-    private static async Task Run(Configuration config)
+    private static async Task<ExitCode> Run(Configuration config)
     {
         (string name, Func<RequestContext, Task> op)[] clientOperations =
             ClientOperations.Operations
@@ -115,13 +120,13 @@ public static class Program
         if ((config.RunMode & RunMode.both) == 0)
         {
             Console.Error.WriteLine("Must specify a valid run mode");
-            return;
+            return ExitCode.CliError;
         }
 
         if (!config.ServerUri.Scheme.StartsWith("http"))
         {
             Console.Error.WriteLine("Invalid server uri");
-            return;
+            return ExitCode.CliError;
         }
 
         if (config.ListOperations)
@@ -130,7 +135,7 @@ public static class Program
             {
                 Console.WriteLine(clientOperations[i].name);
             }
-            return;
+            return ExitCode.Success;
         }
 
         // derive client operations based on arguments
@@ -165,7 +170,7 @@ public static class Program
         Console.WriteLine();
 
 
-        StressServer server = null;
+        StressServer? server = null;
         if (config.RunMode.HasFlag(RunMode.server))
         {
             // Start the Kestrel web server in-proc.
@@ -174,7 +179,7 @@ public static class Program
             Console.WriteLine($"Server started at {server.ServerUri}");
         }
 
-        StressClient client = null;
+        StressClient? client = null;
         if (config.RunMode.HasFlag(RunMode.client))
         {
             // Start the client.
@@ -184,22 +189,32 @@ public static class Program
             client.Start();
         }
 
-        await AwaitCancelKeyPress();
+        await WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(config.MaximumExecutionTime);
 
         client?.Stop();
         client?.PrintFinalReport();
+
+        // return nonzero status code if there are stress errors
+        return client?.TotalErrorCount == 0 ? ExitCode.Success : ExitCode.StressError;
     }
 
-    private static async Task AwaitCancelKeyPress()
+    private static async Task WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(TimeSpan? maxExecutionTime = null)
     {
         var tcs = new TaskCompletionSource<bool>();
         Console.CancelKeyPress += (sender,args) => { Console.Error.WriteLine("Keyboard interrupt"); args.Cancel = true; tcs.TrySetResult(false); };
+        if (maxExecutionTime.HasValue)
+        {
+            Console.WriteLine($"Running for a total of {maxExecutionTime.Value.TotalMinutes:0.##} minutes");
+            var cts = new System.Threading.CancellationTokenSource(delay: maxExecutionTime.Value);
+            cts.Token.Register(() => { Console.WriteLine("Max execution time elapsed"); tcs.TrySetResult(false); });
+        }
+
         await tcs.Task;
     }
 
     private static S? Select<T, S>(this T? value, Func<T, S> mapper) where T : struct where S : struct
     {
-        return value != null ? new S?(mapper(value.Value)) : null;
+        return value is null ? null : new S?(mapper(value.Value));
     }
 
     private static string GetSysNetHttpAssemblyInfo()
index ff2d2ca..9f21aca 100644 (file)
@@ -25,7 +25,9 @@ namespace HttpStress
         private readonly StressResultAggregator _aggregator;
         private readonly Stopwatch _stopwatch = new Stopwatch();
         private readonly CancellationTokenSource _cts = new CancellationTokenSource();
-        private Task _clientTask;
+        private Task? _clientTask;
+
+        public long TotalErrorCount => _aggregator.TotalErrorCount;
 
         public StressClient((string name, Func<RequestContext, Task> operation)[] clientOperations, Configuration configuration)
         {
@@ -191,6 +193,8 @@ namespace HttpStress
             private readonly ConcurrentDictionary<(Type exception, string message, string callSite)[], StressFailureType> _failureTypes;
             private readonly ConcurrentBag<double> _latencies = new ConcurrentBag<double>();
 
+            public long TotalErrorCount => _failures.Sum();
+
             public StressResultAggregator((string name, Func<RequestContext, Task>)[] operations)
             {
                 _operationNames = operations.Select(x => x.name).ToArray();
@@ -237,9 +241,7 @@ namespace HttpStress
 
                     lock (failureType)
                     {
-                        List<DateTime> timestamps;
-
-                        if(!failureType.Failures.TryGetValue(operationIndex, out timestamps))
+                        if(!failureType.Failures.TryGetValue(operationIndex, out List<DateTime>? timestamps))
                         {
                             timestamps = new List<DateTime>();
                             failureType.Failures.Add(operationIndex, timestamps);
@@ -252,10 +254,10 @@ namespace HttpStress
                     {
                         var acc = new List<(Type exception, string message, string callSite)>();
 
-                        while (exn != null)
+                        for (Exception? e = exn; e != null; )
                         {
-                            acc.Add((exn.GetType(), exn.Message ?? "", new StackTrace(exn, true).GetFrame(0)?.ToString() ?? ""));
-                            exn = exn.InnerException;
+                            acc.Add((e.GetType(), e.Message ?? "", new StackTrace(e, true).GetFrame(0)?.ToString() ?? ""));
+                            e = e.InnerException;
                         }
 
                         return acc.ToArray();
index 7a219a4..6acec37 100644 (file)
@@ -34,7 +34,7 @@ namespace HttpStress
         // Header indicating expected response content length to be returned by the server
         public const string ExpectedResponseContentLength = "Expected-Response-Content-Length";
 
-        private EventListener _eventListener;
+        private EventListener? _eventListener;
         private readonly IWebHost _webHost;
 
         public Uri ServerUri { get; }
@@ -312,9 +312,9 @@ namespace HttpStress
         /// <summary>EventListener that dumps HTTP events out to either the console or a stream writer.</summary>
         private sealed class HttpEventListener : EventListener
         {
-            private readonly StreamWriter _writer;
+            private readonly StreamWriter? _writer;
 
-            public HttpEventListener(StreamWriter writer = null) => _writer = writer;
+            public HttpEventListener(StreamWriter? writer = null) => _writer = writer;
 
             protected override void OnEventSourceCreated(EventSource eventSource)
             {
@@ -329,11 +329,11 @@ namespace HttpStress
                     if (_writer != null)
                     {
                         var sb = new StringBuilder().Append($"[{eventData.EventName}] ");
-                        for (int i = 0; i < eventData.Payload.Count; i++)
+                        for (int i = 0; i < eventData.Payload?.Count; i++)
                         {
                             if (i > 0)
                                 sb.Append(", ");
-                            sb.Append(eventData.PayloadNames[i]).Append(": ").Append(eventData.Payload[i]);
+                            sb.Append(eventData.PayloadNames?[i]).Append(": ").Append(eventData.Payload[i]);
                         }
                         _writer.WriteLine(sb);
                     }
@@ -342,12 +342,12 @@ namespace HttpStress
                         Console.ForegroundColor = ConsoleColor.DarkYellow;
                         Console.Write($"[{eventData.EventName}] ");
                         Console.ResetColor();
-                        for (int i = 0; i < eventData.Payload.Count; i++)
+                        for (int i = 0; i < eventData.Payload?.Count; i++)
                         {
                             if (i > 0)
                                 Console.Write(", ");
                             Console.ForegroundColor = ConsoleColor.DarkGray;
-                            Console.Write(eventData.PayloadNames[i] + ": ");
+                            Console.Write(eventData.PayloadNames?[i] + ": ");
                             Console.ResetColor();
                             Console.Write(eventData.Payload[i]);
                         }