This repository was archived by the owner on Jan 23, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Initial SslStream stress app work #42560
Closed
Closed
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a253dc1
add an ssl stress suite
eiriktsarpalis f743838
fix server shutdown deadlock
eiriktsarpalis 3950f03
fix formatting issues
eiriktsarpalis b31fc4b
fix cli issue
eiriktsarpalis 798de3c
cli issue
eiriktsarpalis 7870def
address feedback
eiriktsarpalis bdcab6f
make serialization cancellable
eiriktsarpalis 2ac385b
fix SslServer disposal semantics
eiriktsarpalis 95525ff
add randomized cancellation to connections
eiriktsarpalis 9dd1f2a
use PascalCase
eiriktsarpalis 50f6f74
ensure all deserialization errors are handled properly by the server
eiriktsarpalis 8c280a1
optionally disable server logs; print aggregate stats
eiriktsarpalis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
src/System.Net.Security/tests/StressTests/SslStress/Configuration.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
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 MinConnectionLifetime { get; set; } | ||
public TimeSpan MaxConnectionLifetime { get; set; } | ||
public bool LogServer { get; set; } | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.props
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<Project/> |
1 change: 1 addition & 0 deletions
1
src/System.Net.Security/tests/StressTests/SslStress/Directory.Build.targets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<Project/> |
14 changes: 14 additions & 0 deletions
14
src/System.Net.Security/tests/StressTests/SslStress/Dockerfile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
159 changes: 159 additions & 0 deletions
159
src/System.Net.Security/tests/StressTests/SslStress/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
// 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; | ||
using SslStress.Utils; | ||
|
||
namespace SslStress | ||
{ | ||
public static class Program | ||
{ | ||
public enum ExitCode { Success = 0, StressError = 1, CliError = 2 }; | ||
|
||
public static async Task<int> Main(string[] args) | ||
{ | ||
if (!TryParseCli(args, out Configuration? config)) | ||
{ | ||
return (int)ExitCode.CliError; | ||
} | ||
|
||
return (int)await Run(config); | ||
} | ||
|
||
private static async Task<ExitCode> 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(" Min Conn. Lifetime: " + config.MinConnectionLifetime); | ||
Console.WriteLine(" Max Conn. Lifetime: " + config.MaxConnectionLifetime); | ||
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."); | ||
Console.WriteLine(); | ||
|
||
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<bool>(); | ||
Console.CancelKeyPress += (sender, args) => { Console.Error.WriteLine("Keyboard interrupt"); args.Cancel = true; tcs.TrySetResult(false); }; | ||
if (maxExecutionTime.HasValue) | ||
{ | ||
Console.WriteLine($"Running for a total of {maxExecutionTime.Value.TotalMinutes:0.##} minutes"); | ||
var cts = new System.Threading.CancellationTokenSource(delay: maxExecutionTime.Value); | ||
cts.Token.Register(() => { Console.WriteLine("Max execution time elapsed"); tcs.TrySetResult(false); }); | ||
} | ||
|
||
await tcs.Task; | ||
} | ||
} | ||
|
||
private static 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", RunMode.both) }); | ||
cmd.AddOption(new Option(new[] { "--num-connections", "-n" }, "Max number of connections to open concurrently.") { Argument = new Argument<int>("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<string>("ipEndpoint", "127.0.0.1:5002") }); | ||
cmd.AddOption(new Option(new[] { "--max-execution-time", "-t" }, "Maximum stress suite execution time, in minutes. Defaults to infinity.") { Argument = new Argument<double?>("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<int>("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<double>("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<double>("minutes", 120) }); | ||
cmd.AddOption(new Option(new[] { "--display-interval", "-i" }, "Client stats display interval, in seconds. Defaults to 5 seconds.") { Argument = new Argument<double>("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<int>("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); | ||
} | ||
WriteHelpText(); | ||
config = null; | ||
return false; | ||
} | ||
|
||
config = new Configuration() | ||
{ | ||
RunMode = parseResult.ValueForOption<RunMode>("-m"), | ||
MaxConnections = parseResult.ValueForOption<int>("-n"), | ||
ServerEndpoint = IPEndPoint.Parse(parseResult.ValueForOption<string>("-e")), | ||
MaxExecutionTime = parseResult.ValueForOption<double?>("-t")?.Pipe(TimeSpan.FromMinutes), | ||
MaxBufferLength = parseResult.ValueForOption<int>("-b"), | ||
MinConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption<double>("-l")), | ||
MaxConnectionLifetime = TimeSpan.FromSeconds(parseResult.ValueForOption<double>("-L")), | ||
DisplayInterval = TimeSpan.FromSeconds(parseResult.ValueForOption<double>("-i")), | ||
LogServer = parseResult.HasOption("-S"), | ||
RandomSeed = parseResult.ValueForOption<int>("-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; | ||
|
||
void WriteHelpText() | ||
{ | ||
Console.WriteLine(); | ||
new HelpBuilder(new SystemConsole()).Write(cmd); | ||
} | ||
} | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
src/System.Net.Security/tests/StressTests/SslStress/Readme.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
## SslStress | ||
|
||
Stress testing suite for SslStream |
179 changes: 179 additions & 0 deletions
179
src/System.Net.Security/tests/StressTests/SslStress/SslClientBase.StressResultAggregator.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
// 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; | ||
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]; | ||
} | ||
|
||
public long TotalConnections => _totalConnections; | ||
public long TotalFailures => _failures.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.WriteLine(); | ||
Console.ResetColor(); | ||
} | ||
} | ||
|
||
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, bool showAggregatesOnly) | ||
{ | ||
(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")); | ||
|
||
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($"\tTotal Tx: "); | ||
Console.ResetColor(); | ||
Console.Write(FmtBytes(counters[i].total.BytesWritten)); | ||
Console.ForegroundColor = ConsoleColor.DarkMagenta; | ||
Console.Write($"\tTotal Rx: "); | ||
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")); | ||
|
||
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("\tTotal Tx: "); | ||
Console.ResetColor(); | ||
Console.Write(FmtBytes(counters.Select(c => c.total.BytesWritten).Sum())); | ||
Console.ForegroundColor = ConsoleColor.DarkMagenta; | ||
Console.Write($"\tTotal Rx: "); | ||
Console.ResetColor(); | ||
Console.Write(FmtBytes(counters.Select(c => c.total.BytesRead).Sum())); | ||
|
||
Console.WriteLine(); | ||
Console.WriteLine(); | ||
|
||
static string FmtBytes(long value) => HumanReadableByteSizeFormatter.Format(value); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.