Skip to content

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

Merged
iancooper merged 2 commits intoBrighterCommand: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 intoBrighterCommand: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
Copy link
Copy Markdown
Member

@iancooper iancooper left a comment

Choose a reason for hiding this comment

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

Thanks @thomhurst

Copy link
Copy Markdown

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

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.

@iancooper iancooper merged commit 3d9271c into BrighterCommand:master Apr 26, 2026
27 of 30 checks passed
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