diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ad5cc02d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "dotnet.unitTests.runSettingsPath": "TestExplorer.runsettings", +} \ No newline at end of file diff --git a/TestExplorer.runsettings b/TestExplorer.runsettings new file mode 100644 index 00000000..9f69671b --- /dev/null +++ b/TestExplorer.runsettings @@ -0,0 +1,11 @@ + + + + + tcp://localhost:2375 + "" + "" + 1 + + + \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/CommonCommands.cs b/test/Docker.DotNet.Tests/CommonCommands.cs index b6219392..19db1d0d 100644 --- a/test/Docker.DotNet.Tests/CommonCommands.cs +++ b/test/Docker.DotNet.Tests/CommonCommands.cs @@ -4,5 +4,7 @@ public static class CommonCommands { public static readonly string[] SleepInfinity = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; sleep infinity"]; - public static readonly string[] EchoToStdoutAndStderr = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; while true; do echo \"stdout message\"; echo \"stderr message\" >&2; sleep 1; done"]; + public static readonly string[] EchoToStdoutAndStderr = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; RND=$RANDOM; while true; do echo \"stdout message $RND\"; echo \"stderr message $RND\" >&2; sleep 1; done"]; + + public static readonly string[] EchoToStdoutAndStderrFast = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; RND=$RANDOM; while true; do echo \"stdout message $RND\"; echo \"stderr message $RND\" >&2; done"]; } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index 0c0c14ed..df804674 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -20,10 +20,12 @@ + + diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs index ff3ebab9..56e1289b 100644 --- a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs @@ -167,6 +167,194 @@ await _testFixture.DockerClient.Containers.StopContainerAsync( Assert.NotEmpty(logList); } + [Fact] + public async Task GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs() + { + using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + var parallelContainerCount = 3; + var parallelThreadCount = 100; + var runtimeInSeconds = 9; + + var containerIds = new string[parallelContainerCount]; + + long memoryUsageBefore = GC.GetTotalAllocatedBytes(true); + + long socketsBefore = IPGlobalProperties.GetIPGlobalProperties() + .GetTcpIPv4Statistics() + .CurrentConnections; + + Process process = Process.GetCurrentProcess(); + TimeSpan cpuTimeBefore = process.TotalProcessorTime; + + ParallelOptions parallelOptions = new ParallelOptions + { + MaxDegreeOfParallelism = parallelContainerCount, + CancellationToken = _testFixture.Cts.Token + }; + + await Parallel.ForEachAsync(Enumerable.Range(0, parallelContainerCount), parallelOptions, async (parallel, ct) => + { + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters + { + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, + Tty = false + }, + _testFixture.Cts.Token + ); + + await _testFixture.DockerClient.Containers.StartContainerAsync( + createContainerResponse.ID, + new ContainerStartParameters(), + _testFixture.Cts.Token + ); + containerIds[parallel] = createContainerResponse.ID; + }); + + await Task.Delay(TimeSpan.FromSeconds(runtimeInSeconds)); + + await Parallel.ForEachAsync(Enumerable.Range(0, parallelContainerCount), parallelOptions, async (parallel, ct) => + { + await _testFixture.DockerClient.Containers.StopContainerAsync( + containerIds[parallel], + new ContainerStopParameters(), + _testFixture.Cts.Token + ); + }); + + containerLogsCts.CancelAfter(TimeSpan.FromSeconds(1)); + + var logLists = new ConcurrentDictionary(); + var threads = new List(); + + for (int parallel = 0; parallel < parallelContainerCount * parallelThreadCount; parallel++) + { + int index = parallel; + string containerId = containerIds[parallel % parallelContainerCount]; + CancellationToken ct = containerLogsCts.Token; + + var thread = new Thread(() => + { + var logList = new StringBuilder(2000); + try + { + var task = _testFixture.DockerClient.Containers.GetContainerLogsAsync( + containerId, + new ContainerLogsParameters + { + ShowStderr = true, + ShowStdout = true, + Timestamps = true, + Follow = false + }, + new Progress(m => logList.AppendLine(m)), + ct + ); + + task.GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + } + + Thread.Sleep(100); + + logLists.TryAdd(index, logList.ToString()); + logList.Clear(); + }); + + threads.Add(thread); + thread.Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + TimeSpan cpuTimeAfter = process.TotalProcessorTime; + + long socketsAfter = IPGlobalProperties.GetIPGlobalProperties() + .GetTcpIPv4Statistics() + .CurrentConnections; + + long memoryUsageAfter = GC.GetTotalAllocatedBytes(true); + + var averageLineCount = logLists.Values.Average(logs => logs.Split('\n').Count()); + + _testOutputHelper.WriteLine($"avg. Line count: {averageLineCount:N1}, cpu ticks: {cpuTimeAfter.Ticks - cpuTimeBefore.Ticks:N0}, mem usage: {memoryUsageAfter - memoryUsageBefore:N0}, sockets: {socketsAfter - socketsBefore:N0}"); + _testOutputHelper.WriteLine($"FirstLine: {logLists.Values.FirstOrDefault()}"); + + // one container should produce 2 lines per second (stdout + stderr) plus 1 for last empty line of split + Assert.True(averageLineCount > (runtimeInSeconds + 1) * 2, $"Average line count {averageLineCount:N1} is less than expected {(runtimeInSeconds + 1) * 2}"); + GC.Collect(); + } + + [Fact] + public async Task GetContainerLogs_SpeedTest_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled() + { + using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + var runtimeInSeconds = 15; + + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters + { + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderrFast, + Tty = false + }, + _testFixture.Cts.Token + ); + + await _testFixture.DockerClient.Containers.StartContainerAsync( + createContainerResponse.ID, + new ContainerStartParameters(), + _testFixture.Cts.Token + ); + + containerLogsCts.CancelAfter(TimeSpan.FromSeconds(runtimeInSeconds)); + + long memoryUsageBefore = GC.GetTotalAllocatedBytes(true); + + var counter = 0; + try + { + await _testFixture.DockerClient.Containers.GetContainerLogsAsync( + createContainerResponse.ID, + new ContainerLogsParameters + { + ShowStderr = true, + ShowStdout = true, + Timestamps = true, + Follow = true + }, + new Progress(m => counter++), + containerLogsCts.Token); + } + catch (OperationCanceledException) + { + + } + + + long memoryUsageAfter = GC.GetTotalAllocatedBytes(true); + + await _testFixture.DockerClient.Containers.StopContainerAsync( + createContainerResponse.ID, + new ContainerStopParameters(), + _testFixture.Cts.Token + ); + + _testOutputHelper.WriteLine($"Line count: {counter}, mem usage: {memoryUsageAfter - memoryUsageBefore:N0}"); + + Assert.True(counter > runtimeInSeconds * 25000, $"Line count {counter} is less than expected {runtimeInSeconds * 25000}"); + + GC.Collect(); + } + [Fact] public async Task GetContainerLogs_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled() {