Skip to content

Commit 4ae11fa

Browse files
[17.12] Prevent contention between CancelAllSubmissions and EndBuild (#10745)
* Allow fast-abort of submissions even after EndBuild initiated * Bump version * Add test verifying the proper build abort on CancelAllSubmissions swapped * Bump version * bump version to 17.12.3 --------- Co-authored-by: YuliiaKovalova <[email protected]>
1 parent e7dfc71 commit 4ae11fa

File tree

3 files changed

+54
-14
lines changed

3 files changed

+54
-14
lines changed

eng/Versions.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the MIT license. See License.txt in the project root for full license information. -->
33
<Project>
44
<PropertyGroup>
5-
<VersionPrefix>17.12.2</VersionPrefix><DotNetFinalVersionKind>release</DotNetFinalVersionKind>
5+
<VersionPrefix>17.12.3</VersionPrefix><DotNetFinalVersionKind>release</DotNetFinalVersionKind>
66
<PackageValidationBaselineVersion>17.11.4</PackageValidationBaselineVersion>
77
<AssemblyVersion>15.1.0.0</AssemblyVersion>
88
<PreReleaseVersionLabel>preview</PreReleaseVersionLabel>

src/Build.UnitTests/BackEnd/BuildManager_Tests.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1657,13 +1657,14 @@ public void CancelledBuildWithDelay40()
16571657
string contents = CleanupFileContents(@"
16581658
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
16591659
<Target Name='test'>
1660-
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(10)) + @"'/>
1660+
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(20)) + @"'/>
16611661
<Message Text='[errormessage]'/>
16621662
</Target>
16631663
</Project>
16641664
");
16651665
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
16661666
_buildManager.BeginBuild(_parameters);
1667+
Stopwatch sw = Stopwatch.StartNew();
16671668
BuildSubmission asyncResult = _buildManager.PendBuildRequest(data);
16681669
asyncResult.ExecuteAsync(null, null);
16691670

@@ -1675,6 +1676,50 @@ public void CancelledBuildWithDelay40()
16751676

16761677
Assert.Equal(BuildResultCode.Failure, result.OverallResult); // "Build should have failed."
16771678
_logger.AssertLogDoesntContain("[errormessage]");
1679+
// The build should bail out immediately after executing CancelAllSubmissions, build stalling for a longer time
1680+
// is very unexpected.
1681+
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10));
1682+
}
1683+
1684+
/// <summary>
1685+
/// A canceled build which waits for the task to get started before canceling. Because it is a 12.. task, we should
1686+
/// cancel the task and exit out after a short period wherein we wait for the task to exit cleanly.
1687+
///
1688+
/// This test also exercises the possibility of CancelAllSubmissions being executed after EndBuild -
1689+
/// which can happen even if they are synchronously executed in expected order - the CancelAllSubmissions is internally
1690+
/// asynchronous and hence part of the execution can happen after EndBuild.
1691+
/// </summary>
1692+
[Fact]
1693+
public void CancelledBuildWithDelay40_WithThreatSwap()
1694+
{
1695+
string contents = CleanupFileContents(@"
1696+
<Project xmlns='msbuildnamespace' ToolsVersion='msbuilddefaulttoolsversion'>
1697+
<Target Name='test'>
1698+
<Exec Command='" + Helpers.GetSleepCommand(TimeSpan.FromSeconds(20)) + @"'/>
1699+
<Message Text='[errormessage]'/>
1700+
</Target>
1701+
</Project>
1702+
");
1703+
BuildRequestData data = GetBuildRequestData(contents, Array.Empty<string>(), MSBuildDefaultToolsVersion);
1704+
_buildManager.BeginBuild(_parameters);
1705+
Stopwatch sw = Stopwatch.StartNew();
1706+
BuildSubmission asyncResult = _buildManager.PendBuildRequest(data);
1707+
asyncResult.ExecuteAsync(null, null);
1708+
1709+
Thread.Sleep(500);
1710+
// Simulate the case where CancelAllSubmissions is called after EndBuild or its internal queued task is swapped
1711+
// and executed after EndBuild starts execution.
1712+
System.Threading.Tasks.Task.Delay(500).ContinueWith(t => _buildManager.CancelAllSubmissions());
1713+
_buildManager.EndBuild();
1714+
1715+
asyncResult.WaitHandle.WaitOne();
1716+
BuildResult result = asyncResult.BuildResult;
1717+
1718+
Assert.Equal(BuildResultCode.Failure, result.OverallResult); // "Build should have failed."
1719+
_logger.AssertLogDoesntContain("[errormessage]");
1720+
// The build should bail out immediately after executing CancelAllSubmissions, build stalling for a longer time
1721+
// is very unexpected.
1722+
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10));
16781723
}
16791724

16801725
/// <summary>

src/Build/BackEnd/BuildManager/BuildManager.cs

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -789,15 +789,10 @@ void Callback(object? state)
789789
{
790790
lock (_syncLock)
791791
{
792-
if (_shuttingDown)
793-
{
794-
return;
795-
}
796-
797-
// If we are Idle, obviously there is nothing to cancel. If we are waiting for the build to end, then presumably all requests have already completed
798-
// and there is nothing left to cancel. Putting this here eliminates the possibility of us racing with EndBuild to access the nodeManager before
799-
// EndBuild sets it to null.
800-
if (_buildManagerState != BuildManagerState.Building)
792+
// If the state is Idle - then there is yet or already nothing to cancel
793+
// If state is WaitingForBuildToComplete - we might be already waiting gracefully - but CancelAllSubmissions
794+
// is a request for quick abort - so it's fine to resubmit the request
795+
if (_buildManagerState == BuildManagerState.Idle)
801796
{
802797
return;
803798
}
@@ -2078,17 +2073,17 @@ private void ShutdownConnectedNodes(bool abort)
20782073
lock (_syncLock)
20792074
{
20802075
_shuttingDown = true;
2081-
_executionCancellationTokenSource!.Cancel();
2076+
_executionCancellationTokenSource?.Cancel();
20822077

20832078
// If we are aborting, we will NOT reuse the nodes because their state may be compromised by attempts to shut down while the build is in-progress.
2084-
_nodeManager!.ShutdownConnectedNodes(!abort && _buildParameters!.EnableNodeReuse);
2079+
_nodeManager?.ShutdownConnectedNodes(!abort && _buildParameters!.EnableNodeReuse);
20852080

20862081
// if we are aborting, the task host will hear about it in time through the task building infrastructure;
20872082
// so only shut down the task host nodes if we're shutting down tidily (in which case, it is assumed that all
20882083
// tasks are finished building and thus that there's no risk of a race between the two shutdown pathways).
20892084
if (!abort)
20902085
{
2091-
_taskHostNodeManager!.ShutdownConnectedNodes(_buildParameters!.EnableNodeReuse);
2086+
_taskHostNodeManager?.ShutdownConnectedNodes(_buildParameters!.EnableNodeReuse);
20922087
}
20932088
}
20942089
}

0 commit comments

Comments
 (0)