Skip to content

fix: pin Task.Factory.StartNew to TaskScheduler.Default (#4071)#4079

Merged
iancooper merged 2 commits into
BrighterCommand:masterfrom
thomhurst:fix/4071-task-scheduler-deadlock
Apr 26, 2026
Merged

fix: pin Task.Factory.StartNew to TaskScheduler.Default (#4071)#4079
iancooper merged 2 commits into
BrighterCommand:masterfrom
thomhurst:fix/4071-task-scheduler-deadlock

Conversation

@thomhurst

Copy link
Copy Markdown
Contributor

Closes #4071.

Summary

  • Dispatcher.Start, Performer.Run, TimeoutPolicyHandler, Mediator.Runner, and Mediator.Waker called Task.Factory.StartNew without an explicit TaskScheduler, inheriting TaskScheduler.Current. Under any limited-concurrency scheduler (BrighterAsyncContext, ASP.NET, async test hosts), the control task / message pump queued behind the very thread waiting for them and End() / await never returned. TaskCreationOptions.LongRunning is only a hint — only TaskScheduler.Default guarantees a fresh thread.
  • Pass TaskScheduler.Default explicitly at every affected call site. Performer.Run also dropped its redundant async/await wrapper.
  • Added regression test DispatcherOnLimitedConcurrencySchedulerTests using ConcurrentExclusiveSchedulerPair — verified red without the fix (30 s deadlock) and green with it (~600 ms).

Test plan

  • New regression test fails with TaskCreationOptions.LongRunning-only call (deadlock at 30 s) and passes with TaskScheduler.Default pin.
  • All 81 MessageDispatch tests pass on net9.0.
  • All 40 Policy / Mediator / Workflow tests pass on net9.0 (2 pre-existing skips on obsolete Timeout API).
  • Paramore.Brighter.ServiceActivator and Paramore.Brighter.Mediator build clean.

codescene-delta-analysis[bot]

This comment was marked as outdated.

…and#4071)

Dispatcher.Start, Performer.Run, TimeoutPolicyHandler, and the Mediator
Runner/Waker called Task.Factory.StartNew without an explicit scheduler,
inheriting TaskScheduler.Current. Under any limited-concurrency scheduler
(BrighterAsyncContext, ASP.NET, async test hosts), the spawned tasks
queued behind the very thread waiting for them and End() / await never
returned. LongRunning is only a hint — only TaskScheduler.Default
guarantees a fresh thread.

Pass TaskScheduler.Default explicitly at every affected call site and
add a regression test using ConcurrentExclusiveSchedulerPair that
deadlocks without the fix.
@thomhurst thomhurst force-pushed the fix/4071-task-scheduler-deadlock branch from 0316ae3 to 203740e Compare April 25, 2026 13:12
codescene-delta-analysis[bot]

This comment was marked as outdated.

@iancooper iancooper added 3 - Done .NET Pull requests that update .net code Performance Improvement V10.X labels Apr 25, 2026

@iancooper iancooper left a comment

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.

Thanks @thomhurst

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gates Passed
4 Quality Gates Passed

See analysis details in CodeScene

Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

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

Labels

3 - Done .NET Pull requests that update .net code Performance Improvement V10.X

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ServiceActivator Dispatcher/Performer can deadlock under non-default TaskScheduler

2 participants