fix: prevent Aspire manifest generation hang from MSBuild node reuse - #9347
fix: prevent Aspire manifest generation hang from MSBuild node reuse#9347vhvb1989 wants to merge 2 commits into
Conversation
`azd init --from-code` and `azd infra generate` could hang indefinitely (0% CPU, 0 memory growth) while generating the Aspire AppHost manifest on large solutions. Root cause is the os/exec pipe-inheritance deadlock described in golang/go#23019. azd's command runner captures the child's stdout/stderr into bytes.Buffer, so Go creates OS pipes and copy goroutines and cmd.Wait() blocks until they reach EOF. `dotnet run --publisher manifest` builds the AppHost via MSBuild, which spawns persistent worker nodes (nodeReuse:true) that inherit the pipe. When `dotnet run` exits, those nodes stay alive and keep the pipe's write handle open, so EOF never arrives and Wait() blocks forever even though the manifest was already produced. The same command works standalone because stdout is a console, not a pipe. Fix: set MSBUILDDISABLENODEREUSE=1 on the manifest-generation invocation so the worker nodes terminate when the build completes and release the inherited pipe. The env var is scoped to this single call, leaving node reuse intact for all other azd dotnet operations. Cold-start manifest generation shows no measurable slowdown. Fixes #9295 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 911ac071-1fa0-4422-a427-9e1d3d46dfd6
📋 Prioritization NoteThanks for the contribution! The linked issue isn't in the current milestone yet. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). 21 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Prevents Aspire manifest generation hangs by disabling MSBuild worker-node reuse for that invocation.
Changes:
- Sets
MSBUILDDISABLENODEREUSE=1during manifest generation. - Tests both project-based and single-file AppHosts.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
cli/azd/pkg/tools/dotnet/dotnet.go |
Disables MSBuild node reuse for manifest generation. |
cli/azd/pkg/tools/dotnet/dotnet_commands_test.go |
Verifies the environment override is applied. |
Add a file-scoped cspell override for the MSBUILDDISABLENODEREUSE env var name used in pkg/tools/dotnet/dotnet.go. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 911ac071-1fa0-4422-a427-9e1d3d46dfd6
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
Summary
Fixes #9295 —
azd init --from-codeandazd infra generate --forcecould hang indefinitely (0% CPU, no memory growth) while generating the Aspire AppHost manifest on large solutions, even though the samedotnet run --publisher manifestcommand completes in under a second when run standalone.Root cause
This is the classic Go
os/execpipe-inheritance deadlock: golang/go#23019.pkg/exec/command_runner.go) captures the child's stdout/stderr intobytes.Buffer. Because those aren't*os.File, Go creates OS pipes plus background copy goroutines, andcmd.Wait()blocks until those goroutines reach EOF (i.e. until all write handles to the pipe are closed).dotnet run --publisher manifestbuilds the AppHost via MSBuild, which by default spawns persistent worker nodes (nodeReuse:true). These worker nodes inherit azd's stdout/stderr pipe.dotnet runexits, the manifest is already written — but the persistent worker nodes stay alive (default ~15‑min idle timeout) and keep the pipe's write handle open. EOF never arrives, socmd.Wait()blocks forever. The worker nodes are idle, which is exactly why the reporter saw 0% CPU and 0 memory growth across every process in the tree.This reproduces more reliably on large solutions (the issue reports ~54
.csprojfiles) and on Windows, because more referenced projects spawn and retain more parallel MSBuild worker nodes, raising the chance one is still holding the pipe whenWait()is reached.Fix
Set
MSBUILDDISABLENODEREUSE=1on the manifest-generation invocation. With node reuse disabled, MSBuild worker nodes terminate when the build completes, closing the inherited pipe socmd.Wait()returns normally.The env var is scoped to this single
PublishAppHostManifestcall, so node reuse remains intact for all other azd dotnet operations (restore/build/publishduringazd up/deploy).Performance impact
None meaningful. Node reuse only helps subsequent builds reuse warm worker nodes; it does not affect parallelism within a build. Since manifest generation runs once per
azd init/infra generate(a cold, single-shot invocation), there are no pre-warmed nodes to lose. Benchmark (AppHost + 40 referenced projects, warm build cache):MSBUILDDISABLENODEREUSE=1The cold-start case (how azd actually invokes it) shows no measurable slowdown. As a bonus, azd no longer leaves dozens of idle MSBuild worker processes behind after a run.
Testing
bytes.Buffercapture pattern:cmd.Wait()blocked until a grandchild holding the inherited pipe exited.dotnet run --publisher manifestleaves dozens ofnodeReuse:trueMSBuild workers alive after exit; with the fix, zero worker nodes linger.azd init --from-codesucceeds end-to-end with the fixed binary.Test_Cli_PublishAppHostManifestthatMSBUILDDISABLENODEREUSE=1is passed for both project-based and single-file AppHosts.