Skip to content

The cancellation test runs its tasks on their own threads, so the timer that cancels them is not queued behind them on a starved pool - #1343

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
cancel-on-dedicated-threads
Sep 14, 2026
Merged

Rafael-SOWNet merged 1 commit into
masterfrom
cancel-on-dedicated-threads

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

MultithreadingCancel.TestThoseOnlyCancel failed on CI three times in four days (runs 34595379048, 34789206587 on master, and 34899277352 on #1342), each time on a four-task row, each time with one task RanToCompletion where it should have been cancelled.

Cause. The tasks are processor-bound and were started with Task.Run. On a runner with two cores, four of them hold every pool thread, and the timer behind CancelAfter is a pool work item queued behind them — it does not run until one of the tasks has finished, and that task has by then run to completion uncancelled. Reproduced with the same tasks in a console under DOTNET_PROCESSOR_COUNT=2: fifteen of twenty-four ran to completion; none once on their own threads. (The other hypothesis, a catch in the library swallowing the OperationCanceledException, was instrumented at every bare catch and never fired across 220 cancellations at 20 ms steps.)

Fix. The tasks and their children are started with TaskCreationOptions.LongRunning, which gives each a thread of its own and leaves the pool to the timer and the test's own continuations. The workload is thirty solves rather than ten: warm, ten were 1.1 s on a desktop against a cancellation at 0.75 s, and the test's gate only measures the cold run.

Twenty-six rows pass with the machine's cores, with DOTNET_PROCESSOR_COUNT=2 and with DOTNET_PROCESSOR_COUNT=1. Test-only change.

🤖 Generated with Claude Code

https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura

…er that cancels them is not queued behind them on a starved pool

MultithreadingCancel.TestThoseOnlyCancel failed on CI three times in
four days (runs 34595379048, 34789206587 and 34899277352, the last on
an unrelated PR), each time on a four-task row, each time with one
task "RanToCompletion" where it should have been cancelled. The tasks
are processor-bound and were started with Task.Run: on a runner with
two cores, four of them hold every pool thread, and the timer behind
CancelAfter is a pool work item queued behind them, so it does not run
until one of the tasks has finished -- and that task has run to
completion uncancelled. Reproduced with the same tasks in a console
under DOTNET_PROCESSOR_COUNT=2: fifteen of twenty-four ran to
completion; none once on their own threads.

The tasks and their children are started with
TaskCreationOptions.LongRunning, which gives each a thread of its own
and leaves the pool to the timer and the test's own continuations.
The workload is thirty solves rather than ten: warm, ten were a
second and a tenth on a desktop against a cancellation at three
quarters of a second, and the test's gate measures the cold run.

Twenty-six rows pass with the machine's cores, with
DOTNET_PROCESSOR_COUNT=2 and with DOTNET_PROCESSOR_COUNT=1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
@Rafael-SOWNet
Rafael-SOWNet merged commit 08ee799 into master Sep 14, 2026
27 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the cancel-on-dedicated-threads branch September 14, 2026 22:40
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.

1 participant