From 32b8f50c85c1aa42f1cc4ad5454b118720c788a7 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 15 Jul 2019 17:40:50 +0100 Subject: [PATCH] HttpStress: Randomize cancellation of requests Commit migrated from https://github.com/dotnet/corefx/commit/c4167ed11e1444433a33cc5d44a4aa33e0ffd651 --- .../tests/StressTests/HttpStress/Program.cs | 164 ++++++++++----------- 1 file changed, 79 insertions(+), 85 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs index cd19c42..d46f818 100644 --- a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs +++ b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs @@ -45,7 +45,9 @@ public class Program cmd.AddOption(new Option("-aspnetlog", "Enable ASP.NET warning and error logging.") { Argument = new Argument("enable", false) }); cmd.AddOption(new Option("-listOps", "List available options.") { Argument = new Argument("enable", false) }); cmd.AddOption(new Option("-seed", "Seed for generating pseudo-random parameters for a given -n argument.") { Argument = new Argument("seed", null)}); + cmd.AddOption(new Option("-p", "Max number of query parameters for a request.") { Argument = new Argument("queryParameters", 1) }); cmd.AddOption(new Option("-numParameters", "Max number of query parameters or form fields for a request.") { Argument = new Argument("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("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("-n"), - maxContentLength : cmdline.ValueForOption("-maxContentLength"), - httpVersions : cmdline.ValueForOption("-http"), - connectionLifetime : cmdline.ValueForOption("-connectionLifetime"), - opIndices : cmdline.ValueForOption("-ops"), - logPath : cmdline.HasOption("-trace") ? cmdline.ValueForOption("-trace") : null, - aspnetLog : cmdline.ValueForOption("-aspnetlog"), - listOps : cmdline.ValueForOption("-listOps"), - seed : cmdline.ValueForOption("-seed") ?? new Random().Next(), - numParameters : cmdline.ValueForOption("-numParameters")); + Run(concurrentRequests : cmdline.ValueForOption("-n"), + maxContentLength : cmdline.ValueForOption("-maxContentLength"), + httpVersions : cmdline.ValueForOption("-http"), + connectionLifetime : cmdline.ValueForOption("-connectionLifetime"), + opIndices : cmdline.ValueForOption("-ops"), + logPath : cmdline.HasOption("-trace") ? cmdline.ValueForOption("-trace") : null, + aspnetLog : cmdline.ValueForOption("-aspnetlog"), + listOps : cmdline.ValueForOption("-listOps"), + seed : cmdline.ValueForOption("-seed") ?? new Random().Next(), + numParameters : cmdline.ValueForOption("-numParameters"), + cancellationProbability : Math.Max(0, Math.Min(1, cmdline.ValueForOption("-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)[] + var clientOperations = new (string, Func)[] { ("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 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 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 func) = clientOperations[opIndex]; + (string operation, Func 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); } - /// Client context containing information pertaining to a single worker. - private sealed class ClientContext + /// Client context containing information pertaining to a single request. + 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) => -- 2.7.4