Skip to content

Conversation

@thomhurst
Copy link
Owner

Fixes #4117

Copilot AI review requested due to automatic review settings December 22, 2025 12:03
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the robustness of global timeout cancellation handling by simplifying the forceful exit mechanism in EngineCancellationToken. The key changes address issue #4117 by removing the separate _forcefulExitCts cancellation token source and extending the forceful exit timeout from 10 to 30 seconds.

Key Changes:

  • Removed _forcefulExitCts field and its associated cancellation/disposal logic for a simpler design
  • Added Token.Register callback to trigger Cancel() when the linked token is cancelled
  • Increased forceful exit timeout from 10 to 30 seconds and made it non-cancellable via CancellationToken.None

CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
Token = CancellationTokenSource.Token;

Token.Register(_ => Cancel(), this);
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

Registering a callback that calls Cancel() when the token is cancelled creates a potential issue. When the external cancellation token (passed to Initialise) is cancelled, it will trigger this callback, which calls Cancel(). The Cancel() method then calls CancellationTokenSource.Cancel() on line 56.

While CancellationTokenSource.CreateLinkedTokenSource already propagates cancellation automatically, this explicit registration adds the side effect of starting the forceful exit timer whenever ANY linked token is cancelled. This may be intentional for global timeout scenarios, but it means the forceful exit timer will start even for normal test completion if the parent token is cancelled.

Consider whether this behavior is intended for all cancellation scenarios, or if the forceful exit timer should only start for specific cancellation types (e.g., Ctrl+C). If this is intentional, add a comment explaining why the registration is needed despite the linked token source.

Suggested change
Token.Register(_ => Cancel(), this);
// Cancellation is already propagated via the linked token source above.
// We intentionally do not register a callback here that calls Cancel(),
// to avoid starting the forceful exit timer for every linked-token cancellation.
// The forceful exit timer is reserved for specific signals (e.g. Ctrl+C).

Copilot uses AI. Check for mistakes.

// Start a new forceful exit timer
_ = Task.Delay(TimeSpan.FromSeconds(10), _forcefulExitCts.Token).ContinueWith(t =>
_ = Task.Delay(TimeSpan.FromSeconds(30), CancellationToken.None).ContinueWith(t =>
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The forceful exit timeout has been changed from 10 seconds to 30 seconds, and the cancellation token has been changed from a cancellable token to CancellationToken.None. This means once the forceful exit is triggered, it cannot be cancelled or reset, even if the process exits gracefully.

The removal of _forcefulExitCts means there's no way to cancel this timer if the test run completes normally after cancellation is requested but before the 30-second timeout. This could lead to unnecessary process termination in scenarios where cleanup completes successfully.

Consider:

  1. Documenting why 30 seconds was chosen (was 10 seconds too short?)
  2. Keeping the ability to cancel the forceful exit timer if the process exits gracefully before the timeout
  3. Using the existing CancellationTokenSource.Token instead of CancellationToken.None so the timer can be cancelled during disposal

Copilot uses AI. Check for mistakes.
Comment on lines 60 to 62
if (!_forcefulExitStarted)
{
_forcefulExitStarted = true;
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

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

The _forcefulExitStarted volatile flag is checked and set without proper synchronization. While the volatile keyword ensures visibility across threads, it doesn't provide atomicity for the check-then-set operation. This creates a race condition where multiple threads could simultaneously check _forcefulExitStarted as false, both set it to true, and both start separate forceful exit timers.

This could result in multiple Environment.Exit(1) calls being scheduled, though only one would execute. More importantly, it violates the intent that the forceful exit timer should only be started once.

Use Interlocked.CompareExchange to atomically check and set the flag, ensuring only one thread can start the forceful exit timer.

Copilot uses AI. Check for mistakes.
@thomhurst thomhurst merged commit 9a70062 into main Dec 22, 2025
18 of 19 checks passed
@thomhurst thomhurst deleted the feature/4117 branch December 22, 2025 12:26
This was referenced Dec 22, 2025
This was referenced Dec 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Timeout specified using --timeout 5m command line argument is ignored

2 participants