Allow controller extensions to finalize after timeout - #10797
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Note
🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.
✅ 22/22 dimensions clean — no findings.
Review notes:
- Algorithmic Correctness: The timeout ownership move from
CommonServicestoBuildAsync(gated onhost is not TestHostControllersTestHost && !HasTestHostController) correctly ensures only the in-process host applies the application-level cancellation, while the controller manages its own timeout via a separateCancellationTokenSource. Exit code logic correctly distinguishes timeout-aborted from crashed hosts. - Threading & Concurrency:
_skipLifetimeHandlerDisposalis written duringRunTestHostProcessAsyncand read duringDisposeServicesAsync— these execute sequentially (run → dispose), so no synchronization is needed. AllCancellationTokenSourceinstances are properly scoped withusing. - Resource Management:
timeoutCancellationTokenSource,executionCancellationTokenSource, andfinalizationCancellationTokenSourceare allusing-declared. TheTestHostTerminationTimeoutonWaitForExitAsyncprevents an unbounded hang when a test host ignores termination. - Defensive Coding:
TryFinalizeControllerExtensionAsyncapplies double-cancellation (finalization(token)+.WithCancellationAsync(token)) — this is intentionally defensive against handlers that ignore the token internally. - Test Completeness: Two unit tests cover
TryFinalizeControllerExtensionAsync(uncanceled token, bounded finalization). Two acceptance tests cover end-to-end timeout finalization and TRX recovery with aborted-session diagnostics. - Flakiness:
Thread.Sleep(10000)appears in test asset code (simulating a long-running test host), not in test synchronization — acceptable for acceptance tests. - Dimensions 3 (Security), 4 (Public API), 6 (Cross-TFM), 9 (Localization), 18 (Analyzer Quality), 19 (IPC Wire), 20 (Build Infrastructure), 22 (PowerShell) are N/A for this change set.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🤖 Automated content by GitHub Copilot. Generated by the Test Reviewer on PR (on open / sync) workflow. · auto · 94.8 AIC · ⌖ 1.13 AIC · ⊞ 16.9K · ◷
There was a problem hiding this comment.
Pull request overview
Moves process-isolated timeout ownership to the controller, enabling bounded post-timeout extension finalization and TRX recovery.
Changes:
- Separates execution timeout cancellation from controller cleanup.
- Preserves aborted exit semantics and recovers partial TRX results.
- Adds unit and acceptance coverage for timeout finalization.
Show a summary per file
| File | Description |
|---|---|
TestApplicationBuilderTests.cs |
Tests bounded finalization tokens. |
TrxTests.cs |
Tests partial TRX timeout recovery. |
TestHostProcessLifetimeHandlerTests.cs |
Tests uncanceled cleanup tokens. |
TestHostControllersTestHost.ProcessLifecycle.cs |
Implements timeout and finalization flow. |
TestHostControllersTestHost.Disposal.cs |
Avoids disposing active handlers. |
TestHostControllersTestHost.cs |
Adds timeout configuration and state. |
TestHostBuilder.cs |
Assigns timeout ownership by host role. |
TestHostBuilder.CommonServices.cs |
Removes global timeout scheduling. |
TrxProcessLifetimeHandler.cs |
Adds aborted-run TRX diagnostics. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 9/9 changed files
- Comments generated: 3
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.ProcessLifecycle.cs:130
- The execution timer starts after every
OnTestHostProcessStartedAsynccallback, but those callbacks do not block the child: after the PID handshake reply, the child continues building/running independently. A slow start callback can therefore let tests execute beyond--timeoutbefore this line even arms the timer, extending the advertised global execution deadline. Start the controller-owned deadline when the child is launched or connected, while retaining a separate token only for finalization.
timeoutCancellationTokenSource?.CancelAfter(executionTimeout!.Value);
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.ProcessLifecycle.cs:214
- This wrapper bounds only the current await, not the message-bus shutdown itself.
AsynchronousMessageBus.DisableCoreAsyncuses the still-uncanceled application token and waits indefinitely; after this cleanup token expires,DisposeServicesAsynccallsEnsureMessageBusDisabledAsyncand re-awaits that same_disableTaskwithout a timeout. An uncooperative consumer can therefore still hang a timed-out controller indefinitely. Transition the bus to its canceled/bounded shutdown mode, or make the mandatory disposal re-await honor the cleanup deadline.
await messageBusProxy.DisableAsync().WithCancellationAsync(finalizationCancellationToken).ConfigureAwait(false);
src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxProcessLifetimeHandler.cs:209
- The aborted branch updates the TRX diagnostic, but the recovery warning emitted just below still says
Test host crashed...for every recovered/empty sidecar. A normal--timeoutrun will therefore show contradictory termination and crash diagnoses. Reuse this aborted-vs-crashed distinction when constructingrecoverySummary.
string testHostExitInfo = testHostProcessInformation.ExitCode == (int)ExitCode.TestSessionAborted
? $"Test host process pid: {testHostProcessInformation.PID} was terminated because the test session was aborted."
: $"Test host process pid: {testHostProcessInformation.PID} crashed.";
- Files reviewed: 9/9 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.ProcessLifecycle.cs:283
- The timeout wrapper only starts after
finalization(...)has returned aTask. An extension can block synchronously before its firstawait(or return), in which case this call never reachesWithCancellationAsyncand--timeoutremains unbounded. Invoke the callback on a worker before applying the cancellation wrapper; the bounded test should also cover a synchronously blocking delegate.
await finalization(cancellationToken).WithCancellationAsync(cancellationToken).ConfigureAwait(false);
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.ProcessLifecycle.cs:191
- The cleanup budget is created only when at least one lifetime handler exists, but
AddDataConsumeralone can force a controller restart (TestHostControllersManager.cs:155). On timeout the application token remains uncanceled, so the no-handler path reachesDisposeServicesAsyncandAsynchronousMessageBus.DisableCoreAsynctakes its infinite graceful wait. A controller data consumer that does not finish can therefore still hang--timeout; apply the bounded drain/disable path independently of whether lifetime handlers are registered.
using CancellationTokenSource? finalizationCancellationTokenSource = testExecutionCanceled
? new(ControllerExtensionFinalizationTimeout)
: null;
CancellationToken finalizationCancellationToken = finalizationCancellationTokenSource?.Token ?? applicationCancellationToken;
- Files reviewed: 9/9 changed files
- Comments generated: 1
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.ProcessLifecycle.cs:192
- The cleanup budget is created only when at least one lifetime handler exists.
TestHostControllersManageralso restarts the process for controller data consumers or environment-variable providers alone; on timeout in that configuration the application token remains uncanceled, andDisposeServicesAsynclater awaits the message bus's infinite graceful-disable path. An uncooperative controller consumer can therefore still make--timeouthang forever. Apply the bounded shutdown/message-bus transition for every canceled controller run, independently of whether lifetime handlers are registered.
using CancellationTokenSource? finalizationCancellationTokenSource = testExecutionCanceled
? new(ControllerExtensionFinalizationTimeout)
: null;
CancellationToken finalizationCancellationToken = finalizationCancellationTokenSource?.Token ?? applicationCancellationToken;
- Files reviewed: 13/13 changed files
- Comments generated: 2
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
Stop scheduling controller application cancellation after bounded finalization, join one-shot abort callbacks directly, and derive non-success exits from observed child and protocol state. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Respect the shared disposal ledger during the message-bus pre-pass so bounded controller cleanup cannot be re-awaited by the outer shutdown pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review details
Suppressed comments (1)
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.ProcessLifecycle.cs:320
- This diagnostic also runs through
ProxyOutputDevice, so timing out while its server-mode branch is active must protect the proxy itself. Skipping only the original device still allows process-shutdown disposal to callProxyOutputDevice.Dispose()and dispose the nestedServerModePerCallOutputDeviceunderneath the abandoned callback.
if (!_servicesStillRunning.Contains(outputDevice.OriginalOutputDevice))
{
_servicesStillRunning.Add(outputDevice.OriginalOutputDevice);
- Files reviewed: 20/20 changed files
- Comments generated: 2
- Review effort level: Balanced
Transition cancellation arriving during finalization to a shared bounded cleanup token and retain both output-device layers when callbacks are abandoned. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
When bounded termination cannot observe a custom-launched host exit, preserve its handle and dispose it asynchronously only after the exit task completes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.ProcessLifecycle.cs:257
- When application cancellation arrives while an exit handler is running,
finalized == falseonly means the old application token was canceled; the registration above has just created a fresh, uncanceled cleanup source. This branch nevertheless marks the fresh budget as timed out and stops the loop, so later handlers (including TRX recovery) are never invoked and a misleading timeout warning is emitted. After transitioning tokens, abandon only the current handler and continue with the fresh token unless that cleanup token itself has expired.
if (!finalized)
{
_servicesStillRunning.Add(lifetimeHandler);
TransitionToBoundedFinalizationIfCanceled();
_controllerFinalizationTimedOut = true;
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.Disposal.cs:92
- A cleanup that starts before late application cancellation captures
CancellationToken.None, so cancellation arriving whileEnsureMessageBusDisabledAsyncor a disposer is blocked cannot bound that operation. The transition registration inRunTestHostProcessAsynchas already been disposed beforeDisposeServicesAsyncruns, and even creating a new source would not replace this captured token. Use a stable controller-cleanup token whose deadline is armed on abort, or otherwise race in-flight cleanup against the late-created deadline, so cancellation during disposal cannot hang indefinitely.
private async Task<bool> TryRunControllerCleanupAsync(Func<Task> cleanup)
{
CancellationToken cancellationToken = _controllerFinalizationCancellationTokenSource?.Token ?? CancellationToken.None;
return !cancellationToken.IsCancellationRequested
&& await TryRunControllerExtensionAsync(_ => cleanup(), cancellationToken).ConfigureAwait(false);
- Files reviewed: 21/21 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Create the controller cleanup token before finalization, arm its deadline only when cancellation arrives, and keep the transition registration active through service disposal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
🧵 Parallel-safety audit — PR #10797Parallelization — one row per test assembly audited:
Findings: A (global-state) 0 · B (paths) 0 · C (declaration) 0 · D (over-serialization) 0 — by severity: Critical 0 · High 0 · Warning 0 · Info 0. Audited all 5 changed test files:
No changed Advisory only — heuristic, non-blocking. Re-run with
|
🧪 Expert test review — PR #10797
This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Suggestions on the Files changed tab can be applied with one click. Re-run with
|
Fixes #10791
Summary
--timeoutownership exclusively in the test host, including process-isolated runsValidation
net462,net8.0,net10.0)net8.0,net10.0)net8.0,net10.0)