Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ overrides:
- compatResult
- compatCopy
- Incompat
- filename: pkg/tools/dotnet/dotnet.go
words:
- MSBUILDDISABLENODEREUSE
- filename: pkg/output/pretty_table.go
words:
- runewidth
Expand Down
17 changes: 12 additions & 5 deletions cli/azd/pkg/tools/dotnet/dotnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,17 +202,24 @@ func (cli *Cli) PublishAppHostManifest(

runArgs = runArgs.WithCwd(filepath.Dir(hostProject))

// Disable MSBuild node reuse for this invocation. `dotnet run --publisher manifest` builds the AppHost via
// MSBuild, which by default spawns persistent worker nodes (nodeReuse:true) that survive after `dotnet run`
// exits. Those worker nodes inherit the stdout/stderr pipe that azd's command runner reads from, so the pipe
// never reaches EOF and cmd.Wait() blocks forever even though the manifest was already produced. This is the
// os/exec deadlock described in https://github.com/golang/go/issues/23019 and reported in azd issue #9295
// (hangs with 0% CPU on large solutions where many worker nodes are spawned). Disabling node reuse makes the
// worker nodes terminate when the build completes, closing the inherited pipe.
envArgs := []string{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoping the env var to this one call means Restore, Build, Publish, BuildContainerLocal, and PublishContainer still run MSBuild with node reuse on, through the same commandRunner.Run path that captures stdout/stderr into bytes.Buffer. The deadlock exposure there looks identical to what's described for manifest generation.

The PR description frames leaving them alone as a performance decision, but it doesn't cover the hang risk. On the same 54-project solution from #9295, what keeps dotnet publish during azd deploy from wedging the same way? If the answer is that it can and it just hasn't been reported yet, that's worth noting on the issue so it isn't rediscovered later.

"MSBUILDDISABLENODEREUSE=1",
}

// AppHost may conditionalize their infrastructure based on the environment, so we need to pass the environment when we
// are `dotnet run`ing the app host project to produce its manifest.
var envArgs []string

if dotnetEnv != "" {
envArgs = append(envArgs, fmt.Sprintf("DOTNET_ENVIRONMENT=%s", dotnetEnv))
}

if envArgs != nil {
runArgs = runArgs.WithEnv(envArgs)
}
runArgs = runArgs.WithEnv(envArgs)

_, err := cli.commandRunner.Run(ctx, runArgs)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions cli/azd/pkg/tools/dotnet/dotnet_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ func Test_Cli_PublishAppHostManifest(t *testing.T) {
require.Contains(t, captured.Args, "--output-path")
require.Contains(t, captured.Args, "out.json")
require.Contains(t, captured.Env, "DOTNET_ENVIRONMENT=Development")
// Node reuse must be disabled to avoid the os/exec pipe-inheritance hang (issue #9295).
require.Contains(t, captured.Env, "MSBUILDDISABLENODEREUSE=1")
})

t.Run("single file apphost", func(t *testing.T) {
Expand All @@ -349,6 +351,7 @@ func Test_Cli_PublishAppHostManifest(t *testing.T) {
require.Equal(t, filepath.Dir(hostProject), captured.Cwd)
require.Equal(t, "apphost.cs", captured.Args[1])
require.NotContains(t, captured.Args, "--project")
require.Contains(t, captured.Env, "MSBUILDDISABLENODEREUSE=1")
})

t.Run("run error", func(t *testing.T) {
Expand Down
Loading