Skip to content

Wait for in-flight health check publishers in HealthCheckPublisherHostedService.StopAsync - #68983

Open
SergioAlmeida29 wants to merge 4 commits into
dotnet:mainfrom
SergioAlmeida29:fix/healthcheck-publisher-stopasync-waits-for-inflight
Open

Wait for in-flight health check publishers in HealthCheckPublisherHostedService.StopAsync#68983
SergioAlmeida29 wants to merge 4 commits into
dotnet:mainfrom
SergioAlmeida29:fix/healthcheck-publisher-stopasync-waits-for-inflight

Conversation

@SergioAlmeida29

Copy link
Copy Markdown

Fixes #67304

Problem

HealthCheckPublisherHostedService.StopAsync disposes its timers but doesn't wait for a health-check publish that is already running, so IHealthCheckPublisher.PublishAsync can keep running after the host has stopped.

The timer callback ran RunAsync as fire-and-forget — an async void lambda bound to TimerCallback — so nothing tracked the task. Timer.Dispose() (and even Dispose(WaitHandle) / DisposeAsync()) only wait for the synchronous portion of the callback, which ends at the first await.

Change

  • Track the Task returned by each timer-driven RunAsync in a set.
  • StopAsync awaits the in-flight tasks after disposing the timers, bounded by its CancellationToken (the host links this to ShutdownTimeout). Mirrors BackgroundService.StopAsync: #if NET uses Task.WaitAsync(token) with ConfigureAwaitOptions.SuppressThrowing; older TFMs use Task.WhenAny + token.Register.
  • Publishers that ignore the token are still not force-stopped; overlapping-execution and scheduling behavior is unchanged.

Behavior note

StopAsync may now block up to the host shutdown timeout while a publish drains. This matches the IHostedService contract and BackgroundService, and is not an API or binary breaking change.

Tests

Two regression tests driven by the real timer. StopAsync_WaitsForInFlightPublisher_BeforeReturning fails without the fix (StopAsync completes while the publish is still parked).

cc @danroth27 @BrennanConroy

Copilot AI lite review requested due to automatic review settings September 2, 2026 11:46
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Sep 2, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Thanks for your PR, @SergioAlmeida29. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

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.

🟡 Changes recommended

There is a shutdown race where a timer callback can start a new tracked RunAsync after StopAsync snapshots an empty set and returns, allowing publish work to outlive shutdown.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR updates HealthCheckPublisherHostedService.StopAsync to await timer-triggered health check publishing work that’s already in flight during shutdown, preventing publishers from continuing after the host has stopped.

Changes:

  • Track each timer-driven RunAsync invocation in a shared set and await remaining tasks during StopAsync, bounded by the caller’s cancellation token/shutdown timeout.
  • Replace the timer’s async void callback pattern with a fire-and-forget Task method that records/removes the in-flight operation.
  • Add regression tests validating StopAsync waits for an in-flight publisher, and that a canceled stop token does not wait.
File summaries
File Description
src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs Track timer-triggered RunAsync tasks and await in-flight publishes during StopAsync.
src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs Add regression tests covering shutdown waiting and cancellation-bounded stop behavior.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@Youssef1313 Youssef1313 added the area-healthchecks Includes: Healthchecks (some bugs also in Extensions repo) label Sep 2, 2026
Closes the shutdown race where a timer callback that reaches
RunTimerCallbackAsync after StopAsync snapshots _runningTasks could
still start a RunAsync that outlives StopAsync.
@SergioAlmeida29
SergioAlmeida29 force-pushed the fix/healthcheck-publisher-stopasync-waits-for-inflight branch from 0187e1b to aa14098 Compare September 2, 2026 23:04
private readonly IHealthCheckPublisher[] _publishers;
private List<Timer>? _timers;

private readonly object _runningTasksLock = new object();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
private readonly object _runningTasksLock = new object();
private readonly Lock _runningTasksLock = new();

or does this build for older targets w/o Lock-type?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-healthchecks Includes: Healthchecks (some bugs also in Extensions repo) community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HealthCheckPublisherHostedService.StopAsync uses Timer.Dispose() instead of the overload that waits for a running callback

4 participants