Skip to content

Make duplicate-stop cancellation regression independent of thread-pool scheduling - #7546

Merged
thomhurst merged 1 commit into
mainfrom
issue-7506-stop-cancellation
Nov 4, 2026
Merged

thomhurst merged 1 commit into
mainfrom
issue-7506-stop-cancellation

Conversation

@thomhurst

@thomhurst thomhurst commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

The duplicate-stop regression used two thread-pool tasks while its first caller synchronously blocked a worker. Run both callers on dedicated threads so thread-pool scheduling latency cannot consume the cancellation deadline. Keep the startup handshake and verify cancellation before releasing the inner stop, with a five-second failure guard matching the startup guard.

Always release the inner stop and drain both callers before disposing their synchronization gates, including when startup or the assertion fails.

Closes #7506

Validation

  • .NET SDK 10.0.403, macOS arm64: dotnet test --project tests/TUnit.AspNetCore.Tests --framework net10.0 --treenode-filter "/*/*/HostedServiceStopOnceTests/*" — all 8 tests passed.
  • Temporarily invoked the inner stop under the wrapper lock: the targeted regression failed with the expected timeout and exited cleanly. Restored production code and reran all 8 tests successfully.
  • Windows CI load was not reproduced locally; Windows validation remains for CI. No production code changes.

Summary by CodeRabbit

  • Tests
    • Improved coverage for concurrent stop requests during synchronous startup, including cancellation handling and ensuring the stop operation runs only once.

@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5ece1c4f-5915-410f-8dd7-a49f2e04fe99
Commits

Reviewing files that changed from the base of the PR and between 0d3ecf3 and f5ea962.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

No actionable comments were generated in the recent review.

Walkthrough

The hosted-service stop test now runs both callers on dedicated long-running tasks. It waits up to five seconds for the duplicate caller’s cancellation, then releases the blocked stop and drains both callers before disposing synchronization gates.

Changes: Hosted-service stop cancellation test

Layer / File(s) Summary
Caller scheduling and cleanup
tests/TUnit.AspNetCore.Tests/HostedServiceStopOnceTests.cs
Both stop callers use dedicated long-running tasks. The test waits up to five seconds for duplicate-call cancellation, releases the blocked stop, and drains both callers. It continues to verify that the inner stop runs once.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Other · Severity of issue fixed: Medium

Merge Risk: Minimal · previous review 0d3ec

The change is mergeable after normal CI validation, including Windows.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: making the duplicate-stop cancellation regression independent of thread-pool scheduling.
Linked Issues check Passed Issue #7506 requires a cancellation regression for Duplicate_Stop_Caller_Does_Not_Wait_For_Synchronous_Startup that does not depend on thread-pool scheduling latency, without weakening the cancellat…
Out of Scope Changes check Passed The only changed file is tests/TUnit.AspNetCore.Tests/HostedServiceStopOnceTests.cs, and the edits are confined to the body of Duplicate_Stop_Caller_Does_Not_Wait_For_Synchronous_Startup. The chan…
Full details: Linked Issues check

Explanation

Issue #7506 requires a cancellation regression for Duplicate_Stop_Caller_Does_Not_Wait_For_Synchronous_Startup that does not depend on thread-pool scheduling latency, without weakening the cancellation guarantee. The diff in HostedServiceStopOnceTests.cs (merge base to head) covers this. (1) Both stop callers now run through Task.Factory.StartNew(..., TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(), which uses dedicated threads instead of Task.Run. (2) The startup handshake is kept: inner.WaitUntilEntered() still runs before the second caller starts. (3) The duplicate caller is checked with an already-cancelled token, and the wait guard is raised from 1 to 5 seconds. The assertion still requires OperationCanceledException. (4) inner.StopCalls is still asserted equal to 1. The PR changes only the test. The production wrapper was not inspected directly. The author reports that a temporary mutation, which invoked the inner stop under the wrapper lock, made the regression fail with the expected timeout, so the test detects the regused behavior. The residual wall-clock guard is a timeout, so the test is not fully free of timing. This guard is the same kind of guard the issue accepts, and the change removes the thread-pool dependency the issue identifies. Windows CI remains the final check under load.

Full details: Out of Scope Changes check

Explanation

The only changed file is tests/TUnit.AspNetCore.Tests/HostedServiceStopOnceTests.cs, and the edits are confined to the body of Duplicate_Stop_Caller_Does_Not_Wait_For_Synchronous_Startup. The changes are: dedicated-thread task creation for both callers, a 5-second guard, and a finally block that releases the inner stop, drains both callers, and then allows disposal. Each change serves issue #7506 directly. No production code, unrelated tests, or project files changed. The PR description states that no production code changes were made, and the diff agrees with that statement.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 1 functions across 1 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

A rabbit checks the stop-call queue,
One waits while the other’s token rings through.
On long-running threads, both callers depart,
The blocked stop is released with care from the start.
The inner stop runs once, then the gates rest,
And the rabbit hops off, pleased with the test.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3b4912e8-24c6-486a-9186-92980ce41234
Commits

Reviewing files that changed from the base of the PR and between 10a0af7 and 0d3ecf3.

Files selected for processing: 1
  • tests/TUnit.AspNetCore.Tests/HostedServiceStopOnceTests.cs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 0 remain after this review.

