Fix MSBuild Server client dropping build result under WaitAny race (#14172) - #14251
Conversation
…otnet#14172) The MSBuild Server client's packet loop waits on WaitAny([cancel, PacketPumpCompleted, PacketReceivedEvent]). PacketPumpCompleted is a sticky ManualResetEvent at a lower index than PacketReceivedEvent, and the pump sets it immediately after enqueuing the final ServerNodeBuildResult. If the main loop reaches WaitAny after both are signaled, it took the completed branch, which set _buildFinished without draining the queue - dropping the build result (and trailing console writes such as the 'Build succeeded.' summary). The result's exit type was then null, so MSBuildClientApp returned MSBuildClientFailure and the process exited 1 on a successful build. This regressed in dotnet#7852, which changed the index-1 handle from an error-only event to the always-set PacketPumpCompleted without making the handler drain the queue. Fix: drain the received-packets queue before honoring PacketPumpCompleted (extracted into ProcessPacketsUntilBuildFinished + a shared DrainPacketQueue helper). A delivered result is honored even if the pump also recorded an exception, and queued console output is flushed on all paths. Adds a deterministic regression test that fails on the pre-fix loop and passes on the fixed one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… is forwarded The test seeded a 'Build succeeded.' console write before the build result but only asserted the exit result, not the actual user-visible symptom dotnet#14172 caused (the dropped summary). Capture Console.Out and assert the trailing console packet is flushed after the result is drained; this also keeps the seeded write out of the real test-run output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expert code review — 24-dimension analysisReviewed with the repo's Why it's correctDraining the received-packets queue in both Threading is single-producer (pump thread enqueues + sets events) / single-consumer ( Empirical verificationChecked out the branch, ran a full Follow-up commitPushed Per-dimension results
Optional nits (non-blocking)
Generated with the GitHub Copilot CLI expert-reviewer methodology. |
There was a problem hiding this comment.
Pull request overview
Fixes an MSBuild Server (/mt) client-side race where WaitHandle.WaitAny can observe PacketPumpCompleted before draining a queued ServerNodeBuildResult, causing a successful build to exit 1 with the final success summary dropped.
Changes:
- Refactors the packet-processing loop into
ProcessPacketsUntilBuildFinishedand adds a sharedDrainPacketQueuehelper so queued packets are drained before honoringPacketPumpCompleted. - Ensures a delivered
ServerNodeBuildResultis processed even if the pump also completed (and would otherwise end the loop early). - Adds a regression unit test that deterministically seeds the pump/events to reproduce the race and validates the result + console write are not dropped.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Build/BackEnd/Client/MSBuildClient.cs | Drains the received-packets queue before treating the packet pump as completed; adds helper + test hook to prevent dropping the final build result under the WaitAny ordering race. |
| src/Build.UnitTests/BackEnd/MSBuildClient_Tests.cs | Adds deterministic regression coverage to ensure the queued build result and success summary are not lost when PacketPumpCompleted and PacketReceivedEvent are both signaled. |
Make the MSBuild Server the default instead of a special opt-in, per feedback. SDK (src/sdk): MSBuildForwardingAppWithoutLogging now defaults the server ON - it passes MSBUILDUSESERVER=1 when the user hasn't set it explicitly (an explicit value, e.g. "0", is respected; DOTNET_CLI_USE_MSBUILD_SERVER=false opts out of the default-on behavior). This replaces the VMR-injected MSBUILDUSESERVER=1. VMR wiring (repo-projects): - Directory.Build.props: remove the DotNetBuildMSBuildServer opt-in flag, the UseMSBuildServer computation and the MSBuildServerExcludedRepos/override exclusion machinery, and the explicit MSBUILDUSESERVER=1 injection. Node reuse is now enabled by default for every repo (only a repo setting <UseNodeReuse>false</UseNodeReuse> disables it - which is exactly what turns the server off for that repo, since the server requires node reuse). The per-repo handshake salt is kept (MSBUILDNODEHANDSHAKESALT=dotnet-vmr-<repo>) so parallel repo servers don't collide. - Directory.Build.targets: keep the per-repo salt-scoped `build-server shutdown --msbuild` cleanup, now gated on UseNodeReuse != false instead of the removed flag. - wpf.proj: remove the UseMSBuildServer=false exclusion. wpf failed only due to the MSBuild Server exit-code bug fixed by dotnet/msbuild#14251, which is now merged and present after merging upstream main. Root Directory.Build.props: remove the validation-only DotNetBuildMSBuildServer=true default (the server is now default-on via the SDK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fixes Bumps the SDK-redistributed MSBuild from 18.10.0-preview-26359-117 (on main) to 18.10.0-1.26360.102, which includes dotnet/msbuild#14251 (fixes dotnet#14172): the MSBuild Server client dropping the build result under a WaitAny race, causing a successful build to exit 1 on Linux under load. Also includes the earlier TerminalLogger-under-server fix (dotnet#14077). This MSBuild adds [RequiresUnreferencedCode] to ProjectInstance.Build/BuildManager APIs, so add IL2026 suppressions at the CLI's non-AOT MSBuild call sites (matching the existing IL3050 suppression pattern). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 499249b7-514f-4ecf-88bc-2466455069c5
…fixes Bumps the SDK-redistributed MSBuild from 18.10.0-preview-26359-117 (on main) to 18.10.0-1.26360.102, which includes dotnet/msbuild#14251 (fixes dotnet#14172): the MSBuild Server client dropping the build result under a WaitAny race, causing a successful build to exit 1 on Linux under load. Also includes the earlier TerminalLogger-under-server fix (dotnet#14077). This MSBuild adds [RequiresUnreferencedCode] to ProjectInstance.Build/BuildManager APIs, so add IL2026 suppressions at the CLI's non-AOT MSBuild call sites (matching the existing IL3050 suppression pattern). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 499249b7-514f-4ecf-88bc-2466455069c5
Updated [Microsoft.Build.Utilities.Core](https://github.com/dotnet/msbuild) from 18.9.6 to 18.10.1. <details> <summary>Release notes</summary> _Sourced from [Microsoft.Build.Utilities.Core's releases](https://github.com/dotnet/msbuild/releases)._ ## 18.10.1 ## What's Changed * [vs16.11] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13103 * [vs17.12] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13796 * [vs17.8] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13902 * [vs17.11] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13903 * [vs17.12] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13909 * [vs17.12] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13986 * Add vs18.9 to merge-flow config; retire vs18.3 by @JanProvaznik in dotnet/msbuild#14214 * Bump labeler-cache-retention to use issue-labeler v2.1.0 by @jeffhandley in dotnet/msbuild#14171 * Bump main to 18.10.0 after vs18.9 snap by @JanProvaznik in dotnet/msbuild#14216 * Improve release skill: Phase 2 DARC rules, VMR backflow, deterministic baseline by @JanProvaznik in dotnet/msbuild#14220 * Determinize release: hardcode OptProf baseline + Phase 3.2 baseline resolver by @JanProvaznik in dotnet/msbuild#14222 * Serialize BuildRequestConfiguration.RequestedTargets to fix solution metaproject MSB4057 in parallel builds by @ViktorHofer in dotnet/msbuild#14223 * [main] Update dependencies from nuget/nuget.client by @dotnet-maestro[bot] in dotnet/msbuild#14203 * Core support for AbsolutePath/FileInfo/DirectoryInfo and ITaskItem<T> as task parameters by @baronfel in dotnet/msbuild#13971 * [main] Update dependencies from dotnet/roslyn by @dotnet-maestro[bot] in dotnet/msbuild#14206 * Fix existence cache kind poisoning by @AlesProkop in dotnet/msbuild#14249 * [main] Source code updates from dotnet/dotnet by @dotnet-maestro[bot] in dotnet/msbuild#14226 * Don't disable the MSBuild server for /mt builds when node reuse is off by @AR-May in dotnet/msbuild#14248 * Enhance expert reviewer guidelines with additional checks. by @AR-May in dotnet/msbuild#14255 * [main] Source code updates from dotnet/dotnet by @dotnet-maestro[bot] in dotnet/msbuild#14253 * [main] Update dependencies from dotnet/roslyn by @dotnet-maestro[bot] in dotnet/msbuild#14268 * [main] Update dependencies from nuget/nuget.client by @dotnet-maestro[bot] in dotnet/msbuild#14267 * Bump github/gh-aw-actions/setup from 0.81.6 to 0.82.2 by @dependabot[bot] in dotnet/msbuild#14266 * Avoid boxing the struct enumerator in PropertyDictionary<T>.GetEnumerator() by @nareshjo in dotnet/msbuild#14272 * Refresh copy marker when implementation output changes by @AlesProkop in dotnet/msbuild#14231 * Send task-host build process environment as delta by @OvesN in dotnet/msbuild#14126 * Add regression coverage for metadata newline preservation by @VolPlita in dotnet/msbuild#14261 * Fix EmbedInBinlog items with relative paths from child projects by @huulinhnguyen-dev in dotnet/msbuild#13990 * Stop requiring VersionPrefix updates in servicing - insert prerelease versions to VS by @ViktorHofer in dotnet/msbuild#14277 * Fix WriteLinesToFile rewriting unchanged file when custom encoding is used by @huulinhnguyen-dev in dotnet/msbuild#14146 * Enable trim/AOT analyzers for Microsoft.Build and clean up annotations by @JeremyKuhne in dotnet/msbuild#14064 * [automated] Merge branch 'vs18.9' => 'main' by @github-actions[bot] in dotnet/msbuild#14291 * Fix MicroBuild plugin feed URL to use allowed pkgs.dev.azure.com format by @AlesProkop in dotnet/msbuild#14295 * Pass ExcludeRestorePackageImports during restore to avoid redundant evaluations by @ViktorHofer with @Copilot in dotnet/msbuild#14274 * [vs18.7] Update dependencies from dotnet/arcade by @dotnet-maestro[bot] in dotnet/msbuild#13988 * Adopt Clever Test Selection (CTS) as parallel, non-blocking PR pipeline by @jankratochvilcz in dotnet/msbuild#14212 * Harden exceptions when connecting to server by @JanProvaznik in dotnet/msbuild#14292 * Update MicrosoftBuildVersion in analyzer template by @github-actions[bot] in dotnet/msbuild#13886 * Fix MSBuild Server client dropping build result under WaitAny race (#14172) by @JanProvaznik in dotnet/msbuild#14251 * Partially revert #13660: remove NuGet RestoreTask transient TaskHost workaround by @JanProvaznik in dotnet/msbuild#14297 * Disable daily AI credits guardrail for Expert Code Review workflow by @JanProvaznik with @Copilot in dotnet/msbuild#14314 * Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID 14614733 by @dotnet-bot in dotnet/msbuild#14246 * Add opt-in partial (stop-after-pass) project evaluation by @ViktorHofer in dotnet/msbuild#14290 * Use partial evaluation for -getProperty/-getItem without a target by @ViktorHofer in dotnet/msbuild#14296 * [main] Source code updates from dotnet/dotnet by @dotnet-maestro[bot] in dotnet/msbuild#14324 * [main] Update dependencies from dotnet/roslyn by @dotnet-maestro[bot] in dotnet/msbuild#14333 * [main] Update dependencies from nuget/nuget.client by @dotnet-maestro[bot] in dotnet/msbuild#14330 * Bump github/gh-aw-actions/setup from 0.82.2 to 0.82.8 by @dependabot[bot] in dotnet/msbuild#14328 * Restrict partial evaluation to ProjectInstance by @ViktorHofer in dotnet/msbuild#14340 ... (truncated) Commits viewable in [compare view](dotnet/msbuild@v18.9.6...v18.10.1). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
Addresses #14172: on Linux with MSBuild Server (
/mt), a build that succeeds intermittently makes the entry process exit 1 with the finalBuild succeeded.summary dropped (no error printed).Root cause
The MSBuild Server client's packet loop waits on
WaitAny([cancellation, PacketPumpCompleted, PacketReceivedEvent]):PacketPumpCompletedis a stickyManualResetEventat a lowerWaitAnyindex thanPacketReceivedEvent.ServerNodeBuildResult(settingPacketReceivedEvent) and then, a few instructions later, setsPacketPumpCompleted.WaitAnywhen both are already signaled, it returns the lower index and runsHandlePacketPumpCompleted, which set_buildFinished = truewithout draining the queue — dropping the queuedServerNodeBuildResultand any trailing console writes (e.g. theBuild succeeded.summary).MSBuildAppExitTypeStringstaysnull, soMSBuildClientApp.ExecutereturnsMSBuildApp.ExitType.MSBuildClientFailure→ the process exits 1 on a successful build.This regressed in #7852, which changed the index‑1 wait handle from an error‑only event to the always‑set
PacketPumpCompletedwithout making the handler drain the received-packets queue. Before that change, the low‑priority handle only fired on genuine errors, so a successful result was always drained via thePacketReceivedEventbranch.Fix
Drain the received-packets queue before honoring
PacketPumpCompleted:ProcessPacketsUntilBuildFinishedand added a sharedDrainPacketQueuehelper used by both thePacketReceivedEventandPacketPumpCompletedbranches.DrainPacketQueue, thenif (!_buildFinished) HandlePacketPumpCompleted(...)), and queued console output is flushed on all completion paths.Testing
WaitAnyafter both events are set (the descheduling that CPU saturation causes naturally), the pre‑fix binary reproduces the exact symptom — exit 1,Build succeeded.dropped, no error, output truncated, with the trace confirming theServerNodeBuildResultwas still queued. With the fix, the same scenario exits 0 with the summary shown, and normal builds are unaffected. (Scaffolding not included in this PR.)MSBuildClient_Tests.ProcessPackets_WhenPumpCompletedRacesQueuedResult_DoesNotDropBuildResult): drives the real loop with the result + summary queued and both events pre‑signaled; fails on the pre‑fix loop, passes on the fixed one.Notes on reproduction confidence
The field symptom only reproduces naturally under heavy CPU load on ~32‑core Linux (~2.3%); it did not reproduce end‑to‑end on a 16‑vCPU box. The mechanism is confirmed by the deterministic forced‑race and by ruling out the two competing hypotheses in code (the server writes the result before closing the pipe —
Join()beforeDispose()with drain‑on‑terminate — so it is always delivered; and the build's exit type is frozen before the result is sent, so late node/TaskHost teardown cannot corrupt it). A failing‑build clientMSBUILDDEBUGCOMMtrace would make the attribution airtight; this change removes a real latent defect matching the symptom with no behavioral regression.