HttpStress: Randomize cancellation of requests
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Mon, 15 Jul 2019 16:40:50 +0000 (17:40 +0100)
committerEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Mon, 15 Jul 2019 22:44:45 +0000 (23:44 +0100)
Commit migrated from https://github.com/dotnet/corefx/commit/c4167ed11e1444433a33cc5d44a4aa33e0ffd651

src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs

index cd19c42..d46f818 100644 (file)
@@ -45,7 +45,9 @@ public class Program
         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) });
         cmd.AddOption(new Option("-seed", "Seed for generating pseudo-random parameters for a given -n argument.") { Argument = new Argument<int?>("seed", null)});
+        cmd.AddOption(new Option("-p", "Max number of query parameters for a request.") { Argument = new Argument<int>("queryParameters", 1) });
         cmd.AddOption(new Option("-numParameters", "Max number of query parameters or form fields for a request.") { Argument = new Argument<int>("queryParameters", 1) });
+        cmd.AddOption(new Option("-cancelRate", "Number between 0 and 1 indicating rate of client-side request cancellation attempts. Defaults to 0.1.") { Argument = new Argument<double>("probability", 0.1) });
 
         ParseResult cmdline = cmd.Parse(args);
         if (cmdline.Errors.Count > 0)
@@ -59,19 +61,20 @@ public class Program
             return;
         }
 
-        Run(concurrentRequests  : cmdline.ValueForOption<int>("-n"),
-            maxContentLength    : cmdline.ValueForOption<int>("-maxContentLength"),
-            httpVersions        : cmdline.ValueForOption<Version[]>("-http"),
-            connectionLifetime  : cmdline.ValueForOption<int?>("-connectionLifetime"),
-            opIndices           : cmdline.ValueForOption<int[]>("-ops"),
-            logPath             : cmdline.HasOption("-trace") ? cmdline.ValueForOption<string>("-trace") : null,
-            aspnetLog           : cmdline.ValueForOption<bool>("-aspnetlog"),
-            listOps             : cmdline.ValueForOption<bool>("-listOps"),
-            seed                : cmdline.ValueForOption<int?>("-seed") ?? new Random().Next(),
-            numParameters       : cmdline.ValueForOption<int>("-numParameters"));
+        Run(concurrentRequests      : cmdline.ValueForOption<int>("-n"),
+            maxContentLength        : cmdline.ValueForOption<int>("-maxContentLength"),
+            httpVersions            : cmdline.ValueForOption<Version[]>("-http"),
+            connectionLifetime      : cmdline.ValueForOption<int?>("-connectionLifetime"),
+            opIndices               : cmdline.ValueForOption<int[]>("-ops"),
+            logPath                 : cmdline.HasOption("-trace") ? cmdline.ValueForOption<string>("-trace") : null,
+            aspnetLog               : cmdline.ValueForOption<bool>("-aspnetlog"),
+            listOps                 : cmdline.ValueForOption<bool>("-listOps"),
+            seed                    : cmdline.ValueForOption<int?>("-seed") ?? new Random().Next(),
+            numParameters           : cmdline.ValueForOption<int>("-numParameters"),
+            cancellationProbability : Math.Max(0, Math.Min(1, cmdline.ValueForOption<double>("-cancelRate"))));
     }
 
