Skip to content

[IBuildEngine callbacks] Stage 2: RequestCores/ReleaseCores#13306

Open
JanProvaznik wants to merge 5 commits intodotnet:mainfrom
JanProvaznik:ibuildengine-callbacks-stage2
Open

[IBuildEngine callbacks] Stage 2: RequestCores/ReleaseCores#13306
JanProvaznik wants to merge 5 commits intodotnet:mainfrom
JanProvaznik:ibuildengine-callbacks-stage2

Conversation

@JanProvaznik
Copy link
Member

@JanProvaznik JanProvaznik commented Feb 27, 2026

Context

Stage 2 of implementing IBuildEngine callback support in TaskHost. Stage 1 (#13149) established the packet infrastructure and implemented IsRunningMultipleNodes. This PR adds RequestCores/ReleaseCores (IBuildEngine9) using the same pattern.

Previously, tasks running in TaskHost that called RequestCores or ReleaseCores would get a NotImplementedException. This blocked resource-aware tasks from properly coordinating CPU usage when running out-of-process.

Partial fix for #12863

Changes Made

New Packets:

  • TaskHostCoresRequest (0x22) — single packet type for both operations, with IsRelease bool to distinguish RequestCores vs ReleaseCores
  • TaskHostCoresResponse (0x23) — returns GrantedCores (int) for RequestCores, 0 for ReleaseCores acknowledgment

OutOfProcTaskHostNode (TaskHost side):

  • Replace throw NotImplementedException() with SendCallbackRequestAndWaitForResponse<TaskHostCoresResponse>()
  • Add input validation (requestedCores > 0, coresToRelease > 0) matching in-process behavior
  • Gate behind CallbacksSupported — when disabled, RequestCores returns 0, ReleaseCores is no-op (both log MSB5022)
  • Register TaskHostCoresResponse in packet handler and route in HandlePacket switch

TaskHostTask (Worker node side):

  • Register TaskHostCoresRequest packet handler
  • HandleCoresRequest(): forwards RequestCores/ReleaseCores to the in-process IBuildEngine9
  • The real implicit-core/additional-core accounting happens in the in-process TaskHost class — the OOP node is a thin proxy

Callback Flow

sequenceDiagram
    participant Task as Task Thread<br/>(TaskHost)
    participant Main as Main Thread<br/>(TaskHost)
    participant Worker as TaskHostTask<br/>(Worker Node)

    Task->>Task: BuildEngine.RequestCores(N)
    Task->>Task: Create TaskHostCoresRequest(N, isRelease=false)
    Task->>Task: Store TCS in _pendingCallbackRequests[requestId]
    Task->>Worker: SendData(request)
    Task-->>Task: Block on TCS.Task

    Worker->>Worker: HandleCoresRequest(request)
    Worker->>Worker: granted = _buildEngine.RequestCores(N)
    Worker->>Main: SendData(TaskHostCoresResponse(granted))

    Main->>Main: HandleCallbackResponse(response)
    Main->>Main: _pendingCallbackRequests[requestId].SetResult(response)

    Task-->>Task: TCS unblocks
    Task->>Task: return response.GrantedCores
Loading

Testing

  • Packet serialization (4 tests): Round-trip for request (RequestCores/ReleaseCores variants) and response (granted/acknowledgment)
  • Integration (5 tests):
    • RequestCores_WorksWithExplicitTaskHostFactory — verifies cores granted >= 1
    • RequestAndReleaseCores_WorksWithExplicitTaskHostFactory — full request+release cycle
    • RequestCores_WorksWhenAutoEjectedInMultiThreadedMode — MT auto-ejection to TaskHost
    • RequestCores_LogsErrorWhenCallbacksNotSupported — MSB5022 fallback
    • IsRunningMultipleNodes_LogsErrorWhenCallbacksNotSupported — Stage 1 fallback (unchanged)
  • E2E cross-runtime (1 test): NetTaskHost_CallbackRequestCoresTest via bootstrapped MSBuild
  • All 20 callback/serialization/lifecycle tests pass, 0 regressions

Documentation

Updated taskhost-threading.md with:

  • Supported callbacks table (IsRunningMultipleNodes, RequestCores, ReleaseCores)
  • Resource management flow description

Next Stages

  • Stage 3: BuildProjectFile/BuildProjectFilesInParallel
  • Stage 4: Yield/Reacquire

JanProvaznik and others added 5 commits February 27, 2026 16:57
New IPC packet pair for forwarding IBuildEngine9.RequestCores() and
ReleaseCores() from TaskHost to the owning worker node.

TaskHostCoresRequest (0x22) uses a single packet type with an IsRelease
bool to distinguish RequestCores from ReleaseCores — both operations
share the same int parameter semantics (core count) and response shape
(granted cores or acknowledgment).

TaskHostCoresResponse (0x23) returns GrantedCores for RequestCores calls
and 0 as acknowledgment for ReleaseCores calls.

Both implement ITaskHostCallbackPacket for request/response correlation
via RequestId, following the Stage 1 pattern (TaskHostIsRunningMultipleNodes*).

The NodePacketType enum values 0x22/0x23 were already reserved; this
commit updates their doc comments to use 'owning worker node' terminology.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace 'throw NotImplementedException()' stubs with callback-based
forwarding using the Stage 1 SendCallbackRequestAndWaitForResponse<T>
infrastructure.

Both methods:
- Validate input (requestedCores/coresToRelease > 0) before IPC roundtrip,
  matching the in-process TaskHost's fail-fast behavior
- Check CallbacksSupported gate — when disabled, RequestCores returns 0
  and ReleaseCores is no-op, both logging MSB5022
- Send TaskHostCoresRequest and block on TaskHostCoresResponse via TCS

Register TaskHostCoresResponse handler in the constructor and add the
response routing case to HandlePacket's switch statement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Register TaskHostCoresRequest handler and add HandleCoresRequest() which
forwards calls to the in-process TaskHost's IBuildEngine9 implementation.

The OOP TaskHostNode is a thin proxy — the real implicit-core accounting
and scheduler communication happens in the in-process TaskHost class on
the worker node side. HandleCoresRequest dispatches based on IsRelease:
- RequestCores: forwards to engine9.RequestCores(), returns GrantedCores
- ReleaseCores: forwards to engine9.ReleaseCores(), returns 0 (ack)

When _buildEngine doesn't implement IBuildEngine9 (shouldn't happen in
practice), RequestCores returns 0 and ReleaseCores is no-op — matching
the HandleIsRunningMultipleNodesRequest pattern for IBuildEngine2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
RequestCoresTask is a test task that calls IBuildEngine9.RequestCores(N)
and optionally ReleaseCores. It exposes GrantedCores as an output property
for assertion. Linked into ExampleTask.csproj for E2E cross-runtime use.

TestNetTaskResourceCallback/ is the E2E test project that runs
RequestCoresTask via TaskHostFactory with Runtime='NET', mirroring
the TestNetTaskCallback/ project from Stage 1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Packet serialization (4 tests):
- TaskHostCoresRequest round-trip for RequestCores and ReleaseCores
- TaskHostCoresResponse round-trip for granted cores and acknowledgment

Integration (5 tests):
- RequestCores via explicit TaskHostFactory — verifies granted >= 1
- RequestCores + ReleaseCores full cycle — verifies no errors
- RequestCores auto-ejected in MT mode — verifies TaskHost forwarding
- RequestCores fallback when callbacks disabled — verifies MSB5022
- IsRunningMultipleNodes fallback (Stage 1, unchanged)

E2E cross-runtime (1 test):
- NetTaskHost_CallbackRequestCoresTest via bootstrapped MSBuild

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@JanProvaznik JanProvaznik force-pushed the ibuildengine-callbacks-stage2 branch from 4601b9a to 086f16b Compare February 27, 2026 16:00
JanProvaznik added a commit to JanProvaznik/msbuild that referenced this pull request Feb 27, 2026
- Mark RequestCores/ReleaseCores as implemented in status table
- Rename TaskHostResourceRequest/Response to TaskHostCoresRequest/Response
- Update phased rollout table with PR dotnet#13306
- Replace Stage 2 design considerations with implementation summary

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@JanProvaznik JanProvaznik force-pushed the ibuildengine-callbacks-stage2 branch from 086f16b to 89594d2 Compare February 27, 2026 16:05
@JanProvaznik JanProvaznik marked this pull request as ready for review February 27, 2026 16:08
@JanProvaznik
Copy link
Member Author

@AR-May @rainersigwald this one should be simple, it is conceptually the same as the previous PR, the juicy one will be the next one.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Stage 2 of TaskHost IBuildEngine callback forwarding by implementing IBuildEngine9.RequestCores / ReleaseCores over the existing callback packet infrastructure, enabling resource-aware tasks to run out-of-proc without NotImplementedException.

Changes:

  • Introduces TaskHostCoresRequest / TaskHostCoresResponse packets and new NodePacketType entries.
  • Implements RequestCores / ReleaseCores forwarding in OutOfProcTaskHostNode (TaskHost side) and handles requests in TaskHostTask (worker node side).
  • Adds/updates unit + integration + E2E tests and a new ExampleNetTask asset for the callback.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Shared/TaskHostCoresRequest.cs New callback request packet for RequestCores/ReleaseCores.
src/Shared/TaskHostCoresResponse.cs New callback response packet returning granted cores / ack.
src/Shared/INodePacket.cs Renames/defines packet types for cores request/response (0x22/0x23).
src/MSBuild/OutOfProcTaskHostNode.cs Implements RequestCores/ReleaseCores via callback request/response + handler registration.
src/MSBuild/MSBuild.csproj Includes new shared packet source files.
src/Build/Microsoft.Build.csproj Includes new shared packet source files.
src/Build/Instance/TaskFactories/TaskHostTask.cs Registers/handles cores request packet and forwards to in-proc IBuildEngine9.
src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs Adds round-trip serialization tests for new packets.
src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs Adds integration tests for RequestCores/ReleaseCores through TaskHost.
src/Build.UnitTests/BackEnd/RequestCoresTask.cs Adds a test task that exercises RequestCores/ReleaseCores.
src/Build.UnitTests/NetTaskHost_E2E_Tests.cs Adds E2E test to validate RequestCores callback across runtimes.
src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.csproj Links the new RequestCores test task into ExampleTask.
src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskResourceCallback/TestNetTaskResourceCallback.csproj New test asset project invoking the RequestCores task via TaskHostFactory.
src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskResourceCallback/global.json New global.json for the new test asset.

Comment on lines +275 to +279
new BuildRequestData(projectInstance, targetsToBuild: ["Test"]));

// MSB5022 error should be logged — the callback was not forwarded
logger.ErrorCount.ShouldBeGreaterThan(0);
logger.FullLog.ShouldContain("MSB5022");
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

This test checks that MSB5022 is logged when callbacks are disabled, but it doesn’t assert the expected functional fallback (RequestCores returning 0) or the overall build result (a logged error should typically fail the build). Adding assertions for the returned GrantedCores/Result value and the BuildResult outcome would cover the new behavior more completely.

Copilot generated this review using guidance from repository custom instructions.
@@ -0,0 +1,2 @@
{
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

global.json is currently an empty object. Other ExampleNetTask bootstrap-based test assets include an sdk section (allowPrerelease, rollForward) plus a comment explaining why it exists, to prevent walking up the repo tree and picking up the repo-root SDK settings. Consider matching that established pattern here to avoid non-deterministic SDK selection when running the E2E test from the bootstrap layout.

Suggested change
{
{
// This global.json exists to prevent walking up the repo tree and
// inheriting the repo-root SDK settings when running the E2E test
// from the bootstrap layout. Use the latest installed SDK instead.
"sdk": {
"allowPrerelease": true,
"rollForward": "latestMajor"
}

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +124
<Compile Include="..\Shared\TaskHostCoresRequest.cs" />
<Compile Include="..\Shared\TaskHostCoresResponse.cs" />
Copy link
Member

Choose a reason for hiding this comment

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

It's making me really sad to see us pile up new shared code. Not blocking but I'd appreciate putting some thought into that.

Comment on lines +171 to +177
// Point dotnet resolution to the bootstrap layout so the .NET Core TaskHost
// uses the locally-built MSBuild.dll (with callback support) instead of the system SDK.
string bootstrapCorePath = Path.Combine(RunnerUtilities.BootstrapRootPath, "core");
string bootstrapDotnet = Path.Combine(bootstrapCorePath, "dotnet.exe");
env.SetEnvironmentVariable("DOTNET_HOST_PATH", bootstrapDotnet);
env.SetEnvironmentVariable("DOTNET_ROOT", bootstrapCorePath);
env.SetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR", bootstrapCorePath);
Copy link
Member

Choose a reason for hiding this comment

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

@YuliiaKovalova I feel like you had a less manual mechanism to do this, am I misremembering?

Copy link
Member

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants