Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

System.Net.Security test stabilization #7937

Merged
merged 1 commit into from
Apr 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions src/Common/tests/Scripts/ParallelTestExecution.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# 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.

# Usage:
#
# . ParallelTestExecution.ps1
# cd <testFolder> (e.g. testFolder = src\System.Net.Security\tests\FunctionalTests)
# RunMultiple <n> <delayVarianceMilliseconds> [-UntilFailed]
#
# The above sequence will open up <n> windows running the test project present in the current folder.
# If -UntilFailed is used, the tests will continuously loop until a failure is detected.
# Between loops, the execution will pause for <delayVarianceMilliseconds>.

function BuildAndTestBinary
{
$output = (msbuild /t:rebuild,test)
if ($lastexitcode -ne 0)
{
throw "Build/test failed."
}

return $output
}

function TileWindows
{
$shell = New-Object -ComObject Shell.Application
$shell.TileHorizontally()
}

function CurrentPath
{
return (Get-Item -Path ".\" -Verbose).FullName
}

function ParseCurrentPath
{
$p = CurrentPath

$test_found = $false
$contract_found = $false
$root_found = $false

while ((-not $root_found) -and ($p -ne ""))
{
$leaf = Split-Path $p -Leaf

if (Test-Path (Join-Path $p 'build.cmd'))
{
$Global:RootPath = $p
$root_found = $true
}

if ($test_found -and (-not $contract_found))
{
$Global:ContractName = $leaf
$contract_found = $true
}

if ($leaf -eq "tests")
{
$test_found = $true
}

$p = Split-Path $p
}

if (-not $test_found)
{
throw "This folder doesn't appear to be part of a test (looking for ...\contract\tests\...)."
}
}

function ParseTestExecutionCommand($msBuildOutput)
{
$foundTestExecution = $false
$cmdLine = ""

foreach ($line in $msBuildOutput)
{
if ($foundTestExecution -eq $true)
{
$cmdLine = $line
break
}

if ($line.Contains("RunTestsForProject:"))
{
$foundTestExecution = $true
}
}

if (-not $foundTestExecution)
{
throw "Cannot parse MSBuild output: please ensure that the current folder contains a test."
}

$Global:TestCommand = $cmdLine.Trim()
}

function ParseTestFolder($testExecutionCmdLine)
{
$coreRunPath = $testExecutionCmdLine.Split()[0]
return Split-Path $coreRunPath
}

function Initialize
{
ParseCurrentPath

Write-Host -NoNewline "Initializing tests for $($Global:ContractName) . . . "

try
{
$output = BuildAndTestBinary
ParseTestExecutionCommand($output)

Write-Host -ForegroundColor Green "OK"
}
catch
{
Write-Host -ForegroundColor Red "Failed"
throw
}
}

function RunOne($testCommand)
{
if ($testCommand -ne "")
{
$Global:TestCommand = $testCommand
}

if ($Global:TestCommand -eq $null)
{
throw "Run Initialize first or pass the test command line as a parameter."
}

Write-Host $Global:TestCommand
$path = ParseTestFolder($Global:TestCommand)
Write-Host "$path"

Push-Location
cd $path
Invoke-Expression $Global:TestCommand
if ($lastexitcode -ne 0)
{
throw "Test execution failed."
}

Pop-Location
}

function RunUntilFailed($testCommand, $delayVarianceMilliseconds = 0)
{

try
{
while($true)
{
RunOne $testCommand

if ($delayVarianceMilliseconds -ne 0)
{
$sleepMilliseconds = Get-Random -Minimum 0 -Maximum $delayVarianceMilliseconds
Write-Host -ForegroundColor Cyan "Sleeping $sleepMilliseconds"
Start-Sleep -Milliseconds $sleepMilliseconds
}
}
}
catch
{
Write-Host -ForegroundColor Red "Test execution failed!"
Read-Host "Press ENTER to continue..."
}
}

function RunMultiple(
[int]$n = 2,
[int]$RandomDelayVarianceMilliseconds = 0,
[switch]$UntilFailed = $false)
{
if ($Global:TestCommand -eq $null)
{
Initialize
}

$script = $PSCommandPath
$testCommand = $Global:TestCommand

if ($untilFailed)
{
$executionMethod = "RunUntilFailed"
}
else
{
$executionMethod = "RunOne"
}

$cmdArguments = "-Command `"&{. $script; $executionMethod '$testCommand' $RandomDelayVarianceMilliseconds}`""

$processes = @()

for ($i=0; $i -lt $n; $i++)
{
$thisCmdArguments = $cmdArguments -replace ("testResults.xml", "testResults$i.xml")
$process = Start-Process -PassThru powershell -ArgumentList $thisCmdArguments
$processes += $process
}

$processesExited = $false
while (-not ([console]::KeyAvailable -or $processesExited))
{
Clear-Host

Write-Host -ForegroundColor Cyan "Active test processes:"
Write-Host
Write-Host "[Press any key to close.]"
Write-Host
$processes | Format-Table -Property Id, CPU, Handles, WS, ExitCode

$processesExited = $true
foreach($p in $processes)
{
if (-not $p.HasExited)
{
$processesExited = $false
}
}

Start-Sleep -Milliseconds 1000
TileWindows
}

if (-not $processesExited)
{
Write-Host -ForegroundColor Cyan "Terminating all processes."
foreach ($p in $processes)
{
if (-not $p.HasExited)
{
$p.Kill()
}
}
}

Write-Host "Done."
}
26 changes: 23 additions & 3 deletions src/Common/tests/System/Net/VirtualNetwork/VirtualNetwork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace System.Net.Test.Common
{
public class VirtualNetwork
{
private readonly int WaitForReadDataTimeoutMilliseconds = 10 * 1000;
private readonly int WaitForReadDataTimeoutMilliseconds = 30 * 1000;

private readonly ConcurrentQueue<byte[]> _clientWriteQueue = new ConcurrentQueue<byte[]>();
private readonly ConcurrentQueue<byte[]> _serverWriteQueue = new ConcurrentQueue<byte[]>();
Expand All @@ -34,9 +34,29 @@ public void ReadFrame(bool server, out byte[] buffer)
packetQueue = _serverWriteQueue;
}

semaphore.Wait(WaitForReadDataTimeoutMilliseconds);
if (!semaphore.Wait(WaitForReadDataTimeoutMilliseconds))
{
throw new TimeoutException("VirtualNetwork: Timeout reading the next frame.");
}

bool dequeueSucceeded = false;
int remainingTries = 3;
int backOffDelayMilliseconds = 2;

do
{
dequeueSucceeded = packetQueue.TryDequeue(out buffer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what situation is this going to fail? Rather than looping around the TryDequeue, should we be looping around the semaphore.Wait? If the Wait completes successfully and there's nothing to take from the queue, that's a serious problem.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assert that got removed was the first thing that failed under stress after the test got switched to proper async/await.

The wait is on a semaphore, not a lock. It's acceptable to have multiple threads adding while multiple other threads are reading.

TryDequeue is, by contract, allowed to fail: http://stackoverflow.com/questions/5645348/under-what-conditions-can-trydequeue-and-similar-system-collections-concurrent-c

With this fix, the second TryDequeue always seems to pass. (Given the issue above I would have seen tests hanging for >27hrs which didn't happen overnight.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following.

The reason TryDequeue might return false is if there's nothing in the collection. And the only reason that might happen is if the semaphore.Wait(timeout) returned false due to hitting the timeout (if it returned true, that meant that there was count in the semaphore that could be taken, and that would only happen if there was a corresponding item to be taken from the queue).

So I'm suggesting that instead of wrapping TryDequeue in a loop, you wrap the Wait in a loop. If the Wait returns true, TryDequeue should never return false, and if the Wait returns false, there's no reason to even try calling TryDequeue.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now see what you mean. The reason I didn't think that was possible because the tests were executing in <10s (about 7-8sec) and the semaphore wait timeout was 10s.

Retrying on the semaphore wait doesn't make the code better as the idea was to eventually time out.
Increasing the timeout to 30sec and throwing a TimeoutException if the semaphore wait returns false.

if (dequeueSucceeded)
{
break;
}

remainingTries--;
backOffDelayMilliseconds *= backOffDelayMilliseconds;
Thread.Sleep(backOffDelayMilliseconds);
Copy link
Member

@stephentoub stephentoub Apr 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This math doesn't seem quite right. After the first try, we're going to sleep for 10*10 == 100 milliseconds, that's fine. The second time we're going to sleep for 100*100 == 10,000 millseconds = 10 seconds... a bit long, but maybe ok. The third time we're going to sleep for 10000*10000 milliseconds == 27 hours?

Did you mean to be multiplying by 10 each time rather than by backoffDelayMilliseconds?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. Changing initial delay of 2 ms and rerunning tests.

}
while (!dequeueSucceeded && (remainingTries > 0));

bool dequeueSucceeded = packetQueue.TryDequeue(out buffer);
Debug.Assert(dequeueSucceeded, "Packet queue: TryDequeue failed.");
}

Expand Down
13 changes: 3 additions & 10 deletions src/Common/tests/System/Net/VirtualNetwork/VirtualNetworkStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,9 @@ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancel

public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
Write(buffer, offset, count);
return Task.CompletedTask;
}
catch (Exception e)
{
return Task.FromException(e);
}
return cancellationToken.IsCancellationRequested ?
Task.FromCanceled<int>(cancellationToken) :
Task.Run(() => Write(buffer, offset, count));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine. FWIW, I didn't do this when I changed the ReadAsync because, whereas Read blocks waiting for data, Write completes quickly and without blocking.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed per #5991 - the cancellationToken was not honored correctly.

}
}
}
36 changes: 24 additions & 12 deletions src/Common/tests/Tests/System/Net/VirtualNetworkStreamTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ public void VirtualNetworkStream_SingleThreadIntegrityTest_Ok()
{
for (int i = 0; i < 100000; i++)
{
int bufferSize = rnd.Next(1, 2048);

byte[] writeFrame = new byte[bufferSize];
int bufferSize;
byte[] writeFrame;

bufferSize = rnd.Next(1, 2048);
writeFrame = new byte[bufferSize];
rnd.NextBytes(writeFrame);

uint writeChecksum = Fletcher32.Checksum(writeFrame, 0, writeFrame.Length);
client.Write(writeFrame, 0, writeFrame.Length);

Expand Down Expand Up @@ -99,6 +102,8 @@ public void VirtualNetworkStream_MultiThreadIntegrityTest_Ok()
Assert.True(maxFrameSize > sizeof(int) + 1);

var rnd = new Random();
var rndLock = new object();

var network = new VirtualNetwork();
var checksumAndLengths = new ConcurrentDictionary<int, Tuple<uint, int>>();

Expand All @@ -107,12 +112,21 @@ public void VirtualNetworkStream_MultiThreadIntegrityTest_Ok()
using (var client = new VirtualNetworkStream(network, isServer: false))
using (var server = new VirtualNetworkStream(network, isServer: true))
{
Parallel.For(0, 100000, async (int i) =>
Parallel.For(0, 100, (int i) =>
{
int bufferSize = rnd.Next(sizeof(int) + 1, maxFrameSize);
int bufferSize;
int delayMilliseconds;
byte[] writeFrame;

lock (rndLock)
{
bufferSize = rnd.Next(sizeof(int) + 1, maxFrameSize);
delayMilliseconds = rnd.Next(0, 10);

writeFrame = new byte[bufferSize];
rnd.NextBytes(writeFrame);
}

byte[] writeFrame = new byte[bufferSize];
rnd.NextBytes(writeFrame);

// First 4 bytes represent the sequence number.
byte[] sequenceNo = BitConverter.GetBytes(i);
Expand All @@ -123,14 +137,12 @@ public void VirtualNetworkStream_MultiThreadIntegrityTest_Ok()

checksumAndLengths.AddOrUpdate(i, writeFrameInfo, (seq, checkSum) => { Debug.Fail("Attempt to update checksum."); return new Tuple<uint, int>(0, 0); });

await client.WriteAsync(writeFrame, 0, writeFrame.Length);

int delayMilliseconds = rnd.Next(0, 10);
await Task.Delay(delayMilliseconds);
client.WriteAsync(writeFrame, 0, writeFrame.Length).GetAwaiter().GetResult();
Task.Delay(delayMilliseconds).GetAwaiter().GetResult();

// First read the index to know how much data to read from this frame.
var readFrame = new byte[maxFrameSize];
int readLen = await server.ReadAsync(readFrame, 0, maxFrameSize);
int readLen = server.ReadAsync(readFrame, 0, maxFrameSize).GetAwaiter().GetResult();

int idx = BitConverter.ToInt32(readFrame, 0);
Tuple<uint, int> expectedFrameInfo = checksumAndLengths[idx];
Expand Down
1 change: 1 addition & 0 deletions src/Common/tests/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"System.Runtime.Handles": "4.0.1-rc3-24022-00",
"System.Runtime.InteropServices": "4.1.0-rc3-24022-00",
"System.Text.RegularExpressions": "4.1.0-rc3-24022-00",
"System.Threading.Thread": "4.0.0-rc3-24022-00",
"System.Threading.Tasks": "4.0.11-rc3-24022-00",
"System.Threading.Tasks.Parallel": "4.0.1-rc3-24022-00",
"xunit": "2.1.0",
Expand Down
Loading