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);
}
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;
return checksum ^ InitialCrc;
}
}
-}
\ No newline at end of file
+}
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;
}
}
}
- 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)
{
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; }
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; }
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>preview</LangVersion>
+ <Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
using System.Collections.Generic;
using System.CommandLine;
using System.IO;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
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) });
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"),
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
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)
{
Console.WriteLine(clientOperations[i].name);
}
- return;
+ return ExitCode.Success;
}
// derive client operations based on arguments
Console.WriteLine();
- StressServer server = null;
+ StressServer? server = null;
if (config.RunMode.HasFlag(RunMode.server))
{
// Start the Kestrel web server in-proc.
Console.WriteLine($"Server started at {server.ServerUri}");
}
- StressClient client = null;
+ StressClient? client = null;
if (config.RunMode.HasFlag(RunMode.client))
{
// Start the client.
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()
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)
{
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();
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);
{
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();
// 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; }
/// <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)
{
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);
}
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]);
}