From a253dc13787cc53c14f901de4a04c62eab59e544 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 12 Nov 2019 15:03:31 +0000 Subject: [PATCH 01/12] add an ssl stress suite --- .../StressTests/SslStress/Configuration.cs | 24 ++ .../SslStress/Directory.Build.props | 1 + .../SslStress/Directory.Build.targets | 1 + .../tests/StressTests/SslStress/Dockerfile | 14 + .../tests/StressTests/SslStress/Program.cs | 144 +++++++++ .../tests/StressTests/SslStress/Readme.md | 3 + .../SslClientBase.StressResultAggregator.cs | 163 ++++++++++ .../StressTests/SslStress/SslClientBase.cs | 147 +++++++++ .../StressTests/SslStress/SslServerBase.cs | 131 ++++++++ .../StressTests/SslStress/SslStress.csproj | 11 + .../tests/StressTests/SslStress/SslStress.sln | 24 ++ .../StressTests/SslStress/StressOperations.cs | 283 ++++++++++++++++++ .../SslStress/Utils/ChecksumHelpers.cs | 61 ++++ .../SslStress/Utils/CountingStream.cs | 90 ++++++ .../SslStress/Utils/ErrorAggregator.cs | 120 ++++++++ .../Utils/HumanReadableByteSizeFormatter.cs | 26 ++ .../SslStress/Utils/PipeExtensions.cs | 93 ++++++ .../SslStress/Utils/RandomHelpers.cs | 21 ++ .../SslStress/Utils/TaskExtensions.cs | 35 +++ .../tests/StressTests/SslStress/global.json | 1 + 20 files changed, 1393 insertions(+) create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.props create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.targets create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Dockerfile create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Program.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Readme.md create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/SslStress.csproj create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/SslStress.sln create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Utils/ChecksumHelpers.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Utils/CountingStream.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Utils/ErrorAggregator.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Utils/HumanReadableByteSizeFormatter.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Utils/PipeExtensions.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Utils/RandomHelpers.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Utils/TaskExtensions.cs create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/global.json diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs b/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs new file mode 100644 index 000000000000..ac7af6993f80 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; + +namespace SslStress +{ + [Flags] + public enum RunMode { server = 1, client = 2, both = server | client }; + + public class Configuration + { + public IPEndPoint ServerEndpoint { get; set; } = new IPEndPoint(IPAddress.Loopback, 0); + public RunMode RunMode { get; set; } + public int RandomSeed { get; set; } + public int MaxConnections { get; set; } + public int MaxBufferLength { get; set; } + public TimeSpan? MaxExecutionTime { get; set; } + public TimeSpan DisplayInterval { get; set; } + public TimeSpan? ConnectionLifetime { get; set; } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.props b/src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.props new file mode 100644 index 000000000000..8998bf454677 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.props @@ -0,0 +1 @@ + diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.targets b/src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.targets new file mode 100644 index 000000000000..8998bf454677 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.targets @@ -0,0 +1 @@ + diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Dockerfile b/src/System.Net.Security/tests/StressTests/SslStress/Dockerfile new file mode 100644 index 000000000000..46ef47b8a4d1 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Dockerfile @@ -0,0 +1,14 @@ +ARG SDK_BASE_IMAGE=mcr.microsoft.com/dotnet/core/sdk:3.0.100-buster +FROM $SDK_BASE_IMAGE + +WORKDIR /app +COPY . . + +ARG CONFIGURATION=Release +RUN dotnet build -c $CONFIGURATION + +EXPOSE 5001 + +ENV CONFIGURATION=$CONFIGURATION +ENV SSLSTRESS_ARGS='' +CMD dotnet run --no-build -c $CONFIGURATION -- $SSLSTRESS_ARGS diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs new file mode 100644 index 000000000000..db61d4af734b --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.CommandLine; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; + +namespace SslStress +{ + public static class Program + { + public enum ExitCode { Success = 0, StressError = 1, CliError = 2 }; + + public static async Task Main(string[] args) + { + if (!TryParseCli(args, out Configuration? config)) + { + return (int)ExitCode.CliError; + } + + return (int)await Run(config); + } + + private static async Task Run(Configuration config) + { + if ((config.RunMode & RunMode.both) == 0) + { + Console.Error.WriteLine("Must specify a valid run mode"); + return ExitCode.CliError; + } + + static string GetAssemblyInfo(Assembly assembly) => $"{assembly.Location}, modified {new FileInfo(assembly.Location).LastWriteTime}"; + + Console.WriteLine(" .NET Core: " + GetAssemblyInfo(typeof(object).Assembly)); + Console.WriteLine(" System.Net.Security: " + GetAssemblyInfo(typeof(System.Net.Security.SslStream).Assembly)); + Console.WriteLine(" Server Endpoint: " + config.ServerEndpoint); + Console.WriteLine(" Concurrency: " + config.MaxConnections); + Console.WriteLine(" Max Execution Time: " + ((config.MaxExecutionTime != null) ? config.MaxExecutionTime.Value.ToString() : "infinite")); + Console.WriteLine(" Max Conn. Lifetime: " + config.ConnectionLifetime); + Console.WriteLine(" Random Seed: " + config.RandomSeed); + Console.WriteLine(); + + StressServer? server = null; + if (config.RunMode.HasFlag(RunMode.server)) + { + // Start the SSL web server in-proc. + Console.WriteLine($"Starting SSL server."); + server = new StressServer(config); + server.Start(); + + Console.WriteLine($"Server listening to {server.ServerEndpoint}"); + } + + StressClient? client = null; + if (config.RunMode.HasFlag(RunMode.client)) + { + // Start the client. + Console.WriteLine($"Starting {config.MaxConnections} client workers."); + + client = new StressClient(config); + client.Start(); + } + + await WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(config.MaxExecutionTime); + + try + { + if (client != null) await client.StopAsync(); + if (server != null) await server.StopAsync(); + } + finally + { + client?.PrintFinalReport(); + } + + return client?.TotalErrorCount == 0 ? ExitCode.Success : ExitCode.StressError; + + static async Task WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(TimeSpan? maxExecutionTime = null) + { + var tcs = new TaskCompletionSource(); + 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 bool TryParseCli(string[] args, [NotNullWhen(true)] out Configuration? config) + { + var cmd = new RootCommand(); + cmd.AddOption(new Option(new[] { "--help", "-h" }, "Display this help text.")); + cmd.AddOption(new Option(new[] { "--mode", "-m" }, "Stress suite execution mode. Defaults to Both.") { Argument = new Argument("runMode", RunMode.both) }); + cmd.AddOption(new Option(new[] { "--num-connections", "-n" }, "Max number of connections to open concurrently.") { Argument = new Argument("connections", Math.Max(Environment.ProcessorCount / 2, 2)) }); + cmd.AddOption(new Option(new[] { "--server-endpoint", "-e" }, "Endpoint to bind to if server, endpoint to listen to if client.") { Argument = new Argument("ipEndpoint", "127.0.0.1:5002") }); + cmd.AddOption(new Option(new[] { "--max-execution-time", "-t" }, "Maximum stress execution time, in minutes. Defaults to infinity.") { Argument = new Argument("minutes", null) }); + cmd.AddOption(new Option(new[] { "--max-buffer-length", "-l" }, "Maximum buffer length to write on ssl stream. Defaults to 8192.") { Argument = new Argument("bytes", 8192) }); + cmd.AddOption(new Option(new[] { "--connection-lifetime", "-L" }, "Maximum stress execution time, in seconds. Defaults to 120 seconds.") { Argument = new Argument("minutes", 120) }); + cmd.AddOption(new Option(new[] { "--display-interval", "-i" }, "Client stats display interval, in seconds. Defaults to 5 seconds.") { Argument = new Argument("seconds", 5) }); + cmd.AddOption(new Option(new[] { "--seed", "-s" }, "Seed for generating pseudo-random parameters. Also depends on the -n argument.") { Argument = new Argument("seed", (new Random().Next())) }); + + ParseResult parseResult = cmd.Parse(args); + if (parseResult.Errors.Count > 0 || parseResult.HasOption("-h")) + { + foreach (ParseError error in parseResult.Errors) + { + Console.WriteLine(error); + } + Console.WriteLine(); + new HelpBuilder(new SystemConsole()).Write(cmd); + config = null; + return false; + } + + config = new Configuration() + { + RunMode = parseResult.ValueForOption("-m"), + MaxConnections = parseResult.ValueForOption("-n"), + ServerEndpoint = IPEndPoint.Parse(parseResult.ValueForOption("-e")), + MaxExecutionTime = parseResult.ValueForOption("-M").Select(TimeSpan.FromMinutes), + MaxBufferLength = parseResult.ValueForOption("-l"), + ConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption("-L")), + DisplayInterval = TimeSpan.FromSeconds(parseResult.ValueForOption("-i")), + RandomSeed = parseResult.ValueForOption("-s"), + }; + + return true; + } + + private static S? Select(this T? value, Func mapper) where T : struct where S : struct + { + return value is null ? null : new S?(mapper(value.Value)); + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Readme.md b/src/System.Net.Security/tests/StressTests/SslStress/Readme.md new file mode 100644 index 000000000000..8193c18fb7da --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Readme.md @@ -0,0 +1,3 @@ +## SslStress + +Stress testing suite for SslStream diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs new file mode 100644 index 000000000000..2ca4a3161f3b --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using SslStress.Utils; + +namespace SslStress +{ + public abstract partial class SslClientBase + { + private class StressResultAggregator + { + private long _totalConnections = 0; + private readonly long[] _successes, _failures, _cancellations; + private readonly ErrorAggregator _errors = new ErrorAggregator(); + private readonly StreamCounter[] _currentCounters; + private readonly StreamCounter[] _aggregateCounters; + + public StressResultAggregator(int workerCount) + { + _currentCounters = Enumerable.Range(0, workerCount).Select(_ => new StreamCounter()).ToArray(); + _aggregateCounters = Enumerable.Range(0, workerCount).Select(_ => new StreamCounter()).ToArray(); + _successes = new long[workerCount]; + _failures = new long[workerCount]; + _cancellations = new long[workerCount]; + } + + public long TotalConnections => _totalConnections; + public long TotalFailures => _failures.Sum(); + public long TotalCancellations => _cancellations.Sum(); + + public StreamCounter GetCounters(int workerId) => _currentCounters[workerId]; + + public void RecordSuccess(int workerId) + { + _successes[workerId]++; + Interlocked.Increment(ref _totalConnections); + UpdateCounters(workerId); + } + + public void RecordFailure(int workerId, Exception exn) + { + _failures[workerId]++; + Interlocked.Increment(ref _totalConnections); + _errors.RecordError(exn); + UpdateCounters(workerId); + + lock (Console.Out) + { + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine($"Worker #{workerId}: unhandled exception: {exn}"); + Console.ResetColor(); + } + } + + public void RecordCancellation(int workerId) + { + _cancellations[workerId]++; + Interlocked.Increment(ref _totalConnections); + UpdateCounters(workerId); + } + + private void UpdateCounters(int workerId) + { + // need to synchronize with GetCounterView to avoid reporting bad data + lock (_aggregateCounters) + { + _aggregateCounters[workerId].Append(_currentCounters[workerId]); + _currentCounters[workerId].Reset(); + } + } + + private (StreamCounter total, StreamCounter current)[] GetCounterView() + { + // generate a coherent view of counter state + lock (_aggregateCounters) + { + var view = new (StreamCounter total, StreamCounter current)[_aggregateCounters.Length]; + for (int i = 0; i < _aggregateCounters.Length; i++) + { + StreamCounter current = _currentCounters[i].Clone(); + StreamCounter total = _aggregateCounters[i].Clone().Append(current); + view[i] = (total, current); + } + + return view; + } + } + + public void PrintFailureTypes() => _errors.PrintFailureTypes(); + + public void PrintCurrentResults(TimeSpan elapsed) + { + (StreamCounter total, StreamCounter current)[] counters = GetCounterView(); + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write($"[{DateTime.Now}]"); + Console.ResetColor(); + Console.WriteLine(" Elapsed: " + elapsed.ToString(@"hh\:mm\:ss")); + Console.ResetColor(); + + for (int i = 0; i < _currentCounters.Length; i++) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write($"\tWorker #{i.ToString("N0")}:"); + Console.ResetColor(); + + Console.ForegroundColor = ConsoleColor.Green; + Console.Write($"\tPass: "); + Console.ResetColor(); + Console.Write(_successes[i].ToString("N0")); + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.Write("\tFail: "); + Console.ResetColor(); + Console.Write(_failures[i].ToString("N0")); + + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.Write($"\tTx: "); + Console.ResetColor(); + Console.Write(FmtBytes(counters[i].total.bytesWritten)); + Console.ForegroundColor = ConsoleColor.DarkMagenta; + Console.Write($"\tRx: "); + Console.ResetColor(); + Console.Write(FmtBytes(counters[i].total.bytesRead)); + + Console.WriteLine(); + } + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write("\tTOTAL : "); + + Console.ForegroundColor = ConsoleColor.Green; + Console.Write($"\tPass: "); + Console.ResetColor(); + Console.Write(_successes.Sum().ToString("N0")); + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.Write("\tFail: "); + Console.ResetColor(); + Console.Write(_failures.Sum().ToString("N0")); + + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.Write("\tTx: "); + Console.ResetColor(); + Console.Write(FmtBytes(counters.Select(c => c.total.bytesWritten).Sum())); + Console.ForegroundColor = ConsoleColor.DarkMagenta; + Console.Write($"\tRx: "); + Console.ResetColor(); + Console.Write(FmtBytes(counters.Select(c => c.total.bytesWritten).Sum())); + + Console.WriteLine(); + Console.WriteLine(); + + static string FmtBytes(long value) => HumanReadableByteSizeFormatter.Format(value); + } + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs new file mode 100644 index 000000000000..be98a173de05 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using SslStress.Utils; + +namespace SslStress +{ + + public abstract partial class SslClientBase : IAsyncDisposable + { + protected readonly Configuration _config; + + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly StressResultAggregator _aggregator; + private readonly Lazy _clientTask; + private readonly Stopwatch _stopwatch = new Stopwatch(); + + public SslClientBase(Configuration config) + { + if (config.MaxConnections < 1) throw new ArgumentOutOfRangeException(nameof(config.MaxConnections)); + + _config = config; + _aggregator = new StressResultAggregator(config.MaxConnections); + _clientTask = new Lazy(StartCore); + } + + protected abstract Task HandleConnection(int workerId, SslStream stream, TcpClient client, Random random, CancellationToken token); + + protected virtual async Task EstablishSslStream(Stream networkStream, Random random, CancellationToken token) + { + var sslStream = new SslStream(networkStream, leaveInnerStreamOpen: false); + var clientOptions = new SslClientAuthenticationOptions + { + ApplicationProtocols = new List { SslApplicationProtocol.Http11 }, + RemoteCertificateValidationCallback = ((x, y, z, w) => true), + TargetHost = SslServerBase.Hostname, + }; + + await sslStream.AuthenticateAsClientAsync(clientOptions, token); + return sslStream; + } + + public ValueTask DisposeAsync() => StopAsync(); + + public void Start() + { + if (_cts.IsCancellationRequested) throw new ObjectDisposedException(nameof(SslClientBase)); + _ = _clientTask.Value; + } + + public async ValueTask StopAsync() + { + _cts.Cancel(); + await _clientTask.Value; + } + + public Task Task + { + get + { + if (!_clientTask.IsValueCreated) throw new InvalidOperationException("Client has not been started yet"); + return _clientTask.Value; + } + } + + public long TotalErrorCount => _aggregator.TotalFailures; + + private Task StartCore() + { + _stopwatch.Start(); + + // Spin up a thread dedicated to outputting stats for each defined interval + new Thread(() => + { + while (!_cts.IsCancellationRequested) + { + Thread.Sleep(_config.DisplayInterval); + lock (Console.Out) { _aggregator.PrintCurrentResults(_stopwatch.Elapsed); } + } + }) + { IsBackground = true }.Start(); + + IEnumerable workers = CreateWorkerSeeds().Select(x => RunSingleWorker(x.workerId, x.random)); + return Task.WhenAll(workers); + + async Task RunSingleWorker(int workerId, Random random) + { + StreamCounter counter = _aggregator.GetCounters(workerId); + + for (long testId = 0; !_cts.IsCancellationRequested; testId++) + { + try + { + using var client = new TcpClient(); + await client.ConnectAsync(_config.ServerEndpoint.Address, _config.ServerEndpoint.Port); + var stream = new CountingStream(client.GetStream(), counter); + using SslStream sslStream = await EstablishSslStream(stream, random, _cts.Token); + await HandleConnection(workerId, sslStream, client, random, _cts.Token); + + _aggregator.RecordSuccess(workerId); + } + catch (Exception) when (_cts.IsCancellationRequested) + { + _aggregator.RecordCancellation(workerId); + } + catch (Exception e) + { + _aggregator.RecordFailure(workerId, e); + } + } + } + + IEnumerable<(int workerId, Random random)> CreateWorkerSeeds() + { + // deterministically generate random instance for each individual worker + Random random = new Random(_config.RandomSeed); + for (int workerId = 0; workerId < _config.MaxConnections; workerId++) + { + yield return (workerId, random.NextRandom()); + } + } + } + + public void PrintFinalReport() + { + lock (Console.Out) + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine("HttpStress Run Final Report"); + Console.WriteLine(); + + _aggregator.PrintCurrentResults(_stopwatch.Elapsed); + _aggregator.PrintFailureTypes(); + } + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs new file mode 100644 index 000000000000..7ac0858030a2 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Net.Security; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using System.Runtime.InteropServices; + +namespace SslStress +{ + public abstract class SslServerBase : IAsyncDisposable + { + public const string Hostname = "contoso.com"; + + protected readonly Configuration _config; + + private readonly X509Certificate2 _certificate; + private readonly TcpListener _listener; + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly Lazy _serverTask; + + public EndPoint ServerEndpoint => _listener.LocalEndpoint; + + public SslServerBase(Configuration config) + { + if (config.MaxConnections < 1) throw new ArgumentOutOfRangeException(nameof(config.MaxConnections)); + + _config = config; + _certificate = CreateSelfSignedCertificate(); + _listener = new TcpListener(config.ServerEndpoint) { ExclusiveAddressUse = (config.MaxConnections == 1) }; + _serverTask = new Lazy(StartCore); + } + + protected abstract Task HandleConnection(SslStream sslStream, TcpClient client, CancellationToken token); + + protected virtual async Task EstablishSslStream(Stream networkStream, CancellationToken token) + { + var sslStream = new SslStream(networkStream, leaveInnerStreamOpen: false); + var serverOptions = new SslServerAuthenticationOptions + { + ApplicationProtocols = new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }, + ServerCertificate = _certificate, + }; + + await sslStream.AuthenticateAsServerAsync(serverOptions, token); + return sslStream; + } + + public void Start() + { + if (_cts.IsCancellationRequested) throw new ObjectDisposedException(nameof(SslServerBase)); + _ = _serverTask.Value; + } + + public async ValueTask StopAsync() + { + _cts.Cancel(); + await _serverTask.Value; + _listener.Stop(); + } + + public Task Task + { + get + { + if (!_serverTask.IsValueCreated) throw new InvalidOperationException("Server has not been started yet"); + return _serverTask.Value; + } + } + + public ValueTask DisposeAsync() => StopAsync(); + + private Task StartCore() + { + _listener.Start(); + IEnumerable workers = Enumerable.Range(1, _config.MaxConnections).Select(_ => RunSingleWorker()); + return Task.WhenAll(workers); + + async Task RunSingleWorker() + { + while(!_cts.IsCancellationRequested) + { + try + { + using TcpClient client = await _listener.AcceptTcpClientAsync(); + using SslStream stream = await EstablishSslStream(client.GetStream(), _cts.Token); + await HandleConnection(stream, client, _cts.Token); + } + catch when (_cts.IsCancellationRequested) + { + + } + catch (Exception e) + { + lock (Console.Out) + { + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine($"Server: unhandled exception: {e}"); + Console.ResetColor(); + } + } + } + } + } + + protected virtual X509Certificate2 CreateSelfSignedCertificate() + { + using var rsa = RSA.Create(); + var certReq = new CertificateRequest($"CN={Hostname}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); + certReq.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false)); + certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false)); + X509Certificate2 cert = certReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddMonths(-1), DateTimeOffset.UtcNow.AddMonths(1)); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + cert = new X509Certificate2(cert.Export(X509ContentType.Pfx)); + } + + return cert; + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslStress.csproj b/src/System.Net.Security/tests/StressTests/SslStress/SslStress.csproj new file mode 100644 index 000000000000..7b056ea30cf9 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslStress.csproj @@ -0,0 +1,11 @@ + + + Exe + netcoreapp3.0 + enable + + + + + + \ No newline at end of file diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslStress.sln b/src/System.Net.Security/tests/StressTests/SslStress/SslStress.sln new file mode 100644 index 000000000000..ee4b67b7614b --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslStress.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27428.2002 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SslStress", "SslStress.csproj", "{802E12E4-7E4C-493D-B767-A69223AE7FB2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {802E12E4-7E4C-493D-B767-A69223AE7FB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {802E12E4-7E4C-493D-B767-A69223AE7FB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {802E12E4-7E4C-493D-B767-A69223AE7FB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {802E12E4-7E4C-493D-B767-A69223AE7FB2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {264DF521-D85C-41B0-B753-AB4C5C7505FB} + EndGlobalSection +EndGlobal diff --git a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs new file mode 100644 index 000000000000..bcb5108cc875 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// SslStream stress scenario +// +// * Client sends sequences of random data, accompanied with length and checksum information. +// * Server echoes back the same data. Both client and server validate integrity of received data. +// * Data is written using randomized combinations of the SslStream.Write* methods. +// * Data is ingested using System.IO.Pipelines. + +using System; +using System.Buffers; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SslStress.Utils; + +namespace SslStress +{ + public struct DataSegment + { + private readonly byte[] _buffer; + + public DataSegment(int length) + { + _buffer = ArrayPool.Shared.Rent(length); + Length = length; + } + + public int Length { get; } + public Memory AsMemory() => new Memory(_buffer, 0, Length); + public Span AsSpan() => new Span(_buffer, 0, Length); + + public ulong Checksum => CRC.CalculateCRC(AsSpan()); + public void Return() => ArrayPool.Shared.Return(_buffer); + + /// Create and populate a segment with random data + public static DataSegment CreateRandom(Random random, int maxLength) + { + int size = random.Next(0, maxLength); + var chunk = new DataSegment(size); + foreach (ref byte b in chunk.AsSpan()) + { + b = s_bytePool[random.Next(255)]; + } + + return chunk; + } + + private static readonly byte[] s_bytePool = + Enumerable + .Range(0, 256) + .Select(i => (byte)i) + .Where(b => b != (byte)'\n') + .ToArray(); + } + + // Serializes data segment using the following format: ,, + public class DataSegmentSerializer + { + private static readonly Encoding s_encoding = Encoding.ASCII; + + private readonly byte[] _buffer = new byte[32]; + private readonly char[] _charBuffer = new char[32]; + + public async Task SerializeAsync(Stream stream, DataSegment segment, Random? random = null) + { + // length + int numsize = s_encoding.GetBytes(segment.Length.ToString(), _buffer); + await stream.WriteAsync(_buffer.AsMemory().Slice(0, numsize)); + stream.WriteByte((byte)','); + // checksum + numsize = s_encoding.GetBytes(segment.Checksum.ToString(), _buffer); + await stream.WriteAsync(_buffer.AsMemory().Slice(0, numsize)); + stream.WriteByte((byte)','); + // payload + Memory source = segment.AsMemory(); + // write the entire segment outright if not given random instance + if (random == null) + { + await stream.WriteAsync(source); + return; + } + // randomize chunking otherwise + while (source.Length > 0) + { + if (random.NextBoolean(probability: 0.05)) + { + stream.WriteByte(source.Span[0]); + source = source.Slice(1); + } + else + { + // TODO consider non-uniform distribution for chunk sizes + int chunkSize = random.Next(source.Length); + Memory chunk = source.Slice(0, chunkSize); + source = source.Slice(chunkSize); + + if (random.NextBoolean(probability: 0.9)) + { + await stream.WriteAsync(chunk); + } + else + { + stream.Write(chunk.Span); + } + } + + if (random.NextBoolean(probability: 0.3)) + { + await stream.FlushAsync(); + } + } + } + + public DataSegment Deserialize(ReadOnlySequence buffer) + { + // length + SequencePosition? pos = buffer.PositionOf((byte)','); + if (pos == null) + { + throw new FormatException("should contain comma-separated values"); + } + + ReadOnlySequence lengthBytes = buffer.Slice(0, pos.Value); + int numSize = s_encoding.GetChars(lengthBytes.ToArray(), _charBuffer); + int length = int.Parse(_charBuffer.AsSpan().Slice(0, numSize)); + buffer = buffer.Slice(buffer.GetPosition(1, pos.Value)); + + // checksum + pos = buffer.PositionOf((byte)','); + if (pos == null) + { + throw new FormatException("should contain comma-separated values"); + } + + ReadOnlySequence checksumBytes = buffer.Slice(0, pos.Value); + numSize = s_encoding.GetChars(checksumBytes.ToArray(), _charBuffer); + ulong checksum = ulong.Parse(_charBuffer.AsSpan().Slice(0, numSize)); + buffer = buffer.Slice(buffer.GetPosition(1, pos.Value)); + + // payload + if (length != (int)buffer.Length) + { + throw new Exception("declared length does not match payload length"); + } + + var chunk = new DataSegment((int)buffer.Length); + buffer.CopyTo(chunk.AsSpan()); + + if (checksum != chunk.Checksum) + { + chunk.Return(); + throw new Exception("declared checksum doesn't match payload checksum"); + } + + return chunk; + } + } + + // Client implementation: + // + // Sends randomly generated data segments and validates data echoed back by the server. + // Applies backpressure if the difference between sent and received segments is too large. + public sealed class StressClient : SslClientBase + { + public StressClient(Configuration config) : base(config) { } + + protected override async Task HandleConnection(int workerId, SslStream stream, TcpClient client, Random random, CancellationToken token) + { + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); + if (_config.ConnectionLifetime != null) cts.CancelAfter(_config.ConnectionLifetime.Value); + + long messagesInFlight = 0; + + await Utils.TaskExtensions.WhenAllCancelOnFirstException(cts.Token, Sender, Receiver); + + async Task Sender(CancellationToken token) + { + var serializer = new DataSegmentSerializer(); + + while (!token.IsCancellationRequested) + { + await ApplyBackpressure(token); + + DataSegment chunk = DataSegment.CreateRandom(random, _config.MaxBufferLength); + try + { + await serializer.SerializeAsync(stream, chunk, random); + stream.WriteByte((byte)'\n'); + await stream.FlushAsync(); + Interlocked.Increment(ref messagesInFlight); + } + finally + { + chunk.Return(); + } + } + + // write an empty line to signal completion to the server + stream.WriteByte((byte)'\n'); + await stream.FlushAsync(); + await Task.Delay(1000); + } + + async Task Receiver(CancellationToken token) + { + var serializer = new DataSegmentSerializer(); + await stream.ReadLinesUsingPipesAsync(Callback, token, separator: '\n'); + + Task Callback(ReadOnlySequence buffer) + { + // deserialize to validate the checksum, then discard + DataSegment chunk = serializer.Deserialize(buffer); + chunk.Return(); + Interlocked.Decrement(ref messagesInFlight); + return Task.CompletedTask; + } + } + + async Task ApplyBackpressure(CancellationToken token) + { + if (Volatile.Read(ref messagesInFlight) > 5000) + { + lock (Console.Out) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"worker #{workerId}: applying backpressure"); + Console.ResetColor(); + } + + while (!token.IsCancellationRequested && Volatile.Read(ref messagesInFlight) > 2000) + { + await Task.Delay(20); + } + } + } + } + } + + // Server implementation: + // + // Sets up a pipeline reader which validates checksums and echoes back data. + public sealed class StressServer : SslServerBase + { + public StressServer(Configuration config) : base(config) { } + + protected override async Task HandleConnection(SslStream sslStream, TcpClient client, CancellationToken token) + { + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); + + var serializer = new DataSegmentSerializer(); + await sslStream.ReadLinesUsingPipesAsync(Callback, cts.Token, separator: '\n'); + + async Task Callback(ReadOnlySequence buffer) + { + // got an empty line, client is closing the connection + if (buffer.Length == 0) + { + cts.Cancel(); + return; + } + + DataSegment chunk = serializer.Deserialize(buffer); + try + { + await serializer.SerializeAsync(sslStream, chunk); + sslStream.WriteByte((byte)'\n'); + await sslStream.FlushAsync(token); + } + finally + { + chunk.Return(); + } + } + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/ChecksumHelpers.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/ChecksumHelpers.cs new file mode 100644 index 000000000000..f8102575360d --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/ChecksumHelpers.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace SslStress.Utils +{ + // Adapted from https://github.com/dotnet/corefx/blob/41cd99d051102be4ed83f4f9105ae9e73aa48b7c/src/Common/tests/System/IO/Compression/CRC.cs + public static class CRC + { + // Table of CRCs of all 8-bit messages. + private static readonly ulong[] s_crc_table = new ulong[256]; + public const ulong InitialCrc = 0xffffffffL; + + // Flag: has the table been computed? Initially false. + private static bool s_crc_table_computed = false; + + // Make the table for a fast CRC. + // Derivative work of zlib -- https://github.com/madler/zlib/blob/master/crc32.c (hint: L108) + private static void make_crc_table() + { + ulong c; + int n, k; + + for (n = 0; n < 256; n++) + { + c = (ulong)n; + for (k = 0; k < 8; k++) + { + if ((c & 1) > 0) + c = 0xedb88320L ^ (c >> 1); + else + c >>= 1; + } + s_crc_table[n] = c; + } + s_crc_table_computed = true; + } + + // Update a running CRC with the bytes buf[0..len-1]--the CRC + // should be initialized to all 1's, and the transmitted value + // is the 1's complement of the final running CRC (see the + // crc() routine below)). + public static ulong update_crc(ulong crc, ReadOnlySpan buf) + { + ulong c = crc; + int n; + + if (!s_crc_table_computed) + make_crc_table(); + for (n = 0; n < buf.Length; n++) + { + c = s_crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); + } + return c; + } + + public static ulong CalculateCRC(ReadOnlySpan buf) => update_crc(InitialCrc, buf) ^ InitialCrc; + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/CountingStream.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/CountingStream.cs new file mode 100644 index 000000000000..ba81587e738e --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/CountingStream.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SslStress.Utils +{ + public class StreamCounter + { + public long bytesWritten = 0L; + public long bytesRead = 0L; + + public void Reset() + { + bytesWritten = 0L; + bytesRead = 0L; + } + + public StreamCounter Append(StreamCounter that) + { + bytesRead += that.bytesRead; + bytesWritten += that.bytesWritten; + return this; + } + + public StreamCounter Clone() => new StreamCounter() { bytesRead = bytesRead, bytesWritten = bytesWritten }; + } + + public class CountingStream : Stream + { + private readonly Stream _stream; + private readonly StreamCounter _counter; + + public CountingStream(Stream stream, StreamCounter counters) + { + _stream = stream; + _counter = counters; + } + + public override void Write(byte[] buffer, int offset, int count) + { + _stream.Write(buffer, offset, count); + Interlocked.Add(ref _counter.bytesWritten, count); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int read = _stream.Read(buffer, offset, count); + Interlocked.Add(ref _counter.bytesRead, read); + return read; + } + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + await _stream.WriteAsync(buffer, cancellationToken); + Interlocked.Add(ref _counter.bytesWritten, buffer.Length); + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int read = await _stream.ReadAsync(buffer, cancellationToken); + Interlocked.Add(ref _counter.bytesRead, read); + return read; + } + + // route everything else to the inner stream + + public override bool CanRead => _stream.CanRead; + + public override bool CanSeek => _stream.CanSeek; + + public override bool CanWrite => _stream.CanWrite; + + public override long Length => _stream.Length; + + public override long Position { get => _stream.Position; set => _stream.Position = value; } + + public override void Flush() => _stream.Flush(); + + public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin); + + public override void SetLength(long value) => _stream.SetLength(value); + + public override void Close() => _stream.Close(); + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/ErrorAggregator.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/ErrorAggregator.cs new file mode 100644 index 000000000000..7f83c4b04626 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/ErrorAggregator.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; + +namespace SslStress.Utils +{ + public interface IErrorType + { + string ErrorMessage { get; } + + IReadOnlyCollection<(DateTime timestamp, string? metadata)> Occurrences { get; } + } + + public sealed class ErrorAggregator + { + private readonly ConcurrentDictionary<(Type exception, string message, string callSite)[], ErrorType> _failureTypes; + + public ErrorAggregator() + { + _failureTypes = new ConcurrentDictionary<(Type, string, string)[], ErrorType>(new StructuralEqualityComparer<(Type, string, string)[]>()); + } + + public int TotalErrorTypes => _failureTypes.Count; + public IReadOnlyCollection ErrorTypes => ErrorTypes.ToArray(); + public long TotalErrorCount => _failureTypes.Values.Select(c => (long)c.Occurrences.Count).Sum(); + + public void RecordError(Exception exception, string? metadata = null, DateTime? timestamp = null) + { + timestamp ??= DateTime.Now; + + (Type, string, string)[] key = ClassifyFailure(exception); + + ErrorType failureType = _failureTypes.GetOrAdd(key, _ => new ErrorType(exception.ToString())); + failureType.OccurencesQueue.Enqueue((timestamp.Value, metadata)); + + // classify exception according to type, message and callsite of itself and any inner exceptions + static (Type exception, string message, string callSite)[] ClassifyFailure(Exception exn) + { + var acc = new List<(Type exception, string message, string callSite)>(); + + for (Exception? e = exn; e != null;) + { + acc.Add((e.GetType(), e.Message ?? "", new StackTrace(e, true).GetFrame(0)?.ToString() ?? "")); + e = e.InnerException; + } + + return acc.ToArray(); + } + } + + public void PrintFailureTypes() + { + if (_failureTypes.Count == 0) + return; + + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"There were a total of {TotalErrorCount} failures classified into {TotalErrorTypes} different types:"); + Console.WriteLine(); + Console.ResetColor(); + + int i = 0; + foreach (ErrorType failure in _failureTypes.Values.OrderByDescending(x => x.Occurrences.Count)) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Failure Type {++i}/{_failureTypes.Count}:"); + Console.ResetColor(); + Console.WriteLine(failure.ErrorMessage); + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Yellow; + foreach (IGrouping grouping in failure.Occurrences.GroupBy(o => o.metadata)) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write($"\t{(grouping.Key ?? "").PadRight(30)}"); + Console.ResetColor(); + Console.ForegroundColor = ConsoleColor.Red; + Console.Write("Fail: "); + Console.ResetColor(); + Console.Write(grouping.Count()); + Console.WriteLine($"\tTimestamps: {string.Join(", ", grouping.Select(x => x.timestamp.ToString("HH:mm:ss")))}"); + } + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write("\t TOTAL".PadRight(31)); + Console.ResetColor(); + Console.ForegroundColor = ConsoleColor.Red; + Console.Write($"Fail: "); + Console.ResetColor(); + Console.WriteLine(TotalErrorTypes); + Console.WriteLine(); + } + } + + /// Aggregate view of a particular stress failure type + private sealed class ErrorType : IErrorType + { + public string ErrorMessage { get; } + public ConcurrentQueue<(DateTime, string?)> OccurencesQueue = new ConcurrentQueue<(DateTime, string?)>(); + + public ErrorType(string errorText) + { + ErrorMessage = errorText; + } + + public IReadOnlyCollection<(DateTime timestamp, string? metadata)> Occurrences => OccurencesQueue; + } + + private class StructuralEqualityComparer : IEqualityComparer where T : IStructuralEquatable + { + public bool Equals(T left, T right) => left.Equals(right, StructuralComparisons.StructuralEqualityComparer); + public int GetHashCode(T value) => value.GetHashCode(StructuralComparisons.StructuralEqualityComparer); + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/HumanReadableByteSizeFormatter.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/HumanReadableByteSizeFormatter.cs new file mode 100644 index 000000000000..864f335361ab --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/HumanReadableByteSizeFormatter.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace SslStress.Utils +{ + public static class HumanReadableByteSizeFormatter + { + private static readonly string[] s_suffixes = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" }; + + public static string Format(long byteCount) + { + // adapted from https://stackoverflow.com/a/4975942 + if (byteCount == 0) + { + return $"0{s_suffixes[0]}"; + } + + int position = (int)Math.Floor(Math.Log(Math.Abs(byteCount), 1024)); + double renderedValue = byteCount / Math.Pow(1024, position); + return $"{renderedValue:0.#}{s_suffixes[position]}"; + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/PipeExtensions.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/PipeExtensions.cs new file mode 100644 index 000000000000..4c6f62c98175 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/PipeExtensions.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +namespace SslStress.Utils +{ + public static class PipeExtensions + { + // Adapted from https://devblogs.microsoft.com/dotnet/system-io-pipelines-high-performance-io-in-net/ + public static async Task ReadLinesUsingPipesAsync(this Stream stream, Func, Task> callback, CancellationToken token = default, char separator = '\n') + { + var pipe = new Pipe(); + PipeWriter writer = pipe.Writer; + PipeReader reader = pipe.Reader; + + try + { + await TaskExtensions.WhenAllCancelOnFirstException(token, FillPipeAsync, ReadPipeAsync); + } + catch when (token.IsCancellationRequested) + { + + } + + async Task FillPipeAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + Memory memory = writer.GetMemory(512); + int bytesRead = await stream.ReadAsync(memory, token); + + if (bytesRead == 0) + { + break; + } + + writer.Advance(bytesRead); + FlushResult result = await writer.FlushAsync(token); + + if (result.IsCompleted) + { + break; + } + } + + writer.Complete(); + } + + async Task ReadPipeAsync(CancellationToken token) + { + try + { + while (!token.IsCancellationRequested) + { + ReadResult result = await reader.ReadAsync(token); + ReadOnlySequence buffer = result.Buffer; + SequencePosition? position; + + do + { + position = buffer.PositionOf((byte)separator); + + if (position != null) + { + await callback(buffer.Slice(0, position.Value)); + buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); + } + } + while (position != null); + + reader.AdvanceTo(buffer.Start, buffer.End); + + if (result.IsCompleted) + { + break; + } + } + } + finally + { + reader.Complete(); + } + } + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/RandomHelpers.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/RandomHelpers.cs new file mode 100644 index 000000000000..ba686a9635f8 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/RandomHelpers.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace SslStress.Utils +{ + public static class RandomHelpers + { + public static Random NextRandom(this Random random) => new Random(Seed: random.Next()); + + public static bool NextBoolean(this Random random, double probability = 0.5) + { + if (probability < 0 || probability > 1) + throw new ArgumentOutOfRangeException(nameof(probability)); + + return random.NextDouble() < probability; + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/TaskExtensions.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/TaskExtensions.cs new file mode 100644 index 000000000000..3e4fb4271542 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/TaskExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SslStress.Utils +{ + public static class TaskExtensions + { + // Starts and awaits a collection of tasks while ensuring cancellation has been signaled + // whenever one of them has raised an unhandled exception + public static async Task WhenAllCancelOnFirstException(CancellationToken token, params Func[] tasks) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); + await Task.WhenAll(tasks.Select(RunOne)); + + async Task RunOne(Func task) + { + try + { + await Task.Run(() => task(cts.Token)); + } + catch + { + cts.Cancel(); + throw; + } + } + } + } +} diff --git a/src/System.Net.Security/tests/StressTests/SslStress/global.json b/src/System.Net.Security/tests/StressTests/SslStress/global.json new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/global.json @@ -0,0 +1 @@ +{} \ No newline at end of file From f7438384dc3452c5b40ebc1ac1e9e38581b339b1 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 12 Nov 2019 15:28:31 +0000 Subject: [PATCH 02/12] fix server shutdown deadlock --- .../tests/StressTests/SslStress/SslServerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs index 7ac0858030a2..79ae97e12c6b 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs @@ -64,8 +64,8 @@ public void Start() public async ValueTask StopAsync() { _cts.Cancel(); - await _serverTask.Value; _listener.Stop(); + await _serverTask.Value; } public Task Task From 3950f03084316098ced0b58f43c87d5136c614cf Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 12 Nov 2019 15:31:19 +0000 Subject: [PATCH 03/12] fix formatting issues --- src/System.Net.Security/tests/StressTests/SslStress/Program.cs | 1 + .../tests/StressTests/SslStress/SslClientBase.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs index db61d4af734b..19284b0fc960 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs @@ -61,6 +61,7 @@ private static async Task Run(Configuration config) { // Start the client. Console.WriteLine($"Starting {config.MaxConnections} client workers."); + Console.WriteLine(); client = new StressClient(config); client.Start(); diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs index be98a173de05..dd863b51fdb0 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs @@ -136,7 +136,7 @@ public void PrintFinalReport() lock (Console.Out) { Console.ForegroundColor = ConsoleColor.Magenta; - Console.WriteLine("HttpStress Run Final Report"); + Console.WriteLine("SslStress Run Final Report"); Console.WriteLine(); _aggregator.PrintCurrentResults(_stopwatch.Elapsed); From b31fc4bc88981e1bb906e70c8c3e942591ad0069 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 12 Nov 2019 15:34:23 +0000 Subject: [PATCH 04/12] fix cli issue --- src/System.Net.Security/tests/StressTests/SslStress/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs index 19284b0fc960..4b63db04b13e 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs @@ -127,7 +127,7 @@ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configura RunMode = parseResult.ValueForOption("-m"), MaxConnections = parseResult.ValueForOption("-n"), ServerEndpoint = IPEndPoint.Parse(parseResult.ValueForOption("-e")), - MaxExecutionTime = parseResult.ValueForOption("-M").Select(TimeSpan.FromMinutes), + MaxExecutionTime = parseResult.ValueForOption("-t").Select(TimeSpan.FromMinutes), MaxBufferLength = parseResult.ValueForOption("-l"), ConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption("-L")), DisplayInterval = TimeSpan.FromSeconds(parseResult.ValueForOption("-i")), From 798de3c084e354c253e72446523cda2fd092ba9b Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 12 Nov 2019 15:35:12 +0000 Subject: [PATCH 05/12] cli issue --- src/System.Net.Security/tests/StressTests/SslStress/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs index 4b63db04b13e..2222f13af44d 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs @@ -100,7 +100,7 @@ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configura { var cmd = new RootCommand(); cmd.AddOption(new Option(new[] { "--help", "-h" }, "Display this help text.")); - cmd.AddOption(new Option(new[] { "--mode", "-m" }, "Stress suite execution mode. Defaults to Both.") { Argument = new Argument("runMode", RunMode.both) }); + cmd.AddOption(new Option(new[] { "--mode", "-m" }, "Stress suite execution mode. Defaults to 'both'.") { Argument = new Argument("runMode", RunMode.both) }); cmd.AddOption(new Option(new[] { "--num-connections", "-n" }, "Max number of connections to open concurrently.") { Argument = new Argument("connections", Math.Max(Environment.ProcessorCount / 2, 2)) }); cmd.AddOption(new Option(new[] { "--server-endpoint", "-e" }, "Endpoint to bind to if server, endpoint to listen to if client.") { Argument = new Argument("ipEndpoint", "127.0.0.1:5002") }); cmd.AddOption(new Option(new[] { "--max-execution-time", "-t" }, "Maximum stress execution time, in minutes. Defaults to infinity.") { Argument = new Argument("minutes", null) }); From 7870defddb4081c0ac2cf8cf19b1f31c3a16b7d8 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 13 Nov 2019 11:22:25 +0000 Subject: [PATCH 06/12] address feedback --- .../tests/StressTests/SslStress/Program.cs | 2 +- .../SslClientBase.StressResultAggregator.cs | 3 +- .../StressTests/SslStress/SslClientBase.cs | 2 +- .../StressTests/SslStress/SslServerBase.cs | 3 +- .../StressTests/SslStress/StressOperations.cs | 14 ++-- .../SslStress/Utils/PipeExtensions.cs | 67 ++++++------------- .../SslStress/Utils/TaskExtensions.cs | 29 ++++++-- 7 files changed, 56 insertions(+), 64 deletions(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs index 2222f13af44d..6c1aa6b399ed 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs @@ -101,7 +101,7 @@ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configura var cmd = new RootCommand(); cmd.AddOption(new Option(new[] { "--help", "-h" }, "Display this help text.")); cmd.AddOption(new Option(new[] { "--mode", "-m" }, "Stress suite execution mode. Defaults to 'both'.") { Argument = new Argument("runMode", RunMode.both) }); - cmd.AddOption(new Option(new[] { "--num-connections", "-n" }, "Max number of connections to open concurrently.") { Argument = new Argument("connections", Math.Max(Environment.ProcessorCount / 2, 2)) }); + cmd.AddOption(new Option(new[] { "--num-connections", "-n" }, "Max number of connections to open concurrently.") { Argument = new Argument("connections", Environment.ProcessorCount) }); cmd.AddOption(new Option(new[] { "--server-endpoint", "-e" }, "Endpoint to bind to if server, endpoint to listen to if client.") { Argument = new Argument("ipEndpoint", "127.0.0.1:5002") }); cmd.AddOption(new Option(new[] { "--max-execution-time", "-t" }, "Maximum stress execution time, in minutes. Defaults to infinity.") { Argument = new Argument("minutes", null) }); cmd.AddOption(new Option(new[] { "--max-buffer-length", "-l" }, "Maximum buffer length to write on ssl stream. Defaults to 8192.") { Argument = new Argument("bytes", 8192) }); diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs index 2ca4a3161f3b..84370da15e64 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs @@ -55,6 +55,7 @@ public void RecordFailure(int workerId, Exception exn) { Console.ForegroundColor = ConsoleColor.DarkRed; Console.WriteLine($"Worker #{workerId}: unhandled exception: {exn}"); + Console.WriteLine(); Console.ResetColor(); } } @@ -151,7 +152,7 @@ public void PrintCurrentResults(TimeSpan elapsed) Console.ForegroundColor = ConsoleColor.DarkMagenta; Console.Write($"\tRx: "); Console.ResetColor(); - Console.Write(FmtBytes(counters.Select(c => c.total.bytesWritten).Sum())); + Console.Write(FmtBytes(counters.Select(c => c.total.bytesRead).Sum())); Console.WriteLine(); Console.WriteLine(); diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs index dd863b51fdb0..3e958d5f75bc 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs @@ -109,7 +109,7 @@ async Task RunSingleWorker(int workerId, Random random) _aggregator.RecordSuccess(workerId); } - catch (Exception) when (_cts.IsCancellationRequested) + catch (OperationCanceledException) when (_cts.IsCancellationRequested) { _aggregator.RecordCancellation(workerId); } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs index 79ae97e12c6b..fb42ead55b32 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs @@ -95,7 +95,7 @@ async Task RunSingleWorker() using SslStream stream = await EstablishSslStream(client.GetStream(), _cts.Token); await HandleConnection(stream, client, _cts.Token); } - catch when (_cts.IsCancellationRequested) + catch (OperationCanceledException) when (_cts.IsCancellationRequested) { } @@ -105,6 +105,7 @@ async Task RunSingleWorker() { Console.ForegroundColor = ConsoleColor.DarkRed; Console.WriteLine($"Server: unhandled exception: {e}"); + Console.WriteLine(); Console.ResetColor(); } } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs index bcb5108cc875..0f3f1de69d8e 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs @@ -68,22 +68,22 @@ public class DataSegmentSerializer private readonly byte[] _buffer = new byte[32]; private readonly char[] _charBuffer = new char[32]; - public async Task SerializeAsync(Stream stream, DataSegment segment, Random? random = null) + public async Task SerializeAsync(Stream stream, DataSegment segment, Random? random = null, CancellationToken token = default) { // length int numsize = s_encoding.GetBytes(segment.Length.ToString(), _buffer); - await stream.WriteAsync(_buffer.AsMemory().Slice(0, numsize)); + await stream.WriteAsync(_buffer.AsMemory().Slice(0, numsize), token); stream.WriteByte((byte)','); // checksum numsize = s_encoding.GetBytes(segment.Checksum.ToString(), _buffer); - await stream.WriteAsync(_buffer.AsMemory().Slice(0, numsize)); + await stream.WriteAsync(_buffer.AsMemory().Slice(0, numsize), token); stream.WriteByte((byte)','); // payload Memory source = segment.AsMemory(); // write the entire segment outright if not given random instance if (random == null) { - await stream.WriteAsync(source); + await stream.WriteAsync(source, token); return; } // randomize chunking otherwise @@ -103,7 +103,7 @@ public async Task SerializeAsync(Stream stream, DataSegment segment, Random? ran if (random.NextBoolean(probability: 0.9)) { - await stream.WriteAsync(chunk); + await stream.WriteAsync(chunk, token); } else { @@ -113,7 +113,7 @@ public async Task SerializeAsync(Stream stream, DataSegment segment, Random? ran if (random.NextBoolean(probability: 0.3)) { - await stream.FlushAsync(); + await stream.FlushAsync(token); } } } @@ -178,7 +178,7 @@ protected override async Task HandleConnection(int workerId, SslStream stream, T long messagesInFlight = 0; - await Utils.TaskExtensions.WhenAllCancelOnFirstException(cts.Token, Sender, Receiver); + await Utils.TaskExtensions.WhenAllThrowOnFirstException(cts.Token, Sender, Receiver); async Task Sender(CancellationToken token) { diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/PipeExtensions.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/PipeExtensions.cs index 4c6f62c98175..48cfb88e88e7 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Utils/PipeExtensions.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/PipeExtensions.cs @@ -17,76 +17,49 @@ public static class PipeExtensions public static async Task ReadLinesUsingPipesAsync(this Stream stream, Func, Task> callback, CancellationToken token = default, char separator = '\n') { var pipe = new Pipe(); - PipeWriter writer = pipe.Writer; - PipeReader reader = pipe.Reader; try { - await TaskExtensions.WhenAllCancelOnFirstException(token, FillPipeAsync, ReadPipeAsync); + await TaskExtensions.WhenAllThrowOnFirstException(token, FillPipeAsync, ReadPipeAsync); } - catch when (token.IsCancellationRequested) + catch (OperationCanceledException) when (token.IsCancellationRequested) { } async Task FillPipeAsync(CancellationToken token) { - while (!token.IsCancellationRequested) - { - Memory memory = writer.GetMemory(512); - int bytesRead = await stream.ReadAsync(memory, token); - - if (bytesRead == 0) - { - break; - } - - writer.Advance(bytesRead); - FlushResult result = await writer.FlushAsync(token); - - if (result.IsCompleted) - { - break; - } - } - - writer.Complete(); + await stream.CopyToAsync(pipe.Writer, token); + pipe.Writer.Complete(); } async Task ReadPipeAsync(CancellationToken token) { - try + while (!token.IsCancellationRequested) { - while (!token.IsCancellationRequested) + ReadResult result = await pipe.Reader.ReadAsync(token); + ReadOnlySequence buffer = result.Buffer; + SequencePosition? position; + + do { - ReadResult result = await reader.ReadAsync(token); - ReadOnlySequence buffer = result.Buffer; - SequencePosition? position; + position = buffer.PositionOf((byte)separator); - do + if (position != null) { - position = buffer.PositionOf((byte)separator); - - if (position != null) - { - await callback(buffer.Slice(0, position.Value)); - buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); - } + await callback(buffer.Slice(0, position.Value)); + buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); } - while (position != null); + } + while (position != null); - reader.AdvanceTo(buffer.Start, buffer.End); + pipe.Reader.AdvanceTo(buffer.Start, buffer.End); - if (result.IsCompleted) - { - break; - } + if (result.IsCompleted) + { + break; } } - finally - { - reader.Complete(); - } } } } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/TaskExtensions.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/TaskExtensions.cs index 3e4fb4271542..5aa25423d6a9 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Utils/TaskExtensions.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/TaskExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; @@ -11,23 +12,39 @@ namespace SslStress.Utils { public static class TaskExtensions { - // Starts and awaits a collection of tasks while ensuring cancellation has been signaled - // whenever one of them has raised an unhandled exception - public static async Task WhenAllCancelOnFirstException(CancellationToken token, params Func[] tasks) + + /// + /// Starts and awaits a collection of cancellable tasks. + /// Will surface the first exception that has occured (instead of AggregateException) + /// and trigger cancellation for all sibling tasks. + /// + /// + /// + /// + public static async Task WhenAllThrowOnFirstException(CancellationToken token, params Func[] tasks) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); + Exception? firstException = null; + await Task.WhenAll(tasks.Select(RunOne)); + if (firstException != null) + { + ExceptionDispatchInfo.Capture(firstException).Throw(); + } + async Task RunOne(Func task) { try { await Task.Run(() => task(cts.Token)); } - catch + catch (Exception e) { - cts.Cancel(); - throw; + if (Interlocked.CompareExchange(ref firstException, e, null) == null) + { + cts.Cancel(); + } } } } From bdcab6f95626c69e141a8d6e8674780a570bf6a5 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 13 Nov 2019 12:03:08 +0000 Subject: [PATCH 07/12] make serialization cancellable --- .../tests/StressTests/SslStress/StressOperations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs index 0f3f1de69d8e..60c0c2a73521 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs @@ -269,7 +269,7 @@ async Task Callback(ReadOnlySequence buffer) DataSegment chunk = serializer.Deserialize(buffer); try { - await serializer.SerializeAsync(sslStream, chunk); + await serializer.SerializeAsync(sslStream, chunk, token: token); sslStream.WriteByte((byte)'\n'); await sslStream.FlushAsync(token); } From 2ac385bd1350f570f31b93e80256b878d16f305e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 13 Nov 2019 12:46:53 +0000 Subject: [PATCH 08/12] fix SslServer disposal semantics --- .../StressTests/SslStress/SslServerBase.cs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs index fb42ead55b32..c28834ac4d78 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs @@ -64,7 +64,6 @@ public void Start() public async ValueTask StopAsync() { _cts.Cancel(); - _listener.Stop(); await _serverTask.Value; } @@ -79,11 +78,18 @@ public Task Task public ValueTask DisposeAsync() => StopAsync(); - private Task StartCore() + private async Task StartCore() { _listener.Start(); IEnumerable workers = Enumerable.Range(1, _config.MaxConnections).Select(_ => RunSingleWorker()); - return Task.WhenAll(workers); + try + { + await Task.WhenAll(workers); + } + finally + { + _listener.Stop(); + } async Task RunSingleWorker() { @@ -91,7 +97,7 @@ async Task RunSingleWorker() { try { - using TcpClient client = await _listener.AcceptTcpClientAsync(); + using TcpClient client = await AcceptTcpClientAsync(_cts.Token); using SslStream stream = await EstablishSslStream(client.GetStream(), _cts.Token); await HandleConnection(stream, client, _cts.Token); } @@ -111,6 +117,22 @@ async Task RunSingleWorker() } } } + + async Task AcceptTcpClientAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + if (_listener.Pending()) + { + return await _listener.AcceptTcpClientAsync(); + } + + await Task.Delay(20); + } + + token.ThrowIfCancellationRequested(); + throw new Exception("internal error"); + } } protected virtual X509Certificate2 CreateSelfSignedCertificate() From 95525ffb8eabc3436af1590a184fab1961f2c503 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 13 Nov 2019 16:08:29 +0000 Subject: [PATCH 09/12] add randomized cancellation to connections --- .../StressTests/SslStress/Configuration.cs | 3 +- .../tests/StressTests/SslStress/Program.cs | 38 ++++++---- .../SslClientBase.StressResultAggregator.cs | 11 +-- .../StressTests/SslStress/SslClientBase.cs | 12 ++- .../StressTests/SslStress/StressOperations.cs | 74 ++++++++++++------- .../SslStress/Utils/MiscHelpers.cs | 11 +++ 6 files changed, 94 insertions(+), 55 deletions(-) create mode 100644 src/System.Net.Security/tests/StressTests/SslStress/Utils/MiscHelpers.cs diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs b/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs index ac7af6993f80..15a05efcee27 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs @@ -19,6 +19,7 @@ public class Configuration public int MaxBufferLength { get; set; } public TimeSpan? MaxExecutionTime { get; set; } public TimeSpan DisplayInterval { get; set; } - public TimeSpan? ConnectionLifetime { get; set; } + public TimeSpan MinConnectionLifetime { get; set; } + public TimeSpan MaxConnectionLifetime { get; set; } } } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs index 6c1aa6b399ed..a34cdf778b23 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs @@ -9,6 +9,7 @@ using System.Net; using System.Reflection; using System.Threading.Tasks; +using SslStress.Utils; namespace SslStress { @@ -41,7 +42,8 @@ private static async Task Run(Configuration config) Console.WriteLine(" Server Endpoint: " + config.ServerEndpoint); Console.WriteLine(" Concurrency: " + config.MaxConnections); Console.WriteLine(" Max Execution Time: " + ((config.MaxExecutionTime != null) ? config.MaxExecutionTime.Value.ToString() : "infinite")); - Console.WriteLine(" Max Conn. Lifetime: " + config.ConnectionLifetime); + Console.WriteLine(" Min Conn. Lifetime: " + config.MinConnectionLifetime); + Console.WriteLine(" Max Conn. Lifetime: " + config.MaxConnectionLifetime); Console.WriteLine(" Random Seed: " + config.RandomSeed); Console.WriteLine(); @@ -103,9 +105,10 @@ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configura cmd.AddOption(new Option(new[] { "--mode", "-m" }, "Stress suite execution mode. Defaults to 'both'.") { Argument = new Argument("runMode", RunMode.both) }); cmd.AddOption(new Option(new[] { "--num-connections", "-n" }, "Max number of connections to open concurrently.") { Argument = new Argument("connections", Environment.ProcessorCount) }); cmd.AddOption(new Option(new[] { "--server-endpoint", "-e" }, "Endpoint to bind to if server, endpoint to listen to if client.") { Argument = new Argument("ipEndpoint", "127.0.0.1:5002") }); - cmd.AddOption(new Option(new[] { "--max-execution-time", "-t" }, "Maximum stress execution time, in minutes. Defaults to infinity.") { Argument = new Argument("minutes", null) }); - cmd.AddOption(new Option(new[] { "--max-buffer-length", "-l" }, "Maximum buffer length to write on ssl stream. Defaults to 8192.") { Argument = new Argument("bytes", 8192) }); - cmd.AddOption(new Option(new[] { "--connection-lifetime", "-L" }, "Maximum stress execution time, in seconds. Defaults to 120 seconds.") { Argument = new Argument("minutes", 120) }); + cmd.AddOption(new Option(new[] { "--max-execution-time", "-t" }, "Maximum stress suite execution time, in minutes. Defaults to infinity.") { Argument = new Argument("minutes", null) }); + cmd.AddOption(new Option(new[] { "--max-buffer-length", "-b" }, "Maximum buffer length to write on ssl stream. Defaults to 8192.") { Argument = new Argument("bytes", 8192) }); + cmd.AddOption(new Option(new[] { "--min-connection-lifetime", "-l" }, "Minimum duration for a single connection, in seconds. Defaults to 5 seconds.") { Argument = new Argument("minutes", 5) }); + cmd.AddOption(new Option(new[] { "--max-connection-lifetime", "-L" }, "Maximum duration for a single connection, in seconds. Defaults to 120 seconds.") { Argument = new Argument("minutes", 120) }); cmd.AddOption(new Option(new[] { "--display-interval", "-i" }, "Client stats display interval, in seconds. Defaults to 5 seconds.") { Argument = new Argument("seconds", 5) }); cmd.AddOption(new Option(new[] { "--seed", "-s" }, "Seed for generating pseudo-random parameters. Also depends on the -n argument.") { Argument = new Argument("seed", (new Random().Next())) }); @@ -116,8 +119,7 @@ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configura { Console.WriteLine(error); } - Console.WriteLine(); - new HelpBuilder(new SystemConsole()).Write(cmd); + WriteHelpText(); config = null; return false; } @@ -127,19 +129,29 @@ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configura RunMode = parseResult.ValueForOption("-m"), MaxConnections = parseResult.ValueForOption("-n"), ServerEndpoint = IPEndPoint.Parse(parseResult.ValueForOption("-e")), - MaxExecutionTime = parseResult.ValueForOption("-t").Select(TimeSpan.FromMinutes), - MaxBufferLength = parseResult.ValueForOption("-l"), - ConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption("-L")), + MaxExecutionTime = parseResult.ValueForOption("-t")?.Pipe(TimeSpan.FromMinutes), + MaxBufferLength = parseResult.ValueForOption("-b"), + MinConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption("-l")), + MaxConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption("-L")), DisplayInterval = TimeSpan.FromSeconds(parseResult.ValueForOption("-i")), RandomSeed = parseResult.ValueForOption("-s"), }; + if (config.MaxConnectionLifetime < config.MinConnectionLifetime) + { + Console.WriteLine("Max connection lifetime should be greater than or equal to min connection lifetime"); + WriteHelpText(); + config = null; + return false; + } + return true; - } - private static S? Select(this T? value, Func mapper) where T : struct where S : struct - { - return value is null ? null : new S?(mapper(value.Value)); + void WriteHelpText() + { + Console.WriteLine(); + new HelpBuilder(new SystemConsole()).Write(cmd); + } } } } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs index 84370da15e64..34aadae3c6e2 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs @@ -17,7 +17,7 @@ public abstract partial class SslClientBase private class StressResultAggregator { private long _totalConnections = 0; - private readonly long[] _successes, _failures, _cancellations; + private readonly long[] _successes, _failures; private readonly ErrorAggregator _errors = new ErrorAggregator(); private readonly StreamCounter[] _currentCounters; private readonly StreamCounter[] _aggregateCounters; @@ -28,12 +28,10 @@ public StressResultAggregator(int workerCount) _aggregateCounters = Enumerable.Range(0, workerCount).Select(_ => new StreamCounter()).ToArray(); _successes = new long[workerCount]; _failures = new long[workerCount]; - _cancellations = new long[workerCount]; } public long TotalConnections => _totalConnections; public long TotalFailures => _failures.Sum(); - public long TotalCancellations => _cancellations.Sum(); public StreamCounter GetCounters(int workerId) => _currentCounters[workerId]; @@ -60,13 +58,6 @@ public void RecordFailure(int workerId, Exception exn) } } - public void RecordCancellation(int workerId) - { - _cancellations[workerId]++; - Interlocked.Increment(ref _totalConnections); - UpdateCounters(workerId); - } - private void UpdateCounters(int workerId) { // need to synchronize with GetCounterView to avoid reporting bad data diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs index 3e958d5f75bc..d34f2f4999f6 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs @@ -99,19 +99,23 @@ async Task RunSingleWorker(int workerId, Random random) for (long testId = 0; !_cts.IsCancellationRequested; testId++) { + TimeSpan duration = _config.MinConnectionLifetime + random.NextDouble() * (_config.MaxConnectionLifetime - _config.MinConnectionLifetime); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + cts.CancelAfter(duration); + try { using var client = new TcpClient(); await client.ConnectAsync(_config.ServerEndpoint.Address, _config.ServerEndpoint.Port); var stream = new CountingStream(client.GetStream(), counter); - using SslStream sslStream = await EstablishSslStream(stream, random, _cts.Token); - await HandleConnection(workerId, sslStream, client, random, _cts.Token); + using SslStream sslStream = await EstablishSslStream(stream, random, cts.Token); + await HandleConnection(workerId, sslStream, client, random, cts.Token); _aggregator.RecordSuccess(workerId); } - catch (OperationCanceledException) when (_cts.IsCancellationRequested) + catch (OperationCanceledException) when (cts.IsCancellationRequested) { - _aggregator.RecordCancellation(workerId); + _aggregator.RecordSuccess(workerId); } catch (Exception e) { diff --git a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs index 60c0c2a73521..3bd7899ee217 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs @@ -60,6 +60,11 @@ public static DataSegment CreateRandom(Random random, int maxLength) .ToArray(); } + public class ChecksumMismatchException : Exception + { + public ChecksumMismatchException(string message) : base(message) { } + } + // Serializes data segment using the following format: ,, public class DataSegmentSerializer { @@ -156,7 +161,7 @@ public DataSegment Deserialize(ReadOnlySequence buffer) if (checksum != chunk.Checksum) { chunk.Return(); - throw new Exception("declared checksum doesn't match payload checksum"); + throw new ChecksumMismatchException("declared checksum doesn't match payload checksum"); } return chunk; @@ -173,39 +178,42 @@ public StressClient(Configuration config) : base(config) { } protected override async Task HandleConnection(int workerId, SslStream stream, TcpClient client, Random random, CancellationToken token) { - using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(token); - if (_config.ConnectionLifetime != null) cts.CancelAfter(_config.ConnectionLifetime.Value); - long messagesInFlight = 0; - await Utils.TaskExtensions.WhenAllThrowOnFirstException(cts.Token, Sender, Receiver); + await Utils.TaskExtensions.WhenAllThrowOnFirstException(token, Sender, Receiver); - async Task Sender(CancellationToken token) + async Task Sender(CancellationToken taskToken) { var serializer = new DataSegmentSerializer(); - while (!token.IsCancellationRequested) + try { - await ApplyBackpressure(token); - - DataSegment chunk = DataSegment.CreateRandom(random, _config.MaxBufferLength); - try - { - await serializer.SerializeAsync(stream, chunk, random); - stream.WriteByte((byte)'\n'); - await stream.FlushAsync(); - Interlocked.Increment(ref messagesInFlight); - } - finally + while (!taskToken.IsCancellationRequested) { - chunk.Return(); + await ApplyBackpressure(taskToken); + + DataSegment chunk = DataSegment.CreateRandom(random, _config.MaxBufferLength); + try + { + await serializer.SerializeAsync(stream, chunk, random, token); + stream.WriteByte((byte)'\n'); + await stream.FlushAsync(token); + Interlocked.Increment(ref messagesInFlight); + } + finally + { + chunk.Return(); + } } } - - // write an empty line to signal completion to the server - stream.WriteByte((byte)'\n'); - await stream.FlushAsync(); - await Task.Delay(1000); + finally + { + // write an empty line to signal completion to the server + stream.WriteByte((byte)'\n'); + stream.WriteByte((byte)'\n'); + await stream.FlushAsync(); + await Task.Delay(1000); + } } async Task Receiver(CancellationToken token) @@ -231,6 +239,7 @@ async Task ApplyBackpressure(CancellationToken token) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"worker #{workerId}: applying backpressure"); + Console.WriteLine(); Console.ResetColor(); } @@ -266,16 +275,27 @@ async Task Callback(ReadOnlySequence buffer) return; } - DataSegment chunk = serializer.Deserialize(buffer); + DataSegment? chunk = null; try { - await serializer.SerializeAsync(sslStream, chunk, token: token); + chunk = serializer.Deserialize(buffer); + await serializer.SerializeAsync(sslStream, chunk.Value, token: token); sslStream.WriteByte((byte)'\n'); await sslStream.FlushAsync(token); } + catch (ChecksumMismatchException e) + { + // report as warning and continue + lock (Console.Out) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Server: {e.Message}"); + Console.ResetColor(); + } + } finally { - chunk.Return(); + chunk?.Return(); } } } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/MiscHelpers.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/MiscHelpers.cs new file mode 100644 index 000000000000..9ec11ce6073f --- /dev/null +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/MiscHelpers.cs @@ -0,0 +1,11 @@ +using System; + +namespace SslStress.Utils +{ + public static class MiscHelpers + { + // help transform `(foo != null) ? Bar(foo) : null` expressions into `foo?.Select(Bar)` + public static S Pipe(this T value, Func mapper) => mapper(value); + public static void Pipe(this T value, Action body) => body(value); + } +} From 9dd1f2a763fe0d15ef02168dca6239770dd57f76 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 13 Nov 2019 16:25:18 +0000 Subject: [PATCH 10/12] use PascalCase --- .../SslClientBase.StressResultAggregator.cs | 8 +++---- .../SslStress/Utils/CountingStream.cs | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs index 34aadae3c6e2..44908f23aa72 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs @@ -115,11 +115,11 @@ public void PrintCurrentResults(TimeSpan elapsed) Console.ForegroundColor = ConsoleColor.DarkBlue; Console.Write($"\tTx: "); Console.ResetColor(); - Console.Write(FmtBytes(counters[i].total.bytesWritten)); + Console.Write(FmtBytes(counters[i].total.BytesWritten)); Console.ForegroundColor = ConsoleColor.DarkMagenta; Console.Write($"\tRx: "); Console.ResetColor(); - Console.Write(FmtBytes(counters[i].total.bytesRead)); + Console.Write(FmtBytes(counters[i].total.BytesRead)); Console.WriteLine(); } @@ -139,11 +139,11 @@ public void PrintCurrentResults(TimeSpan elapsed) Console.ForegroundColor = ConsoleColor.DarkBlue; Console.Write("\tTx: "); Console.ResetColor(); - Console.Write(FmtBytes(counters.Select(c => c.total.bytesWritten).Sum())); + Console.Write(FmtBytes(counters.Select(c => c.total.BytesWritten).Sum())); Console.ForegroundColor = ConsoleColor.DarkMagenta; Console.Write($"\tRx: "); Console.ResetColor(); - Console.Write(FmtBytes(counters.Select(c => c.total.bytesRead).Sum())); + Console.Write(FmtBytes(counters.Select(c => c.total.BytesRead).Sum())); Console.WriteLine(); Console.WriteLine(); diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Utils/CountingStream.cs b/src/System.Net.Security/tests/StressTests/SslStress/Utils/CountingStream.cs index ba81587e738e..80f9d3052258 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Utils/CountingStream.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Utils/CountingStream.cs @@ -11,23 +11,23 @@ namespace SslStress.Utils { public class StreamCounter { - public long bytesWritten = 0L; - public long bytesRead = 0L; + public long BytesWritten = 0L; + public long BytesRead = 0L; public void Reset() { - bytesWritten = 0L; - bytesRead = 0L; + BytesWritten = 0L; + BytesRead = 0L; } public StreamCounter Append(StreamCounter that) { - bytesRead += that.bytesRead; - bytesWritten += that.bytesWritten; + BytesRead += that.BytesRead; + BytesWritten += that.BytesWritten; return this; } - public StreamCounter Clone() => new StreamCounter() { bytesRead = bytesRead, bytesWritten = bytesWritten }; + public StreamCounter Clone() => new StreamCounter() { BytesRead = BytesRead, BytesWritten = BytesWritten }; } public class CountingStream : Stream @@ -44,26 +44,26 @@ public CountingStream(Stream stream, StreamCounter counters) public override void Write(byte[] buffer, int offset, int count) { _stream.Write(buffer, offset, count); - Interlocked.Add(ref _counter.bytesWritten, count); + Interlocked.Add(ref _counter.BytesWritten, count); } public override int Read(byte[] buffer, int offset, int count) { int read = _stream.Read(buffer, offset, count); - Interlocked.Add(ref _counter.bytesRead, read); + Interlocked.Add(ref _counter.BytesRead, read); return read; } public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { await _stream.WriteAsync(buffer, cancellationToken); - Interlocked.Add(ref _counter.bytesWritten, buffer.Length); + Interlocked.Add(ref _counter.BytesWritten, buffer.Length); } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { int read = await _stream.ReadAsync(buffer, cancellationToken); - Interlocked.Add(ref _counter.bytesRead, read); + Interlocked.Add(ref _counter.BytesRead, read); return read; } From 50f6f74d6da9dab25d14e50059cad425b7b3ccd3 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 13 Nov 2019 16:47:08 +0000 Subject: [PATCH 11/12] ensure all deserialization errors are handled properly by the server --- .../StressTests/SslStress/StressOperations.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs index 3bd7899ee217..5f86f8749d98 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs @@ -60,9 +60,9 @@ public static DataSegment CreateRandom(Random random, int maxLength) .ToArray(); } - public class ChecksumMismatchException : Exception + public class DataMismatchException : Exception { - public ChecksumMismatchException(string message) : base(message) { } + public DataMismatchException(string message) : base(message) { } } // Serializes data segment using the following format: ,, @@ -129,7 +129,7 @@ public DataSegment Deserialize(ReadOnlySequence buffer) SequencePosition? pos = buffer.PositionOf((byte)','); if (pos == null) { - throw new FormatException("should contain comma-separated values"); + throw new DataMismatchException("should contain comma-separated values"); } ReadOnlySequence lengthBytes = buffer.Slice(0, pos.Value); @@ -141,7 +141,7 @@ public DataSegment Deserialize(ReadOnlySequence buffer) pos = buffer.PositionOf((byte)','); if (pos == null) { - throw new FormatException("should contain comma-separated values"); + throw new DataMismatchException("should contain comma-separated values"); } ReadOnlySequence checksumBytes = buffer.Slice(0, pos.Value); @@ -152,7 +152,7 @@ public DataSegment Deserialize(ReadOnlySequence buffer) // payload if (length != (int)buffer.Length) { - throw new Exception("declared length does not match payload length"); + throw new DataMismatchException("declared length does not match payload length"); } var chunk = new DataSegment((int)buffer.Length); @@ -161,7 +161,7 @@ public DataSegment Deserialize(ReadOnlySequence buffer) if (checksum != chunk.Checksum) { chunk.Return(); - throw new ChecksumMismatchException("declared checksum doesn't match payload checksum"); + throw new DataMismatchException("declared checksum doesn't match payload checksum"); } return chunk; @@ -283,7 +283,7 @@ async Task Callback(ReadOnlySequence buffer) sslStream.WriteByte((byte)'\n'); await sslStream.FlushAsync(token); } - catch (ChecksumMismatchException e) + catch (DataMismatchException e) { // report as warning and continue lock (Console.Out) From 8c280a12ba951b3d6a62569a5fed3cc382e1c4ee Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 13 Nov 2019 17:15:05 +0000 Subject: [PATCH 12/12] optionally disable server logs; print aggregate stats --- .../StressTests/SslStress/Configuration.cs | 1 + .../tests/StressTests/SslStress/Program.cs | 2 ++ .../SslClientBase.StressResultAggregator.cs | 34 ++++++++++++++++--- .../StressTests/SslStress/SslClientBase.cs | 4 +-- .../StressTests/SslStress/SslServerBase.cs | 13 ++++--- .../StressTests/SslStress/StressOperations.cs | 12 ++++--- 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs b/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs index 15a05efcee27..c2a8bc1a69b8 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs @@ -21,5 +21,6 @@ public class Configuration public TimeSpan DisplayInterval { get; set; } public TimeSpan MinConnectionLifetime { get; set; } public TimeSpan MaxConnectionLifetime { get; set; } + public bool LogServer { get; set; } } } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs index a34cdf778b23..9e3a3f8b0e9d 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/Program.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/Program.cs @@ -110,6 +110,7 @@ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configura cmd.AddOption(new Option(new[] { "--min-connection-lifetime", "-l" }, "Minimum duration for a single connection, in seconds. Defaults to 5 seconds.") { Argument = new Argument("minutes", 5) }); cmd.AddOption(new Option(new[] { "--max-connection-lifetime", "-L" }, "Maximum duration for a single connection, in seconds. Defaults to 120 seconds.") { Argument = new Argument("minutes", 120) }); cmd.AddOption(new Option(new[] { "--display-interval", "-i" }, "Client stats display interval, in seconds. Defaults to 5 seconds.") { Argument = new Argument("seconds", 5) }); + cmd.AddOption(new Option(new[] { "--log-server", "-S" }, "Print server logs to stdout.")); cmd.AddOption(new Option(new[] { "--seed", "-s" }, "Seed for generating pseudo-random parameters. Also depends on the -n argument.") { Argument = new Argument("seed", (new Random().Next())) }); ParseResult parseResult = cmd.Parse(args); @@ -134,6 +135,7 @@ private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configura MinConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption("-l")), MaxConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption("-L")), DisplayInterval = TimeSpan.FromSeconds(parseResult.ValueForOption("-i")), + LogServer = parseResult.HasOption("-S"), RandomSeed = parseResult.ValueForOption("-s"), }; diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs index 44908f23aa72..90cca466f8db 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs @@ -87,7 +87,7 @@ private void UpdateCounters(int workerId) public void PrintFailureTypes() => _errors.PrintFailureTypes(); - public void PrintCurrentResults(TimeSpan elapsed) + public void PrintCurrentResults(TimeSpan elapsed, bool showAggregatesOnly) { (StreamCounter total, StreamCounter current)[] counters = GetCounterView(); @@ -112,12 +112,24 @@ public void PrintCurrentResults(TimeSpan elapsed) Console.ResetColor(); Console.Write(_failures[i].ToString("N0")); + if (!showAggregatesOnly) + { + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.Write($"\tTx: "); + Console.ResetColor(); + Console.Write(FmtBytes(counters[i].current.BytesWritten)); + Console.ForegroundColor = ConsoleColor.DarkMagenta; + Console.Write($"\tRx: "); + Console.ResetColor(); + Console.Write(FmtBytes(counters[i].current.BytesRead)); + } + Console.ForegroundColor = ConsoleColor.DarkBlue; - Console.Write($"\tTx: "); + Console.Write($"\tTotal Tx: "); Console.ResetColor(); Console.Write(FmtBytes(counters[i].total.BytesWritten)); Console.ForegroundColor = ConsoleColor.DarkMagenta; - Console.Write($"\tRx: "); + Console.Write($"\tTotal Rx: "); Console.ResetColor(); Console.Write(FmtBytes(counters[i].total.BytesRead)); @@ -136,12 +148,24 @@ public void PrintCurrentResults(TimeSpan elapsed) Console.ResetColor(); Console.Write(_failures.Sum().ToString("N0")); + if (!showAggregatesOnly) + { + Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.Write("\tTx: "); + Console.ResetColor(); + Console.Write(FmtBytes(counters.Select(c => c.current.BytesWritten).Sum())); + Console.ForegroundColor = ConsoleColor.DarkMagenta; + Console.Write($"\tRx: "); + Console.ResetColor(); + Console.Write(FmtBytes(counters.Select(c => c.current.BytesRead).Sum())); + } + Console.ForegroundColor = ConsoleColor.DarkBlue; - Console.Write("\tTx: "); + Console.Write("\tTotal Tx: "); Console.ResetColor(); Console.Write(FmtBytes(counters.Select(c => c.total.BytesWritten).Sum())); Console.ForegroundColor = ConsoleColor.DarkMagenta; - Console.Write($"\tRx: "); + Console.Write($"\tTotal Rx: "); Console.ResetColor(); Console.Write(FmtBytes(counters.Select(c => c.total.BytesRead).Sum())); diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs index d34f2f4999f6..3fe5ddfc8cbf 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.cs @@ -85,7 +85,7 @@ private Task StartCore() while (!_cts.IsCancellationRequested) { Thread.Sleep(_config.DisplayInterval); - lock (Console.Out) { _aggregator.PrintCurrentResults(_stopwatch.Elapsed); } + lock (Console.Out) { _aggregator.PrintCurrentResults(_stopwatch.Elapsed, showAggregatesOnly: false); } } }) { IsBackground = true }.Start(); @@ -143,7 +143,7 @@ public void PrintFinalReport() Console.WriteLine("SslStress Run Final Report"); Console.WriteLine(); - _aggregator.PrintCurrentResults(_stopwatch.Elapsed); + _aggregator.PrintCurrentResults(_stopwatch.Elapsed, showAggregatesOnly: true); _aggregator.PrintFailureTypes(); } } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs index c28834ac4d78..bd4dee8e0460 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/SslServerBase.cs @@ -107,12 +107,15 @@ async Task RunSingleWorker() } catch (Exception e) { - lock (Console.Out) + if (_config.LogServer) { - Console.ForegroundColor = ConsoleColor.DarkRed; - Console.WriteLine($"Server: unhandled exception: {e}"); - Console.WriteLine(); - Console.ResetColor(); + lock (Console.Out) + { + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine($"Server: unhandled exception: {e}"); + Console.WriteLine(); + Console.ResetColor(); + } } } } diff --git a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs index 5f86f8749d98..860abd3bf9a3 100644 --- a/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs +++ b/src/System.Net.Security/tests/StressTests/SslStress/StressOperations.cs @@ -285,12 +285,14 @@ async Task Callback(ReadOnlySequence buffer) } catch (DataMismatchException e) { - // report as warning and continue - lock (Console.Out) + if (_config.LogServer) { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"Server: {e.Message}"); - Console.ResetColor(); + lock (Console.Out) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Server: {e.Message}"); + Console.ResetColor(); + } } } finally