-    private static void Run(int concurrentRequests, int maxContentLength, Version[] httpVersions, int? connectionLifetime, int[] opIndices, string logPath, bool aspnetLog, bool listOps, int seed, int numParameters)
+    private static void Run(int concurrentRequests, int maxContentLength, Version[] httpVersions, int? connectionLifetime, int[] opIndices, string logPath, bool aspnetLog, bool listOps, int seed, int numParameters, double cancellationProbability)
     {
         // Handle command-line arguments.
         EventListener listener =
@@ -121,14 +124,14 @@ public class Program
         // and the delegate to invoke for it, provided with the HttpClient instance on which to make the call and
         // returning asynchronously the retrieved response string from the server.  Individual operations can be
         // commented out from here to turn them off, or additional ones can be added.
-        var clientOperations = new (string, Func<ClientContext, Task>)[]
+        var clientOperations = new (string, Func<RequestContext, Task>)[]
         {
             ("GET",
             async ctx =>
             {
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
                 using (var req = new HttpRequestMessage(HttpMethod.Get, serverUri) { Version = httpVersion })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     ValidateContent(contentSource, await m.Content.ReadAsStringAsync());
@@ -140,7 +143,7 @@ public class Program
             {
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
                 using (var req = new HttpRequestMessage(HttpMethod.Get, serverUri + "/slow") { Version = httpVersion })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     using (Stream s = await m.Content.ReadAsStreamAsync())
@@ -155,7 +158,7 @@ public class Program
             {
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
                 using (var req = new HttpRequestMessage(HttpMethod.Get, serverUri + "/headers") { Version = httpVersion })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     ValidateContent(contentSource, await m.Content.ReadAsStringAsync());
@@ -168,35 +171,13 @@ public class Program
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
                 (string query, string expected) variables = GetGetQueryParameters(contentSource, ctx, numParameters);
                 using (var req = new HttpRequestMessage(HttpMethod.Get, serverUri + "/variables" + variables.query) { Version = httpVersion })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     ValidateContent(variables.expected, await m.Content.ReadAsStringAsync());
                 }
             }),
 
-            ("GET Cancellation",
-            async ctx =>
-            {
-                Version httpVersion = ctx.GetRandomVersion(httpVersions);
-                using (var req = new HttpRequestMessage(HttpMethod.Get, serverUri) { Version = httpVersion })
-                {
-                    var cts = new CancellationTokenSource();
-                    Task<HttpResponseMessage> t = ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cts.Token);
-                    await Task.Delay(1);
-                    cts.Cancel();
-                    try
-                    {
-                        using (HttpResponseMessage m = await t)
-                        {
-                            ValidateResponse(m, httpVersion);
-                            ValidateContent(contentSource, await m.Content.ReadAsStringAsync());
-                        }
-                    }
-                    catch (OperationCanceledException) { }
-                }
-            }),
-
             ("GET Aborted",
             async ctx =>
             {
@@ -205,7 +186,7 @@ public class Program
                 {
                     using (var req = new HttpRequestMessage(HttpMethod.Get, serverUri + "/abort") { Version = httpVersion })
                     {
-                        await ctx.HttpClient.SendAsync(req);
+                        await ctx.HttpClient.SendAsync(req, ctx.CancellationToken);
                     }
                     throw new Exception("Completed unexpectedly");
                 }
@@ -248,7 +229,7 @@ public class Program
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
 
                 using (var req = new HttpRequestMessage(HttpMethod.Post, serverUri) { Version = httpVersion, Content = new StringDuplexContent(content) })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     ValidateContent(content, await m.Content.ReadAsStringAsync());;
@@ -276,7 +257,7 @@ public class Program
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
 
                 using (var req = new HttpRequestMessage(HttpMethod.Post, serverUri + "/duplex") { Version = httpVersion, Content = new StringDuplexContent(content) })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     ValidateContent(content, await m.Content.ReadAsStringAsync());
@@ -290,7 +271,7 @@ public class Program
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
 
                 using (var req = new HttpRequestMessage(HttpMethod.Post, serverUri + "/duplexSlow") { Version = httpVersion, Content = new ByteAtATimeNoLengthContent(Encoding.ASCII.GetBytes(content)) })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     ValidateContent(content, await m.Content.ReadAsStringAsync());
@@ -306,7 +287,7 @@ public class Program
                 using (var req = new HttpRequestMessage(HttpMethod.Post, serverUri) { Version = httpVersion, Content = new StringContent(content) })
                 {
                     req.Headers.ExpectContinue = true;
-                    using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
+                    using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ctx.CancellationToken))
                     {
                         ValidateResponse(m, httpVersion);
                         ValidateContent(content, await m.Content.ReadAsStringAsync());
@@ -314,37 +295,12 @@ public class Program
                 }
             }),
 
-            ("POST Cancellation",
-            async ctx =>
-            {
-                string content = ctx.GetRandomSubstring(contentSource);
-                Version httpVersion = ctx.GetRandomVersion(httpVersions);
-
-                using (var req = new HttpRequestMessage(HttpMethod.Post, serverUri) { Version = httpVersion, Content = new StringContent(content) })
-                {
-                    var cts = new CancellationTokenSource();
-                    req.Content = new CancelableContent(cts.Token);
-                    Task<HttpResponseMessage> t = ctx.HttpClient.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cts.Token);
-                    await Task.Delay(1);
-                    cts.Cancel();
-                    try
-                    {
-                        using (HttpResponseMessage m = await t)
-                        {
-                            ValidateResponse(m, httpVersion);
-                            ValidateContent(content, await m.Content.ReadAsStringAsync());
-                        }
-                    }
-                    catch (OperationCanceledException) { }
-                }
-            }),
-
             ("HEAD",
             async ctx =>
             {
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
                 using (var req = new HttpRequestMessage(HttpMethod.Head, serverUri) { Version = httpVersion })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     if (m.Content.Headers.ContentLength != maxContentLength)
@@ -363,7 +319,7 @@ public class Program
                 Version httpVersion = ctx.GetRandomVersion(httpVersions);
 
                 using (var req = new HttpRequestMessage(HttpMethod.Put, serverUri) { Version = httpVersion, Content = new StringContent(content) })
-                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req))
+                using (HttpResponseMessage m = await ctx.HttpClient.SendAsync(req, ctx.CancellationToken))
                 {
                     ValidateResponse(m, httpVersion);
                     string r = await m.Content.ReadAsStringAsync();
@@ -396,6 +352,7 @@ public class Program
         Console.WriteLine("        Lifetime: " + (connectionLifetime.HasValue ? $"{connectionLifetime}ms" : "(infinite)"));
         Console.WriteLine("      Operations: " + string.Join(", ", clientOperations.Select(o => o.Item1)));
         Console.WriteLine("     Random Seed: " + seed);
+        Console.WriteLine("    Cancellation: " + 100 * cancellationProbability + "%");
         Console.WriteLine("Query Parameters: " + numParameters);
         Console.WriteLine();
 
@@ -539,7 +496,7 @@ public class Program
         {
             // Track all successes and failures
             long total = 0;
-            long[] success = new long[clientOperations.Length], fail = new long[clientOperations.Length];
+            long[] success = new long[clientOperations.Length], cancel = new long[clientOperations.Length], fail = new long[clientOperations.Length];
             long reuseAddressFailure = 0;
 
             void Increment(ref long counter)
@@ -575,8 +532,12 @@ public class Program
                             Console.ResetColor();
                             Console.ForegroundColor = ConsoleColor.Green;
                             Console.Write("Success: ");
-                            Console.ResetColor();
                             Console.Write(success[i].ToString("N0"));
+                            Console.ResetColor();
+                            Console.ForegroundColor = ConsoleColor.Yellow;
+                            Console.Write("\tCancelled: ");
+                            Console.Write(cancel[i].ToString("N0"));
+                            Console.ResetColor();
                             Console.ForegroundColor = ConsoleColor.DarkRed;
                             Console.Write("\tFail: ");
                             Console.ResetColor();
@@ -591,18 +552,38 @@ public class Program
             // Start N workers, each of which sits in a loop making requests.
             Task.WaitAll(Enumerable.Range(0, concurrentRequests).Select(taskNum => Task.Run(async () =>
             {
-                var clientContext = new ClientContext(client, taskNum: taskNum, seed: seed);
+                // Creates a System.Random instance that is specific to the current client job
+                // Generated using the global seed and the task index
+                Random CreateRandomInstance()
+                {
+                    // deterministic hashing copied from System.Runtime.Hashing
+                    int Combine(int h1, int h2)
+                    {
+                        uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
+                        return ((int)rol5 + h1) ^ h2;
+                    }
+
+                    return new Random(Seed: Combine(taskNum, seed));
+                }
+
+                var random = CreateRandomInstance();
 
                 for (long i = taskNum; ; i++)
                 {
                     long opIndex = i % clientOperations.Length;
-                    (string operation, Func<ClientContext, Task> func) = clientOperations[opIndex];
+                    (string operation, Func<RequestContext, Task> func) = clientOperations[opIndex];
+                    // request-specific context
+                    var requestContext = new RequestContext(client, random, taskNum, cancellationProbability);
                     try
                     {
-                        await func(clientContext);
+                        await func(requestContext);
 
                         Increment(ref success[opIndex]);
                     }
+                    catch (OperationCanceledException) when (requestContext.CancellationToken.IsCancellationRequested)
+                    {
+                        Increment(ref cancel[opIndex]);
+                    }
                     catch (Exception e)
                     {
                         Increment(ref fail[opIndex]);
@@ -631,7 +612,7 @@ public class Program
         GC.KeepAlive(listener);
     }
 
-    private static (string, string) GetGetQueryParameters(string contentSource, ClientContext clientContext, int numParameters)
+    private static (string, string) GetGetQueryParameters(string contentSource, RequestContext clientContext, int numParameters)
     {
         StringBuilder queryString = new StringBuilder();
         StringBuilder expectedString = new StringBuilder();
@@ -650,7 +631,7 @@ public class Program
         return (queryString.ToString(), expectedString.ToString());
     }
 
-    private static (string, MultipartContent) GetMultipartContent(string contentSource, ClientContext clientContext, int numFormFields)
+    private static (string, MultipartContent) GetMultipartContent(string contentSource, RequestContext clientContext, int numFormFields)
     {
         var multipartContent = new MultipartContent("prefix" + clientContext.GetRandomSubstring(contentSource), "test_boundary");
         StringBuilder sb = new StringBuilder();
@@ -673,28 +654,33 @@ public class Program
         return (sb.ToString(), multipartContent);
     }
 
-    /// <summary>Client context containing information pertaining to a single worker.</summary>
-    private sealed class ClientContext
+    /// <summary>Client context containing information pertaining to a single request.</summary>
+    private sealed class RequestContext
     {
         private readonly Random _random;
 
-        public ClientContext(HttpClient httpClient, int taskNum, int seed)
+        public RequestContext(HttpClient httpClient, Random random, int taskNum, double cancellationProbability)
         {
-            _random = new Random(Combine(seed, taskNum)); // derived from global seed and worker number
+            _random = random;
             TaskNum = taskNum;
             HttpClient = httpClient;
+            CancellationToken =
+                (GetRandomBoolean(cancellationProbability))
+                ? CreateCancellationTokenWithRandomizedCancellationDelay()
+                : CancellationToken.None;
 
-            // deterministic hashing copied from System.Runtime.Hashing
-            int Combine(int h1, int h2)
+            CancellationToken CreateCancellationTokenWithRandomizedCancellationDelay(int maxDelayMs = 5)
             {
-                uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
-                return ((int)rol5 + h1) ^ h2;
+                var delay = TimeSpan.FromMilliseconds(GetRandomInt(maxDelayMs));
+                return new CancellationTokenSource(delay).Token;
             }
         }
         public int TaskNum { get; }
 
         public HttpClient HttpClient { get; }
 
+        public CancellationToken CancellationToken { get; }
+
         public string GetRandomSubstring(string input)
         {
             int offset = _random.Next(0, input.Length);
@@ -702,6 +688,14 @@ public class Program
             return input.Substring(offset, length);
         }
 
+        public bool GetRandomBoolean(double probability = 0.5)
+        {
+            if (probability < 0 || probability > 1)
+                throw new ArgumentOutOfRangeException(nameof(probability));
+
+            return _random.NextDouble() < probability;
+        }
+
         public int GetRandomInt(int maxValue) => _random.Next(0, maxValue);
 
         public Version GetRandomVersion(Version[] versions) =>