Skip to content

Ensure BackgroundService invokes ExecuteAsync after start - #132241

Open
steveisok wants to merge 1 commit into
mainfrom
steveisok-investigate-backgroundservice-race
Open

Ensure BackgroundService invokes ExecuteAsync after start#132241
steveisok wants to merge 1 commit into
mainfrom
steveisok-investigate-backgroundservice-race

Conversation

@steveisok

Copy link
Copy Markdown
Member

Fixes #131249.

BackgroundService.StartAsync passed its stopping token to Task.Run as the scheduling token. If StopAsync or Dispose canceled that token before the queued delegate began, the task transitioned to Canceled without invoking ExecuteAsync.

This change:

  • Preserves the .NET 10 behavior where all ExecuteAsync work runs asynchronously on a thread-pool thread.
  • Uses CancellationToken.None for scheduling so cancellation cannot suppress delegate invocation after a non-canceled start is accepted.
  • Preserves pre-canceled StartAsync behavior by explicitly assigning Task.FromCanceled(cancellationToken) to ExecuteTask.

Regression tests deterministically occupy the sole thread-pool worker and verify that immediate stop and dispose still invoke ExecuteAsync exactly once on a thread-pool thread with an already-canceled stopping token. Pre-canceled startup coverage also verifies that ExecuteAsync is not invoked.

Note

This pull request description was generated with GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3dfd3a15-eb10-455b-8c1c-16ecd87fd841
Copilot AI lite review requested due to automatic review settings August 12, 2026 22:59
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@steveisok

Copy link
Copy Markdown
Member Author

@jeffhandley deferring to you on who best to review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 adjusts BackgroundService.StartAsync scheduling so that ExecuteAsync is still invoked even if the service is stopped/disposed immediately after start, and adds regression coverage to validate the behavior under deterministic thread-pool starvation.

Changes:

  • Update BackgroundService.StartAsync to avoid using the stopping token as the Task.Run scheduling token.
  • Add a regression test that blocks the sole thread-pool worker and verifies ExecuteAsync still runs exactly once when immediately stopped/disposed.
  • Update the pre-canceled StartAsync test to assert ExecuteTask is canceled and ExecuteAsync is not invoked.
Show a summary per file
File Description
src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs Changes how ExecuteAsync is scheduled/canceled to prevent cancellation from suppressing delegate invocation.
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceTests.cs Adds deterministic regression coverage for immediate stop/dispose and refines pre-canceled start assertions.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines 45 to +48
// Execute all of ExecuteAsync asynchronously, and store the task we're executing so that we can wait for it later.
_executeTask = Task.Run(() => ExecuteAsync(_stoppingCts.Token), _stoppingCts.Token);
_executeTask = cancellationToken.IsCancellationRequested
? Task.FromCanceled(cancellationToken)
: Task.Run(() => ExecuteAsync(_stoppingCts.Token), CancellationToken.None);
Comment on lines 152 to +154

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(false)]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants