Skip to content

Commit 6fa57bb

Browse files
Fix RequestCores/ReleaseCores fallback in OOP TaskHost: throw NotImplementedException instead of logging error (#13345)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ef957b7 commit 6fa57bb

4 files changed

Lines changed: 160 additions & 19 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.Build.Framework;
6+
using Microsoft.Build.Utilities;
7+
8+
namespace Microsoft.Build.UnitTests.BackEnd
9+
{
10+
/// <summary>
11+
/// A test task that calls IBuildEngine9.RequestCores with the same fallback pattern
12+
/// used by real callers (MonoAOTCompiler, EmccCompile, ILStrip, EmitBundleBase):
13+
/// try { cores = be9.RequestCores(N); }
14+
/// catch (NotImplementedException) { be9 = null; /* use own estimate */ }
15+
/// finally { be9?.ReleaseCores(cores); }
16+
/// Used by regression tests for https://github.com/dotnet/msbuild/issues/13333
17+
/// </summary>
18+
public class RequestCoresWithFallbackTask : Task
19+
{
20+
/// <summary>
21+
/// Number of cores to request. Defaults to 1.
22+
/// </summary>
23+
public int CoreCount { get; set; } = 1;
24+
25+
/// <summary>
26+
/// Whether to call ReleaseCores after the parallel work (via be9?.ReleaseCores pattern).
27+
/// </summary>
28+
public bool ReleaseAfter { get; set; }
29+
30+
/// <summary>
31+
/// Number of cores granted (either from RequestCores or the fallback value).
32+
/// </summary>
33+
[Output]
34+
public int GrantedCores { get; set; }
35+
36+
/// <summary>
37+
/// True if NotImplementedException was caught and the fallback was used.
38+
/// </summary>
39+
[Output]
40+
public bool UsedFallback { get; set; }
41+
42+
public override bool Execute()
43+
{
44+
int allowedParallelism = CoreCount;
45+
IBuildEngine9? be9 = BuildEngine as IBuildEngine9;
46+
try
47+
{
48+
if (be9 is not null)
49+
{
50+
allowedParallelism = be9.RequestCores(CoreCount);
51+
}
52+
}
53+
catch (NotImplementedException)
54+
{
55+
// This is the expected path when callbacks are not supported.
56+
// Real callers null out be9 so ReleaseCores is skipped.
57+
Log.LogMessage(MessageImportance.High, "RequestCores threw NotImplementedException, using fallback");
58+
be9 = null;
59+
UsedFallback = true;
60+
}
61+
62+
GrantedCores = allowedParallelism;
63+
Log.LogMessage(MessageImportance.High, $"GrantedCores = {GrantedCores}");
64+
65+
// Simulate parallel work...
66+
67+
if (ReleaseAfter)
68+
{
69+
// Real callers use be9?.ReleaseCores — skipped when be9 was nulled in catch.
70+
if (be9 is not null)
71+
{
72+
be9.ReleaseCores(allowedParallelism);
73+
Log.LogMessage(MessageImportance.High, $"ReleaseCores({allowedParallelism}) completed");
74+
}
75+
}
76+
77+
return true;
78+
}
79+
}
80+
}

src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -248,21 +248,28 @@ public void RequestCores_WorksWhenAutoEjectedInMultiThreadedMode()
248248
}
249249

250250
/// <summary>
251-
/// Verifies that RequestCores when callbacks are disabled logs error MSB5022 and returns 0.
251+
/// Regression test for https://github.com/dotnet/msbuild/issues/13333
252+
/// When callbacks are not supported (cross-version OOP TaskHost), RequestCores must
253+
/// throw NotImplementedException (not log a build error and return 0).
254+
/// Real callers (MonoAOTCompiler, EmccCompile, ILStrip, EmitBundleBase) catch this
255+
/// exception and fall back to their own parallelism estimate. The previous behavior
256+
/// of logging BuildErrorEventArgs caused the build to fail silently.
252257
/// </summary>
253258
[Fact]
254-
public void RequestCores_LogsErrorWhenCallbacksNotSupported()
259+
public void RequestCores_ThrowsNotImplementedWhenCallbacksNotSupported()
255260
{
256261
using TestEnvironment env = TestEnvironment.Create(_output);
257262

258-
// Explicitly do NOT set MSBUILDENABLETASKHOSTCALLBACKS
263+
// Explicitly do NOT set MSBUILDENABLETASKHOSTCALLBACKS — callbacks should be disabled.
264+
// Use RequestCoresWithFallbackTask which catches NotImplementedException like real callers do.
259265
string projectContents = $@"
260266
<Project>
261-
<UsingTask TaskName=""{nameof(RequestCoresTask)}"" AssemblyFile=""{typeof(RequestCoresTask).Assembly.Location}"" TaskFactory=""TaskHostFactory"" />
267+
<UsingTask TaskName=""{nameof(RequestCoresWithFallbackTask)}"" AssemblyFile=""{typeof(RequestCoresWithFallbackTask).Assembly.Location}"" TaskFactory=""TaskHostFactory"" />
262268
<Target Name=""Test"">
263-
<{nameof(RequestCoresTask)} CoreCount=""2"">
264-
<Output PropertyName=""Result"" TaskParameter=""GrantedCores"" />
265-
</{nameof(RequestCoresTask)}>
269+
<{nameof(RequestCoresWithFallbackTask)} CoreCount=""4"">
270+
<Output PropertyName=""GrantedResult"" TaskParameter=""GrantedCores"" />
271+
<Output PropertyName=""FellBack"" TaskParameter=""UsedFallback"" />
272+
</{nameof(RequestCoresWithFallbackTask)}>
266273
</Target>
267274
</Project>";
268275

@@ -274,9 +281,60 @@ public void RequestCores_LogsErrorWhenCallbacksNotSupported()
274281
new BuildParameters { MaxNodeCount = 4, EnableNodeReuse = false, Loggers = [logger] },
275282
new BuildRequestData(projectInstance, targetsToBuild: ["Test"]));
276283

277-
// MSB5022 error should be logged — the callback was not forwarded
278-
logger.ErrorCount.ShouldBeGreaterThan(0);
279-
logger.FullLog.ShouldContain("MSB5022");
284+
// Build must succeed — the task catches NotImplementedException and falls back.
285+
buildResult.OverallResult.ShouldBe(BuildResultCode.Success);
286+
287+
// No errors should be logged — NotImplementedException is caught by the task, not by MSBuild.
288+
logger.ErrorCount.ShouldBe(0);
289+
290+
// The task should have used its fallback path (NotImplementedException was thrown).
291+
logger.FullLog.ShouldContain("RequestCores threw NotImplementedException, using fallback");
292+
293+
// GrantedCores should be the task's own fallback (CoreCount), not 0.
294+
logger.FullLog.ShouldContain("GrantedCores = 4");
295+
}
296+
297+
/// <summary>
298+
/// Regression test for https://github.com/dotnet/msbuild/issues/13333
299+
/// When callbacks are not supported, the full caller pattern (RequestCores with catch,
300+
/// then skip ReleaseCores) must work. This matches MonoAOTCompiler/EmccCompile/ILStrip:
301+
/// try { cores = be9.RequestCores(N); }
302+
/// catch (NotImplementedException) { be9 = null; }
303+
/// finally { be9?.ReleaseCores(cores); }
304+
/// ReleaseCores must NOT be called when the fallback fires (be9 is nulled).
305+
/// </summary>
306+
[Fact]
307+
public void RequestAndReleaseCores_FallbackSkipsReleaseWhenCallbacksNotSupported()
308+
{
309+
using TestEnvironment env = TestEnvironment.Create(_output);
310+
311+
// Explicitly do NOT set MSBUILDENABLETASKHOSTCALLBACKS — callbacks should be disabled
312+
string projectContents = $@"
313+
<Project>
314+
<UsingTask TaskName=""{nameof(RequestCoresWithFallbackTask)}"" AssemblyFile=""{typeof(RequestCoresWithFallbackTask).Assembly.Location}"" TaskFactory=""TaskHostFactory"" />
315+
<Target Name=""Test"">
316+
<{nameof(RequestCoresWithFallbackTask)} CoreCount=""4"" ReleaseAfter=""true"">
317+
<Output PropertyName=""GrantedResult"" TaskParameter=""GrantedCores"" />
318+
<Output PropertyName=""FellBack"" TaskParameter=""UsedFallback"" />
319+
</{nameof(RequestCoresWithFallbackTask)}>
320+
</Target>
321+
</Project>";
322+
323+
TransientTestProjectWithFiles project = env.CreateTestProjectWithFiles(projectContents);
324+
ProjectInstance projectInstance = new(project.ProjectFile);
325+
326+
var logger = new MockLogger(_output);
327+
BuildResult buildResult = BuildManager.DefaultBuildManager.Build(
328+
new BuildParameters { MaxNodeCount = 4, EnableNodeReuse = false, Loggers = [logger] },
329+
new BuildRequestData(projectInstance, targetsToBuild: ["Test"]));
330+
331+
// Build must succeed.
332+
buildResult.OverallResult.ShouldBe(BuildResultCode.Success);
333+
logger.ErrorCount.ShouldBe(0);
334+
335+
// Fallback fired — ReleaseCores should have been skipped (be9 nulled in catch).
336+
logger.FullLog.ShouldContain("RequestCores threw NotImplementedException, using fallback");
337+
logger.FullLog.ShouldNotContain("ReleaseCores(");
280338
}
281339
}
282340
}

src/Build/Instance/TaskFactories/TaskHostTask.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,9 @@ private void HandleIsRunningMultipleNodesRequest(TaskHostIsRunningMultipleNodesR
675675
/// </summary>
676676
private void HandleCoresRequest(TaskHostCoresRequest request)
677677
{
678-
int grantedCores = 0;
678+
// Default to 1 for RequestCores (not 0) to satisfy the API contract (return ∈ [1, requested]).
679+
// For ReleaseCores, 0 is correct as it's just an acknowledgment.
680+
int grantedCores = request.IsRelease ? 0 : 1;
679681

680682
if (request.IsRelease)
681683
{

src/MSBuild/OutOfProcTaskHostNode.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -565,15 +565,17 @@ public IReadOnlyDictionary<string, string> GetGlobalProperties()
565565
public int RequestCores(int requestedCores)
566566
{
567567
#if CLR2COMPATIBILITY
568-
LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
569-
return 0;
568+
// CLR2 task host doesn't support resource management.
569+
// If they somehow get here, throw.
570+
throw new NotImplementedException();
570571
#else
571572
ErrorUtilities.VerifyThrowArgumentOutOfRange(requestedCores > 0, nameof(requestedCores));
572573

573574
if (!CallbacksSupported)
574575
{
575-
LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
576-
return 0;
576+
// Callbacks not available (cross-version scenario). Throw so callers' existing
577+
// catch (NotImplementedException) blocks fire and fall back gracefully.
578+
throw new NotImplementedException();
577579
}
578580

579581
var request = new TaskHostCoresRequest(requestedCores, isRelease: false);
@@ -585,15 +587,14 @@ public int RequestCores(int requestedCores)
585587
public void ReleaseCores(int coresToRelease)
586588
{
587589
#if CLR2COMPATIBILITY
588-
LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
589-
return;
590+
// CLR2 task host doesn't support resource management.
591+
throw new NotImplementedException();
590592
#else
591593
ErrorUtilities.VerifyThrowArgumentOutOfRange(coresToRelease > 0, nameof(coresToRelease));
592594

593595
if (!CallbacksSupported)
594596
{
595-
LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
596-
return;
597+
throw new NotImplementedException();
597598
}
598599

599600
var request = new TaskHostCoresRequest(coresToRelease, isRelease: true);

0 commit comments

Comments
 (0)