@thomhurst

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T04:28:04.523487Z f5ea962 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Review

Test-only change, and it looks good.

  • Both callers now run on LongRunning dedicated threads, so thread-pool starvation can't eat the cancellation deadline. This addresses the flakiness in Hosted-service duplicate-stop cancellation test times out on Windows CI #7506.
  • WaitUntilEntered() has its own 5s timeout and now sits inside the try. A startup failure still reaches finally, which releases the inner stop and drains first. The gates are not disposed while a caller is still running.
  • second is null-checked and its cancellation is swallowed only when cancellation was actually requested. That keeps the drain from hiding real errors.
  • The 5s assertion timeout matches the startup guard. It is only a failure ceiling, because the cancelled caller should return immediately.

Minor, non-blocking: if first faults in the finally, that exception can mask the original assertion failure. That is acceptable for a test.

No blocking issues. Windows CI remains the real validation, as the description notes.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 0d3ecf3844

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Low impact] The PR appears safe to merge; no actionable issues were found.

Summary

Makes the duplicate-stop regression less sensitive to thread-pool delays.

  • The duplicate-stop test no longer depends on thread-pool timing.

Reviews (2) · Last reviewed commit: "Fix hosted-service cancellation test sch..." · Reviewed by Greptile

@thomhurst
thomhurst marked this pull request as ready for review November 4, 2026 02:51
@thomhurst

Copy link
Copy Markdown
Owner Author

Marked ready after Claude and Codex both returned no-blocker verdicts for 0d3ecf3, with no review threads. Claude's minor exception-masking observation is accepted as non-blocking for this test; no code change is needed.

Promotion started Claude review run 41763438515 and the ready-for-review CI workflows. Auto-merge remains unarmed until the promotion review passes and its comment is checked, as required by the merge gate.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Review

Test-only change, and it looks good. The fix targets the cause of the flake. The old test ran both callers on thread-pool tasks while the first caller blocked a worker synchronously. Under CI load the 1s deadline could then be consumed by scheduling latency. Using LongRunning on TaskScheduler.Default gives each caller its own thread. The 5s guard matches the startup guard.

  • Moving WaitUntilEntered inside the try is correct. finally now always releases the inner stop and drains both callers before the gates are disposed, even if the handshake or assertion fails.
  • Observation: if the assertion fails and first also faults, await first in finally can replace the original assertion failure with the caller's exception. This is unlikely and fine for a test. Wrapping the drain in a try/catch that preserves the primary failure would make diagnosis a little easier.
  • A small helper such as RunOnDedicatedThread(Func<Task>) would remove the duplicated Task.Factory.StartNew(...).Unwrap() boilerplate. This is optional.

I did not run the tests. Windows CI is the real check for the flake. No production code changes, so there are no AOT or generator concerns. Approving.

@thomhurst

thomhurst commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner Author

The prerequisite is cleared: #7549 merged as 4891fce and its .NET run 41766395044 succeeded on all three operating systems. Rebased this PR onto that main commit and pushed f5ea962, preserving the original test-only diff.

Validation after rebase: SDK 10.0.403, macOS arm64, targeted HostedServiceStopOnceTests on net10.0: 8 passed, 0 failed. git diff --check passed. Existing build warnings remain tracked by #7410 and #7438. Fresh CI and reviews must validate the new head; the old snapshot failures are no longer a blocker. No CI rerun was used and auto-merge remains unarmed pending current-head review.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Review: LGTM. Test-only change that makes the duplicate-stop regression robust against thread-pool starvation.

  • Both callers now run on dedicated LongRunning threads, so the 5s cancellation deadline no longer depends on pool scheduling.
  • WaitUntilEntered has a 5s guard and now sits inside the try, so a startup failure still reaches finally.
  • finally always calls inner.Release() and drains both callers before the gates are disposed. Swallowing OperationCanceledException for second is correct, because that exception is the expected result.
  • await first in finally could in theory mask an assertion failure if first throws. That seems unlikely here and is acceptable.

Minor, non-blocking: wait on the second result with a single helper shared by the assertion and the drain. That would remove the duplicated try/catch. It isn't needed for this PR.

I did not run the tests. The PR reports 8/8 passing locally and says Windows validation is left to CI.

@thomhurst

Copy link
Copy Markdown
Owner Author

Final disposition for f5ea962: #7549 is merged and run 41766395044 succeeded. All 29 current checks pass, including Windows, macOS, Linux, and AOT. Claude and Codex both returned positive verdicts for this head; Greptile found no actionable issues. There are no inline comments or review threads.

Claude's optional helper suggestions do not require changes: the two explicit task launches and the separate assertion/drain paths keep this regression self-contained. The possible cleanup exception masking is accepted as non-blocking for this test, as the reviewer states. CodeRabbit's docstring-coverage warning is also non-blocking: this change is confined to an existing, descriptively named test with comments explaining its synchronization and cleanup. It introduces no undocumented public API.

The fresh Assert-PrGreen gate passed. Proceeding with the pinned squash merge.

This was referenced Nov 4, 2026

This branch was previously deployed

1 inactive deployment
Pull Requests — f5ea962f Deployed Nov 4, 2026 by thomhurst via modularpipeline (macos-latest) #21113
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.

Hosted-service duplicate-stop cancellation test times out on Windows CI

1 participant