Avoid reporting expected BuildHost shutdowns - #85853
mwiemer-microsoft wants to merge 3 commits into
Conversation
Detach disconnect handlers before intentionally disposing managed BuildHost processes so graceful RPC shutdown cannot be mistaken for a process failure. Add regression coverage for disposal diagnostics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 2 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
|
I'm wondering if this is related to #84412 and may actually explain a more root cause that we were looking for there. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The implementation does not address the demonstrated interleaving, and its test cannot detect the regression.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Attempts to suppress false BuildHost shutdown diagnostics by detaching disconnect handlers.
Changes:
- Detaches handlers before disposing BuildHost processes.
- Adds a shutdown diagnostic test.
Holistic Assessment: The reported issue is valid, but the new unsubscription does not alter the relevant locking outcomes, and the test passes without the fix.
| File | Description |
|---|---|
BuildHostProcessManager.cs |
Detaches handlers during disposal. |
BuildHostProcessManagerTests.cs |
Tests disposal diagnostics. |
| foreach (var process in processesToDispose) | ||
| process.Disconnected -= BuildHostProcess_Disconnected; |
There was a problem hiding this comment.
AI-generated:
Agreed—the original unsubscription does not cover a callback that has already claimed the process. Commit 62907f419e2 now records disposal intent before waiting on the gate and checks it both when the callback takes the gate and immediately before logging after it has released the gate. A callback that already claimed the process remains responsible for disposing it, but does not report an intentional shutdown as a failure. The controlled test failed on the old callback behavior and passes with this guard. I’ve left the thread open for review.
There was a problem hiding this comment.
resolving ai-to-ai thread
There was a problem hiding this comment.
Looking at this carefully, it seems that the original Copilot review in this thread was correct: because _processes was cleared in the Dispose, this race it was imagining cannot occur. The test the coding agent then wrote doesn't really make sense: it's saying if we dispose the build host process manager while we're processing a disconnect (but one that was started after the disconnect was observed) shouldn't trigger any diagnostics. But that's wrong, because we only got into this situation because indeed the process went away first!
Whatever the bug is, this isn't the right fix. I'd propose closing this PR.
| await manager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None); | ||
| await manager.DisposeAsync(); | ||
|
|
||
| await Task.Delay(500); |
There was a problem hiding this comment.
AI-generated:
Agreed. Commit 62907f419e2 replaces the delay with a TestAccessor barrier after the disconnect callback claims the live BuildHost and before it logs. The test starts manager disposal at that barrier, then releases and awaits the callback. It failed against the pre-fix callback logic and passed with the disposal guard; a separate test confirms an unexpected disconnect still reports a failure. The existing Windows test suite and original file-based-app test pass. I’ve left the thread open for review.
|
AI-generated: I looked at #84412 and the underlying #84411 scenario. They involve related disconnect ordering, but I cannot establish that this is the root cause of that crash. This PR addresses a narrower case: during intentional BuildHostProcessManager disposal, the RPC pipe can disconnect before the child has exited, and our disconnect callback can emit a false nonresponsive-process diagnostic. #84412 addresses the BuildHost server side: writing a response after its client disconnects can throw an unhandled IOException, regardless of why the pipe closed. The #84411 report describes sustained LSP activity, not a known manager teardown; requests on an already-cached BuildHost do not themselves trigger the disposal path. Concurrent workspace teardown could connect the two, but we have no evidence it occurred. I think the fixes are complementary. I would retain the server-side protection in #84412 and this client-side intentional-shutdown fix. Logs showing manager teardown during the active session would be needed to confirm a shared trigger. |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e22abb5d-c5f6-4804-852a-4608fb19758f
Use generic TaskCompletionSource instances so the new shutdown-race test compiles on both net10.0 and .NET Framework. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
@RikkiGibson requesting review :) |
|
I didn't debug the tests, but read the description and tried to interpret how the problem was originally occurring:
Is that an accurate sequence of events? One thing doesn't track for me with this--we shouldn't have been able to extract a |
Pull request was converted to draft
|
(marking draft: learning about BuildHost as someone new to the domain, want to get this right instead of fixing symptoms) |
@RikkiGibson Your analysis was correct here (or at least matches my own analysis!) and that matched the Copilot code review analysis too. @mwiemer-microsoft's agent then doubled down on the analysis, by introducing a new test that triggered Dispose() on the BuildHostProcessManager while it's in the middle of processing the early disposal, claiming "that shouldn't log a diagnostic. But of course that should -- the process still failed first! @mwiemer-microsoft I'd recommend closing this PR since it seems your agent is going down a bad path (or at least, it's missing the real root cause and then making things more confusing). |


Ref roslyn-CI failure (link will expire around 2026-10-09):
Full persistent log: 2026-09-30.23-roslyn-CI-log-224-flaky-failure-pr-85853.txt
AI-generated:
Problem
When
BuildHostProcessManagerintentionally shuts down a BuildHost, the RPC stream closes before the OS necessarily reports the child process as exited. The disconnect handler can observeHasExited == falseduring this window and logThe BuildHost process is not responding, even though the BuildHost is exiting normally. That failure diagnostic causedNetCoreTests.TestOpenProject_FileBasedApp_RefDirective_Selfto fail itsAssert.Empty(workspace.Diagnostics)assertion on loaded Linux CI. The CI output ended with the BuildHost's normalRPC channel closed; process exiting.message. RunTests then failed writing its xUnitFailure log becauseartifacts/log/Debugwas missing, obscuring the assertion in the job log; AzDO's structured test result retained it.Pre-fix reproduction command
This repeats the exact test that failed in Linux CI; on an unpatched checkout, Linux or a heavily loaded machine may expose the timing race:
Reproduction limitation: this race is timing-dependent, not deterministic. The available machine is Windows, not Linux. On the unpatched code it passed 20 sequential runs and 40 runs with four parallel testhosts; the original disposal test also passed 80/80 pre-fix runs. The failure itself was observed in the loaded Linux CI run. Thus this command represents the failing test and may reproduce under suitable Linux load, but we cannot claim it reliably fails locally.
Fix
Manager disposal records its intent before waiting on the process-map gate. Disconnect callbacks check that intent both when claiming a process and just before logging, including the case where a callback already claimed it. Detaching handlers during disposal avoids subsequent callbacks; unexpected disconnects still report failure. The revised regression test uses a barrier after the disconnect callback claims a running BuildHost but before logging; it starts manager disposal and then releases and awaits the callback. This controlled test failed against the previous callback behavior and passes with the new guard. A companion test verifies genuine unexpected disconnects still report failure.
Validation
BuildHostProcessManagerTestsplus the original file-based-app test: 27 passed. Debug/net472 focused tests: 2 passed.TaskCompletionSource, unavailable on net472. Commit056fdef2460corrected the cross-target test; both net472 and net10.0 builds and tests passed locally.056fdef2460passed, including Linux Debug tests, Windows Helix, analyzer/correctness, and integration checks.