From: Eirik Tsarpalis Date: Thu, 25 Jul 2019 17:21:30 +0000 (+0100) Subject: HttpStress: add randomized header operation X-Git-Tag: submit/tizen/20210909.063632~11031^2~830^2~6 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=fbf7c6e7965af33dec81c1c55ad18624eb0a9f2f;p=platform%2Fupstream%2Fdotnet%2Fruntime.git HttpStress: add randomized header operation Aguments the stress suite with an "Echo Header" operation which sends and receives a randomly generated set of headers. Additionally expands random string to full alphanumeric or ascii range. Fixes a bug where `n - 1` workers were being initialized. Commit migrated from https://github.com/dotnet/corefx/commit/ac3e0d6f35dfccdd70bacfc09941fef791192da5 --- diff --git a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs index 6263ecf..8c0d858 100644 --- a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs +++ b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs @@ -8,15 +8,19 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Web; namespace HttpStress { /// Client context containing information pertaining to a single request. public sealed class RequestContext { + private const string alphaNumeric = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private readonly Random _random; private readonly HttpClient _client; private readonly double _cancellationProbability; @@ -75,22 +79,33 @@ namespace HttpStress } } - public string GetRandomString(int maxLength) + /// Gets a random ASCII string within specified length range + public string GetRandomString(int minLength, int maxLength, bool alphaNumericOnly = true) { - int length = _random.Next(0, maxLength); + int length = _random.Next(minLength, maxLength); var sb = new StringBuilder(length); for (int i = 0; i < length; i++) { - sb.Append((char)(_random.Next(0, 26) + 'a')); + if (alphaNumericOnly) + { + // alpha character + sb.Append(alphaNumeric[_random.Next(alphaNumeric.Length)]); + } + else + { + // use a random ascii character + sb.Append((char)_random.Next(0, 128)); + } } + return sb.ToString(); } - public string GetRandomSubstring(string input) + public byte[] GetRandomBytes(int minBytes, int maxBytes) { - int offset = _random.Next(0, input.Length); - int length = _random.Next(0, input.Length - offset + 1); - return input.Substring(offset, length); + byte[] bytes = new byte[_random.Next(minBytes, maxBytes)]; + _random.NextBytes(bytes); + return bytes; } public bool GetRandomBoolean(double probability = 0.5) @@ -101,6 +116,18 @@ namespace HttpStress return _random.NextDouble() < probability; } + public void PopulateWithRandomHeaders(HttpRequestHeaders headers) + { + int numHeaders = _random.Next(maxValue: 100); + + for (int i = 0; i < numHeaders; i++) + { + string name = $"Header-{i}"; + IEnumerable values = Enumerable.Range(0, _random.Next(0, 5)).Select(_ => HttpUtility.UrlEncode(GetRandomString(0, 30, alphaNumericOnly: false))); + headers.Add(name, values); + } + } + public int GetRandomInt32(int minValueInclusive, int maxValueExclusive) => _random.Next(minValueInclusive, maxValueExclusive); public Version GetRandomHttpVersion() => GetRandomBoolean(_http2Probability) ? new Version(2, 0) : new Version(1, 1); @@ -122,7 +149,8 @@ namespace HttpStress using (var req = new HttpRequestMessage(HttpMethod.Get, "/") { Version = httpVersion }) using (HttpResponseMessage m = await ctx.SendAsync(req)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); ValidateContent(ctx.ContentSource, await m.Content.ReadAsStringAsync()); } }), @@ -134,7 +162,9 @@ namespace HttpStress using (var req = new HttpRequestMessage(HttpMethod.Get, "/slow") { Version = httpVersion }) using (HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); + using (Stream s = await m.Content.ReadAsStreamAsync()) { s.ReadByte(); // read single byte from response and throw the rest away @@ -149,21 +179,55 @@ namespace HttpStress using (var req = new HttpRequestMessage(HttpMethod.Get, "/headers") { Version = httpVersion }) using (HttpResponseMessage m = await ctx.SendAsync(req)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); ValidateContent(ctx.ContentSource, await m.Content.ReadAsStringAsync()); } }), + ("GET Echo Headers", + async ctx => + { + Version httpVersion = ctx.GetRandomHttpVersion(); + + using (var req = new HttpRequestMessage(HttpMethod.Get, "/echoHeaders") { Version = httpVersion }) + { + ctx.PopulateWithRandomHeaders(req.Headers); + + using (HttpResponseMessage res = await ctx.SendAsync(req)) + { + ValidateHttpVersion(res, httpVersion); + ValidateStatusCode(res); + + // Validate that response headers are being echoed + foreach (var reqHeader in req.Headers) + { + if (!res.Headers.TryGetValues(reqHeader.Key, out var values)) + { + throw new Exception($"Expected response header name {reqHeader.Key} missing."); + } + else if (!reqHeader.Value.SequenceEqual(values)) + { + string FmtValues(IEnumerable values) => $"{string.Join(", ", values.Select(x => $"\"{x}\""))}"; + throw new Exception($"Unexpected values for header {reqHeader.Key}. Expected {FmtValues(reqHeader.Value)} but got {FmtValues(values)}"); + } + } + } + + } + }), + ("GET Parameters", async ctx => { Version httpVersion = ctx.GetRandomHttpVersion(); string uri = "/variables"; - string expectedResponse = GetGetQueryParameters(ref uri, ctx.MaxRequestLineSize, ctx.ContentSource, ctx, ctx.MaxRequestParameters); + string expectedResponse = GetGetQueryParameters(ref uri, ctx.MaxRequestLineSize, ctx, ctx.MaxRequestParameters); using (var req = new HttpRequestMessage(HttpMethod.Get, uri) { Version = httpVersion }) using (HttpResponseMessage m = await ctx.SendAsync(req)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); ValidateContent(expectedResponse, await m.Content.ReadAsStringAsync(), $"Uri: {uri}"); } }), @@ -216,13 +280,14 @@ namespace HttpStress ("POST", async ctx => { - string content = ctx.GetRandomSubstring(ctx.ContentSource); + string content = ctx.GetRandomString(0, ctx.MaxContentLength); Version httpVersion = ctx.GetRandomHttpVersion(); using (var req = new HttpRequestMessage(HttpMethod.Post, "/") { Version = httpVersion, Content = new StringDuplexContent(content) }) using (HttpResponseMessage m = await ctx.SendAsync(req)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); ValidateContent(content, await m.Content.ReadAsStringAsync());; } }), @@ -230,13 +295,14 @@ namespace HttpStress ("POST Multipart Data", async ctx => { - (string expected, MultipartContent formDataContent) formData = GetMultipartContent(ctx.ContentSource, ctx, ctx.MaxRequestParameters); + (string expected, MultipartContent formDataContent) formData = GetMultipartContent(ctx, ctx.MaxRequestParameters); Version httpVersion = ctx.GetRandomHttpVersion(); using (var req = new HttpRequestMessage(HttpMethod.Post, "/") { Version = httpVersion, Content = formData.formDataContent }) using (HttpResponseMessage m = await ctx.SendAsync(req)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); ValidateContent($"{formData.expected}", await m.Content.ReadAsStringAsync());; } }), @@ -244,13 +310,14 @@ namespace HttpStress ("POST Duplex", async ctx => { - string content = ctx.GetRandomSubstring(ctx.ContentSource); + string content = ctx.GetRandomString(0, ctx.MaxContentLength); Version httpVersion = ctx.GetRandomHttpVersion(); using (var req = new HttpRequestMessage(HttpMethod.Post, "/duplex") { Version = httpVersion, Content = new StringDuplexContent(content) }) using (HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); ValidateContent(content, await m.Content.ReadAsStringAsync()); } }), @@ -258,13 +325,14 @@ namespace HttpStress ("POST Duplex Slow", async ctx => { - string content = ctx.GetRandomSubstring(ctx.ContentSource); + string content = ctx.GetRandomString(0, ctx.MaxContentLength); Version httpVersion = ctx.GetRandomHttpVersion(); using (var req = new HttpRequestMessage(HttpMethod.Post, "/duplexSlow") { Version = httpVersion, Content = new ByteAtATimeNoLengthContent(Encoding.ASCII.GetBytes(content)) }) using (HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); ValidateContent(content, await m.Content.ReadAsStringAsync()); } }), @@ -272,7 +340,7 @@ namespace HttpStress ("POST ExpectContinue", async ctx => { - string content = ctx.GetRandomSubstring(ctx.ContentSource); + string content = ctx.GetRandomString(0, ctx.MaxContentLength); Version httpVersion = ctx.GetRandomHttpVersion(); using (var req = new HttpRequestMessage(HttpMethod.Post, "/") { Version = httpVersion, Content = new StringContent(content) }) @@ -280,7 +348,8 @@ namespace HttpStress req.Headers.ExpectContinue = true; using (HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); ValidateContent(content, await m.Content.ReadAsStringAsync()); } } @@ -293,7 +362,9 @@ namespace HttpStress using (var req = new HttpRequestMessage(HttpMethod.Head, "/") { Version = httpVersion }) using (HttpResponseMessage m = await ctx.SendAsync(req)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); + if (m.Content.Headers.ContentLength != ctx.MaxContentLength) { throw new Exception($"Expected {ctx.MaxContentLength}, got {m.Content.Headers.ContentLength}"); @@ -306,13 +377,15 @@ namespace HttpStress ("PUT", async ctx => { - string content = ctx.GetRandomSubstring(ctx.ContentSource); + string content = ctx.GetRandomString(0, ctx.MaxContentLength); Version httpVersion = ctx.GetRandomHttpVersion(); using (var req = new HttpRequestMessage(HttpMethod.Put, "/") { Version = httpVersion, Content = new StringContent(content) }) using (HttpResponseMessage m = await ctx.SendAsync(req)) { - ValidateResponse(m, httpVersion); + ValidateHttpVersion(m, httpVersion); + ValidateStatusCode(m); + string r = await m.Content.ReadAsStringAsync(); if (r != "") throw new Exception($"Got unexpected response: {r}"); } @@ -320,7 +393,7 @@ namespace HttpStress }; // Validation of a response message - private static void ValidateResponse(HttpResponseMessage m, Version expectedVersion) + private static void ValidateHttpVersion(HttpResponseMessage m, Version expectedVersion) { if (m.Version != expectedVersion) { @@ -328,6 +401,14 @@ namespace HttpStress } } + private static void ValidateStatusCode(HttpResponseMessage m, HttpStatusCode expectedStatus = HttpStatusCode.OK) + { + if (m.StatusCode != expectedStatus) + { + throw new Exception($"Expected status code {expectedStatus}, got {m.StatusCode}"); + } + } + private static void ValidateContent(string expectedContent, string actualContent, string details = null) { if (actualContent != expectedContent) @@ -336,7 +417,7 @@ namespace HttpStress } } - private static string GetGetQueryParameters(ref string uri, int maxRequestLineSize, string contentSource, RequestContext clientContext, int numParameters) + private static string GetGetQueryParameters(ref string uri, int maxRequestLineSize, RequestContext clientContext, int numParameters) { if (maxRequestLineSize < uri.Length) { @@ -366,7 +447,7 @@ namespace HttpStress uriSb.Append(key); - string value = clientContext.GetRandomString(Math.Min(appxMaxValueLength, remainingLength)); + string value = clientContext.GetRandomString(0, Math.Min(appxMaxValueLength, remainingLength)); expectedString.Append(value); uriSb.Append(value); } @@ -375,9 +456,9 @@ namespace HttpStress return expectedString.ToString(); } - private static (string, MultipartContent) GetMultipartContent(string contentSource, RequestContext clientContext, int numFormFields) + private static (string, MultipartContent) GetMultipartContent(RequestContext clientContext, int numFormFields) { - var multipartContent = new MultipartContent("prefix" + clientContext.GetRandomSubstring(contentSource), "test_boundary"); + var multipartContent = new MultipartContent("prefix" + clientContext.GetRandomString(0, clientContext.MaxContentLength, alphaNumericOnly: true), "test_boundary"); StringBuilder sb = new StringBuilder(); int num = clientContext.GetRandomInt32(1, numFormFields + 1); @@ -385,7 +466,7 @@ namespace HttpStress for (int i = 0; i < num; i++) { sb.Append("--test_boundary\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n"); - string content = clientContext.GetRandomSubstring(contentSource); + string content = clientContext.GetRandomString(0, clientContext.MaxContentLength); sb.Append(content); sb.Append("\r\n"); multipartContent.Add(new StringContent(content)); 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 1f59f03..0658180 100644 --- a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs +++ b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs @@ -19,7 +19,7 @@ public class Program [Flags] enum RunMode { server = 1, client = 2, both = 3 }; - public static void Main(string[] args) + public static async Task Main(string[] args) { var cmd = new RootCommand(); cmd.AddOption(new Option("-n", "Max number of requests to make concurrently.") { Argument = new Argument("numWorkers", Environment.ProcessorCount) }); @@ -30,6 +30,7 @@ public class Program cmd.AddOption(new Option("-http", "HTTP version (1.1 or 2.0)") { Argument = new Argument("version", HttpVersion.Version20) }); cmd.AddOption(new Option("-connectionLifetime", "Max connection lifetime length (milliseconds).") { Argument = new Argument("connectionLifetime", null)}); cmd.AddOption(new Option("-ops", "Indices of the operations to use") { Argument = new Argument("space-delimited indices", null) }); + cmd.AddOption(new Option("-xops", "Indices of the operations to exclude") { Argument = new Argument("space-delimited indices", null) }); cmd.AddOption(new Option("-trace", "Enable Microsoft-System-Net-Http tracing.") { Argument = new Argument("\"console\" or path") }); 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) }); @@ -51,6 +52,7 @@ public class Program return; } + await Run( runMode : cmdline.ValueForOption("-runMode"), serverUri : cmdline.ValueForOption("-serverUri"), @@ -61,6 +63,7 @@ public class Program httpVersion : cmdline.ValueForOption("-http"), connectionLifetime : cmdline.ValueForOption("-connectionLifetime"), opIndices : cmdline.ValueForOption("-ops"), + excludedOpIndices : cmdline.ValueForOption("-xops"), logPath : cmdline.HasOption("-trace") ? cmdline.ValueForOption("-trace") : null, aspnetLog : cmdline.ValueForOption("-aspnetlog"), listOps : cmdline.ValueForOption("-listOps"), @@ -70,16 +73,35 @@ public class Program displayIntervalSeconds : cmdline.ValueForOption("-displayInterval")); } - private static void Run(RunMode runMode, Uri serverUri, bool httpSys, int concurrentRequests, int maxContentLength, int maxRequestLineSize, Version httpVersion, int? connectionLifetime, int[] opIndices, string logPath, bool aspnetLog, bool listOps, int seed, int numParameters, double cancellationProbability, int displayIntervalSeconds) + private static async Task Run(RunMode runMode, Uri serverUri, bool httpSys, int concurrentRequests, int maxContentLength, int maxRequestLineSize, Version httpVersion, int? connectionLifetime, int[] opIndices, int[] excludedOpIndices, string logPath, bool aspnetLog, bool listOps, int seed, int numParameters, double cancellationProbability, int displayIntervalSeconds) { - if (serverUri.Scheme != "https") + (string name, Func op)[] clientOperations = ClientOperations.Operations; + + // handle operation index arguments + switch (opIndices, excludedOpIndices) { - Console.Error.WriteLine("Server uri must be https."); - return; - } + case (null, null): + break; + case (_, null): + clientOperations = opIndices.Select(i => clientOperations[i]).ToArray(); + break; + case (null, _): + opIndices = + Enumerable + .Range(0, clientOperations.Length) + .Concat(excludedOpIndices) + .GroupBy(x => x) + .Where(gp => gp.Count() < 2) + .Select(gp => gp.Key) + .ToArray(); - (string name, Func op)[] clientOperations = ClientOperations.Operations; + clientOperations = opIndices.Select(i => clientOperations[i]).ToArray(); + break; + default: + Console.Error.WriteLine("Cannot specify both -ops and -xops flags simultaneously"); + return; + } Console.WriteLine(" .NET Core: " + Path.GetFileName(Path.GetDirectoryName(typeof(object).Assembly.Location))); Console.WriteLine(" ASP.NET Core: " + Path.GetFileName(Path.GetDirectoryName(typeof(WebHost).Assembly.Location))); @@ -112,6 +134,12 @@ public class Program return; } + if (serverUri.Scheme != "https") + { + Console.Error.WriteLine("Server uri must be https."); + return; + } + if (runMode.HasFlag(RunMode.server)) { // Start the Kestrel web server in-proc. @@ -125,10 +153,6 @@ public class Program { // Start the client. Console.WriteLine($"Starting {concurrentRequests} client workers."); - if (opIndices != null) - { - clientOperations = opIndices.Select(i => clientOperations[i]).ToArray(); - } new StressClient( serverUri: serverUri, @@ -144,7 +168,7 @@ public class Program displayInterval: TimeSpan.FromSeconds(displayIntervalSeconds)); } - AwaitCancelKeyPress().Wait(); + await AwaitCancelKeyPress(); } private static async Task AwaitCancelKeyPress() diff --git a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressClient.cs b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressClient.cs index 0aa3b57..8968baa 100644 --- a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressClient.cs +++ b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressClient.cs @@ -174,7 +174,7 @@ namespace HttpStress } // Start N workers, each of which sits in a loop making requests. - Task[] tasks = Enumerable.Range(0, concurrentRequests - 1).Select(RunWorker).ToArray(); + Task[] tasks = Enumerable.Range(0, concurrentRequests).Select(RunWorker).ToArray(); await Task.WhenAll(tasks); } } diff --git a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressServer.cs b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressServer.cs index 400d7d3..7bee0e6 100644 --- a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressServer.cs +++ b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/StressServer.cs @@ -145,6 +145,21 @@ namespace HttpStress } } }); + endpoints.MapGet("/echoHeaders", async context => + { + foreach(KeyValuePair header in context.Request.Headers) + { + // skip pseudo-headers surfaced by kestrel + if (header.Key.StartsWith(':')) continue; + + // kestrel seems to not be splitting comma separated header values, handle here + string[] values = header.Value.SelectMany(v => v.Split(',')).Select(x => x.Trim()).ToArray(); + context.Response.Headers.Add(header.Key, new StringValues(values)); + } + + await context.Response.WriteAsync("ok"); + + }); endpoints.MapGet("/variables", async context => { string queryString = context.Request.QueryString.Value;