Skip to content

Node reuse over-provisioning heuristic keeps zero nodes when builds share a machine #15060

Description

@ViktorHofer

DetermineNodesForReuse (added in #13220) compares a system-wide node count against 1.5 x logical cores. That assumes one build owns the machine. When several MSBuild builds run concurrently, every build ends up terminating all of its own nodes, so node reuse stops working.

Seen in the .NET VMR, which builds up to 8 repos in parallel, each running its own MSBuild. On an 8-core agent (threshold 12), comm traces show:

System-wide node count: 33, threshold: 12, this instance has: 16 nodes
Keeping 0 of 16 nodes in this instance to help meet threshold of 12

System-wide counts of 25/31/33/43 were normal for that build.

An instance can only terminate its own nodes

nodesToKeepInThisInstance = Math.Max(0, maxNodesToKeep - (systemWideNodeCount - nodeCount)) -> Max(0, 12 - 17) = 0.

The 17 nodes belonging to other builds are what pushed it over the limit, and they survive untouched. So the just-warmed nodes get killed while older idle ones persist, and the next build to finish does the same. It never converges.

An idle-expiry mechanism already exists

NodeEndpointOutOfProcBase.WaitForConnection waits CommunicationsUtilities.NodeConnectionTimeout for a host to reconnect and then exits (NodeEndpointOutOfProcBase.cs L419, "Connection timed out waiting a host to contact us"). The default:

// CommunicationsUtilities.cs L45
private const int DefaultNodeConnectionTimeout = 900 * 1000; // 15 minutes; enough time that a dev will typically do another build in this time

15 minutes, explicitly tuned for an interactive developer. In CI nobody "does another build in this time", so every orphaned node lingers for 15 minutes and the system-wide count climbs. That is most of the over-provisioning this feature is reacting to.

Suggestions

  • Shorten the idle timeout of excess nodes rather than terminating them. NodeBuildComplete already carries PrepareForReuse, so it could also carry a timeout. Over-provisioned nodes then expire on their own in seconds instead of 15 minutes, warm nodes stay warm, and no instance has to make a global decision from local information.
  • Lower the default timeout when ContinuousIntegrationBuild is set.
  • Never let an instance go to zero: Math.Min(nodeCount, maxNodesToKeep) instead of the expression above. Bounded and convergent, and it stops sacrificing warm nodes for orphans the instance does not control.
  • Add an escape hatch (e.g. MSBUILDNODEREUSETHRESHOLD, 0 = disabled). There is currently none: disabling change wave 18.5 makes things worse, because GetPossibleRunningNodes then returns every MSBuild.exe process unfiltered.

src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs L697-L745 - affects v11.0.0+ (repro on SDK 11.0.100-rc.1.26420.103)

Related: #15061 covers a second problem with the same feature: the node count is not filtered to handshake-compatible nodes.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions