fix shutdown tests - #129853
Conversation
|
Tagging subscribers to this area: @dotnet/area-extensions-hosting |
There was a problem hiding this comment.
Pull request overview
This PR updates the Microsoft.Extensions.Hosting functional-test deployment infrastructure to make shutdown-related tests more reliable on constrained/Helix environments by hardening process launch and shutdown handling.
Changes:
- Add a retry loop when launching the self-hosted test process, and surface early-exit failures more consistently.
- Resolve the
dotnetmuxer path relative to the currently running shared framework (with PATH fallback) for Helix environments that don’t provide a globaldotnet. - Avoid
Process.HasExitedthrowing during shutdown/cleanup when theProcessobject was never successfully started.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs | Adds start retry logic and reorders startup flow to reduce flaky failures from transient process-launch issues. |
| src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs | Improves dotnet muxer resolution for Helix and hardens shutdown checks against unstarted Process instances. |
|
I'm disabling these tests in crossgen2 and native AOT testing in #130066 because this has been broken for too long and makes it difficult to find real issues. Please remove the disabling and run |
|
/azp run runtime-nativeaot-outerloop |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "25d32f0c830cecd53dcdacbf88f21022a2e5f57b",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "e3f7eec1b9f39efe769c3cd316181110546500f8",
"last_reviewed_commit": "25d32f0c830cecd53dcdacbf88f21022a2e5f57b",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "e3f7eec1b9f39efe769c3cd316181110546500f8",
"last_recorded_worker_run_id": "29679143706",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "25d32f0c830cecd53dcdacbf88f21022a2e5f57b",
"review_id": 4730523149
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: Justified. Linked issue #129832 shows a real, reproducible failure: ShutDownIfAnyHostProcess calls Process.HasExited on a Process that was created but never successfully started, throwing InvalidOperationException ("No process is associated with this object") during Dispose, which masks the real launch failure. The stack trace in the issue matches the code paths touched here.
Approach: Reasonable and targeted for test infrastructure. The IsRunning helper that swallows InvalidOperationException directly addresses the throwing HasExited call; adding throw; in the launch catch surfaces the real start failure instead of hitting the misleading HasExited/ExitCode access later; RunContinuationsAsynchronously avoids inline continuations on the Exited event thread; and GetHostDotNetExecutable resolves the muxer from the running host/shared-framework layout for Helix where dotnet isn't on PATH. These are consistent with the codebase and low-risk.
Summary:
Detailed Findings
✅ Correctness — Core fix is correct
IsRunning (ApplicationDeployer.cs) correctly treats a never-started/disposed Process as not running by catching InvalidOperationException, which is exactly the exception from the issue's stack trace. Replacing both !hostProcess.HasExited checks in ShutDownIfAnyHostProcess with IsRunning makes cleanup non-throwing without masking real state. Adding throw; in StartSelfHostAsync's catch is the right complement: it propagates the genuine launch error rather than letting the subsequent HostProcess.HasExited access throw a misleading InvalidOperationException.
⚠️ Documentation — PR description claims retry logic that is not in the diff
The PR description states: "Add retry logic for self-host process launch (3 attempts with short delay) to tolerate transient CI/Helix launch failures." The current base-to-head diff contains no retry loop — StartAndCaptureOutAndErrToLogger is still called exactly once inside a single try/catch that now rethrows. Please update the description (it notes it was AI-generated) so it matches the actual change, or add the retry if it was intended. As written, the description overstates the change.
💡 Robustness — GetHostDotNetExecutable fallback assumptions (low confidence, test-only)
Two minor, non-blocking observations on the new helper:
- The muxer fallback assumes
dotnetlives exactly three directories abovetypeof(object).Assembly.Location(shared/Microsoft.NETCore.App/<version>→ root). This holds for the standard testhost/dotnet layout and self-contained builds may differ, but the method falls back toDotnetCommandName, so worst case is the prior behavior. Fine as-is. Process.GetCurrentProcess().MainModule?.FileNamecan throw on some restricted platforms; in the normal test host it is fine. Given this is test infrastructure and the failure mode is only a fallback, not worth guarding, but flagging for visibility.
✅ Concurrency — RunContinuationsAsynchronously and reordering
Switching started to TaskCreationOptions.RunContinuationsAsynchronously avoids running the awaiting continuation inline on the process Exited/output-reader thread, which is a good default. Moving the hostExitTokenSource declaration earlier is a no-op reorder with no semantic impact.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 59 AIC · ⌖ 10.9 AIC · ⊞ 10K
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs:46
- Environment.ProcessPath is declared as nullable (string?), so Path.GetFileName(processPath) can throw ArgumentNullException when the process path is unavailable. Since DotNetMuxerPath is meant to be a best-effort probe, this should treat a null ProcessPath as “no muxer found” and return null instead of throwing during type initialization.
var processPath = Environment.ProcessPath;
return string.Equals(Path.GetFileName(processPath), DotNetExecutableName, StringComparison.OrdinalIgnoreCase)
? processPath
: null;
eng/testing/RunnerTemplate.sh:28
- RUNTIME_PATH is now exported, but the assignment is unquoted. Quoting avoids breakage if the runtime path ever contains spaces or shell-special characters, and matches the quoting used in other runner templates (e.g., BionicRunnerTemplate.sh).
# Exported so that tests which need to launch a portable application of their own can find the
# same host the test run itself was handed. RunnerTemplate.cmd's "set" already does this.
export RUNTIME_PATH=$2
shift
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/ApplicationDeployer.cs:103
- This new muxer-resolution helper is only used for launching portable apps, but dotnet publish still hardcodes FileName = "dotnet" in ApplicationPublisher (IntegrationTesting/src/ApplicationPublisher.cs). In Helix environments where dotnet isn’t on PATH, publishing will still fail, so the deployer isn’t fully hardened end-to-end. Consider switching ApplicationPublisher to use the same resolved muxer path (or plumb it in) so both publish and launch work without relying on PATH.
protected static string GetDotNetMuxerPath()
=> DotNetCommands.DotNetMuxerPath
?? throw new Exception($"Unable to find '{DotNetCommands.DotNetExecutableName}'.");
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs:156
- The PR description mentions adding retry logic for self-host process launch (3 attempts) and keeping x86-on-x64 muxer selection behavior unchanged. In the current code, StartSelfHostAsync still calls StartAndCaptureOutAndErrToLogger once with no retry, and the previous architecture-specific dotnet resolution helper was removed. If retries / arch-specific muxer selection are still intended, they should be implemented here; otherwise the PR description should be updated to match the actual change.
try
{
HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger);
}
catch (Exception ex)
{
// Surface the real launch failure instead of letting it be masked later during disposal.
Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString());
throw;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs:151
- The PR description states that self-host process launch has retry logic (e.g., 3 attempts with a short delay), but the current implementation still does a single StartAndCaptureOutAndErrToLogger call and immediately rethrows on failure. Either add the intended retry loop here (ensuring each failed attempt cleans up/terminates any partially-started process) or update the PR description to reflect the actual behavior.
try
{
HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger);
}
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs:21
- DotNetMuxerPath (and FindDotNetMuxer) are documented/implemented to return null when no suitable muxer is available, but the members are declared as non-nullable string. In this repo (nullable enabled), this will either produce nullable warnings or mislead callers into assuming a non-null value. Make the API explicitly nullable (
string?) to match the behavior and existing call sites that already handle null.
/// <summary>
/// Gets the full path of the muxer that portable applications are launched with, or
/// <see langword="null"/> when the current test environment has not got one.
/// </summary>
public static string DotNetMuxerPath { get; } = FindDotNetMuxer();
eng/testing/RunnerTemplate.sh:27
- RUNTIME_PATH assignment should be quoted to avoid word-splitting if the runtime path contains spaces or glob characters. Even if uncommon in Helix, quoting is low-cost and more robust.
# Exported so that tests which need to launch a portable application of their own can find the
# same host the test run itself was handed. RunnerTemplate.cmd's "set" already does this.
export RUNTIME_PATH=$2
shift
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs:30
- FindDotNetMuxer assumes Environment.ProcessPath is non-null, but ProcessPath can be null in some hosting scenarios; Path.GetFileName(null) will throw during type initialization and can break test discovery. Consider returning null when ProcessPath is null, and make the method return type nullable to match the actual return paths.
private static string FindDotNetMuxer()
{
var runtimePath = Environment.GetEnvironmentVariable(RuntimePathVariableName);
if (!string.IsNullOrEmpty(runtimePath))
{
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs:22
- DotNetMuxerPath is documented/used as potentially null (and FindDotNetMuxer returns null in several branches), but the property is declared as non-nullable string. Mark it as nullable to match its actual contract and avoid misleading API surface in this helper class.
This issue also appears on line 26 of the same file.
/// <summary>
/// Gets the full path of the muxer that portable applications are launched with, or
/// <see langword="null"/> when the current test environment has not got one.
/// </summary>
public static string DotNetMuxerPath { get; } = FindDotNetMuxer();
eng/testing/RunnerTemplate.sh:28
- RUNTIME_PATH is assigned from a CLI argument; quoting the value prevents breakage if the runtime path contains spaces or other shell-special characters.
# Exported so that tests which need to launch a portable application of their own can find the
# same host the test run itself was handed. RunnerTemplate.cmd's "set" already does this.
export RUNTIME_PATH=$2
shift
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs:43
- CanDeployTestApplication checks for the app .dll and .deps.json, but launching a framework-dependent app via
dotnet <app>.dllalso requires<app>.runtimeconfig.json. Without checking it, the test can run in layouts where start will inevitably fail (and then be skipped only after a noisy failure).
var applicationPath = Path.Combine(AppContext.BaseDirectory, TestApplicationName);
return File.Exists(applicationPath + ".dll") && File.Exists(applicationPath + ".deps.json");
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs:46
Environment.ProcessPathcan be null/empty in some environments. CallingPath.GetFileName(processPath)without guarding can throw, which would make DotNetMuxerPath initialization fail and potentially break unrelated tests. Guard for null/empty before using it.
var processPath = Environment.ProcessPath;
return string.Equals(Path.GetFileName(processPath), DotNetExecutableName, StringComparison.OrdinalIgnoreCase)
? processPath
: null;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/ShutdownTests.cs:43
CanDeployTestApplicationchecks for*.dlland*.deps.json, but portable execution also requires the app's*.runtimeconfig.json. Without it,dotnet <app>.dllfails even when the other files exist, so the test will run and fail instead of being conditionally skipped.
return File.Exists(applicationPath + ".dll") && File.Exists(applicationPath + ".deps.json");
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Common/DotNetCommands.cs:46
Environment.ProcessPathis nullable, but this code passes it directly toPath.GetFileName(processPath). IfProcessPathis unavailable (e.g., restricted environments or after early access failure), this will throwArgumentNullExceptionduring type initialization and break all callers ofDotNetMuxerPath. Handle the null case explicitly before callingPath.*.
var processPath = Environment.ProcessPath;
return string.Equals(Path.GetFileName(processPath), DotNetExecutableName, StringComparison.OrdinalIgnoreCase)
? processPath
: null;
src/libraries/Microsoft.Extensions.Hosting/tests/FunctionalTests/IntegrationTesting/src/Deployers/SelfHostDeployer.cs:156
- The PR description claims a retry loop for self-host process launch ("3 attempts with short delay"), but
StartSelfHostAsynccurrently logs and rethrows immediately on the firstStartAndCaptureOutAndErrToLoggerfailure. Either implement the described retry behavior here or update the PR description to match the current code.
// Surface the real launch failure instead of letting it be masked later during disposal.
Logger.LogError("Error occurred while starting the process. Exception: {exception}", ex.ToString());
throw;
|
If you are merging from main to get a green CI, it won't work due to dotnet/arcade#17340. You can suppress the failure using ba-g |
|
/ba-g all known issues already filed #132336 |
Fixes #129832
Summary
This change makes the functional test deployer more robust in Helix environments where dotnet is not on PATH and where process start can fail transiently.
Testing
Note
This PR description was generated with AI assistance.