Skip to content

ChangeWaves state is not reset between builds in reused nodes (worker nodes and MSBuild Server) #14547

Closed

Description

@ViktorHofer

Issue Description

ChangeWaves caches the parsed value of MSBUILDDISABLEFEATURESFROMVERSION on first use and never re-reads it. In any process that serves more than one build — a reused out-of-proc worker node (node reuse is on by default) or an MSBuild Server node — later builds silently inherit the change-wave setting of the first build that process served.

The worker-node variant is the more serious one: because the entry-point process is fresh each invocation but worker nodes are reused, projects within a single build can disagree about which change waves are enabled, depending on which node happened to evaluate them.

Traits.UpdateFromEnvironment() is already called when a new build is queued on both the server node and the out-of-proc node. ChangeWaves needs the same treatment.

Steps to Reproduce

Four trivial child projects and a traversal project that builds them in parallel:

Child1.proj … Child4.proj (identical apart from the number):

<Project>
  <Target Name="Probe">
    <Message Importance="High" Text="CHILD1 WAVE=[$(MSBuildDisableFeaturesFromVersion)]" />
  </Target>
</Project>

All.proj:

<Project>
  <ItemGroup><C Include="$(MSBuildThisFileDirectory)Child*.proj" /></ItemGroup>
  <Target Name="Probe">
    <MSBuild Projects="@(C)" Targets="Probe" BuildInParallel="true" />
  </Target>
</Project>

Run twice in the same session with different in-rotation wave values (server explicitly off, so this is purely the worker-node path):

set MSBUILDUSESERVER=0

set MSBUILDDISABLEFEATURESFROMVERSION=17.12
dotnet msbuild All.proj -t:Probe -m:4 -v:m

set MSBUILDDISABLEFEATURESFROMVERSION=18.5
dotnet msbuild All.proj -t:Probe -m:4 -v:m

Expected Behavior

The second invocation reports 18.5 for every child, because that is what the environment says.

Actual Behavior

The second invocation reports a mix: the project evaluated in the fresh entry-point process sees the new value, and the projects evaluated on reused worker nodes see the previous build's value.

set=17.12  -> 1:17.12  2:17.12  3:17.12  4:17.12
set=18.5   -> 1:18.5   2:17.12  3:17.12  4:17.12
                       ^^^^^^^^^^^^^^^^^^^^^^^^^ stale, from the previous build

Reproduced identically on 3 consecutive trials.

The same staleness occurs with MSBuild Server (MSBUILDUSESERVER=1), where it is consistent rather than mixed because a single server process serves the whole build:

MSBUILDDISABLEFEATURESFROMVERSION Without server With server
17.0 17.10 17.10
99.0 18.10 17.10 (stuck on the first build's value)
17.0 17.10 17.10

A fresh server session reads the value correctly, so the leak is scoped to process lifetime.

This matters more now that recent .NET SDKs enable MSBuild Server by default (verified on 11.0.100-preview.7.26377.110), but note the worker-node variant needs no server at all and affects the default multi-proc configuration.

Analysis

ChangeWaves.ShouldApplyChangeWave only permits re-evaluation while the wave has never been successfully applied:

private static bool ShouldApplyChangeWave
{
    get
    {
        return ConversionState == ChangeWaveConversionState.NotConvertedYet || _cachedWave == null;
    }
}

Once a build applies a wave, _cachedWave is non-null and ConversionState is no longer NotConvertedYet, so ApplyChangeWave never re-reads the environment variable for the remaining lifetime of the process. The only reset path, ChangeWaves.ResetStateForTests, is test-only (and sets _runningTests under #if DEBUG), and nothing in the node or server request paths calls it.

There is also an ordering hazard worth fixing at the same time: the general refresh mechanism depends on the state that is going stale.

public static void UpdateFromEnvironment()
{
    // Re-create Traits instance to update values in Traits according to current environment.
    if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10))
    {
        _instance = new Traits();
    }
}

If a build ever disables Wave17_10, that gate reads stale ChangeWaves state and Traits stops refreshing for every subsequent build in the process too.

Suggested Fix

Reset ChangeWaves wherever Traits.UpdateFromEnvironment() is already called when a new build is queued:

  • src/Build/BackEnd/Node/OutOfProcNode.cs:510
  • src/Build/BackEnd/Node/OutOfProcNode.cs:749
  • src/Build/BackEnd/Node/OutOfProcServerNode.cs:398

This needs a non-test reset entry point on ChangeWaves (ResetStateForTests sets _runningTests in DEBUG, so it is not appropriate to call from product code), and ChangeWaves must be reset before Traits.UpdateFromEnvironment() so the Wave17_10 gate is evaluated against fresh state.

Suggested regression tests:

  • Two builds in one server session with different MSBUILDDISABLEFEATURESFROMVERSION values; assert the second observes its own value.
  • A multi-proc build with node reuse, run twice with different values; assert all projects in the second build agree and observe the new value.

More broadly, it is worth auditing the remaining environment-derived static state reachable from a build for the same cached-on-first-use-with-no-reset pattern, and giving reused nodes a single explicit "reset per request" path rather than the current scattered resets.

Versions & Configurations

  • Reproduced on dotnet/msbuild main at 867c13677e (local build) and on .NET SDK 11.0.100-preview.7.26377.110.
  • Windows 11 (10.0.26200).
  • Affects both the reused out-of-proc worker node path (no server required, default multi-proc configuration) and MSBuild Server.

Activity

  1. added theissue type on Jul 28, 2026
  2. JanProvaznik commented on Jul 28, 2026

    @JanProvaznik
    Member

    step 1: document MicrosoftDocs/visualstudio-docs#11409
    step 2: changewave should be part of handshake

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

Metadata

Metadata

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions