CountActiveNodesWithMode / GetPossibleRunningNodes filter only on process name MSBuild plus NodeMode.OutOfProcNode in the command line. Nothing checks whether a counted node is actually usable by the counting instance, so the count includes nodes that instance can never reuse. That count then feeds DetermineNodesForReuse, which terminates the instance's own nodes.
Measured on a .NET VMR CI agent building up to 8 repos in parallel, 8 cores, threshold 12. Counts logged at build completion:
| repo build |
system-wide count |
owned by this instance |
kept |
| arcade |
18 |
8 |
2 |
| symreader |
30 |
4 |
0 |
| xdt |
31 |
4 |
0 |
| cecil |
33 |
6 |
0 |
| emsdk |
46 |
4 |
0 |
| nuget-client |
59 |
8 |
0 |
| fsharp |
69 |
14 |
0 |
Two independent reasons a counted node is unusable, both observed:
1. The node is busy. It is attached to another concurrently running build's host, so the pipe connect times out. In a set of repo builds that all shared one toolset (.dotnet/sdk/11.0.100-rc.1.26420.103), probing produced 147 pipe-connect timeouts and 0 handshake rejections. The probing path already anticipates this:
// A candidate belongs to another build and may exit, be killed, hit its idle timeout,
// or refuse a handshake at any moment, so probing it is inherently racy.
2. The node belongs to a different toolset. Repos that use VS MSBuild for part of their build run two toolsets side by side - C:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Current\Bin and the bootstrapped SDK - within a single repo build. Because toolsDirectory is part of the handshake salt, those nodes correctly reject each other. In those legs probing produced 60 handshake rejections ("Handshake failed on part 1. Probably the client is a different MSBuild build") against 34 timeouts.
Note this needs no MSBUILDNODEHANDSHAKESALT; the default salt already encodes the tools directory, so the rejections are the handshake working as designed.
Either way the node is not a reuse candidate, and either way it still counts toward the threshold that terminates nodes which are reuse candidates.
The instance already has this information, then discards it
Every probed candidate is recorded in _processesToIgnore (L370), keyed {handshake}|{pid}. ShutdownConnectedNodes clears that dictionary (L117) and calls DetermineNodesForReuse nine lines later (L126).
Suggestions
- Move the
_processesToIgnore clear after the reuse decision and subtract those PIDs from the count. Nearly free, and it covers both cases above, since both end up recorded there.
- Count only nodes that are genuinely reuse candidates. A busy node belongs to a live build and will be handled by its own owner at completion.
- Longer term, fold a handshake hash into the pipe name (for example
MSBuild.{handshakeHash}.{pid}) so compatible nodes can be counted by enumerating pipe names, with no process enumeration and no connect attempt. That makes case 2 free to detect.
src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs L763-L841 - affects v11.0.0+
Related: #15060
CountActiveNodesWithMode/GetPossibleRunningNodesfilter only on process nameMSBuildplusNodeMode.OutOfProcNodein the command line. Nothing checks whether a counted node is actually usable by the counting instance, so the count includes nodes that instance can never reuse. That count then feedsDetermineNodesForReuse, which terminates the instance's own nodes.Measured on a .NET VMR CI agent building up to 8 repos in parallel, 8 cores, threshold 12. Counts logged at build completion:
Two independent reasons a counted node is unusable, both observed:
1. The node is busy. It is attached to another concurrently running build's host, so the pipe connect times out. In a set of repo builds that all shared one toolset (
.dotnet/sdk/11.0.100-rc.1.26420.103), probing produced 147 pipe-connect timeouts and 0 handshake rejections. The probing path already anticipates this:2. The node belongs to a different toolset. Repos that use VS MSBuild for part of their build run two toolsets side by side -
C:\Program Files\Microsoft Visual Studio\18\Enterprise\MSBuild\Current\Binand the bootstrapped SDK - within a single repo build. BecausetoolsDirectoryis part of the handshake salt, those nodes correctly reject each other. In those legs probing produced 60 handshake rejections ("Handshake failed on part 1. Probably the client is a different MSBuild build") against 34 timeouts.Note this needs no
MSBUILDNODEHANDSHAKESALT; the default salt already encodes the tools directory, so the rejections are the handshake working as designed.Either way the node is not a reuse candidate, and either way it still counts toward the threshold that terminates nodes which are reuse candidates.
The instance already has this information, then discards it
Every probed candidate is recorded in
_processesToIgnore(L370), keyed{handshake}|{pid}.ShutdownConnectedNodesclears that dictionary (L117) and callsDetermineNodesForReusenine lines later (L126).Suggestions
_processesToIgnoreclear after the reuse decision and subtract those PIDs from the count. Nearly free, and it covers both cases above, since both end up recorded there.MSBuild.{handshakeHash}.{pid}) so compatible nodes can be counted by enumerating pipe names, with no process enumeration and no connect attempt. That makes case 2 free to detect.src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.csL763-L841 - affects v11.0.0+Related: #15060