Skip to content

fix: replace sync-over-async Task.Delay with Thread.Sleep in sync pumps (#4073)#4074

Merged
iancooper merged 4 commits intoBrighterCommand:masterfrom
thomhurst:fix/4073-sync-sleep
Apr 26, 2026
Merged

fix: replace sync-over-async Task.Delay with Thread.Sleep in sync pumps (#4073)#4074
iancooper merged 4 commits intoBrighterCommand:masterfrom
thomhurst:fix/4073-sync-sleep

Conversation

@thomhurst
Copy link
Copy Markdown
Contributor

Summary

Closes #4073.

Replaces Task.Delay(x).GetAwaiter().GetResult() / .Wait() with Thread.Sleep(x) at sites running on dedicated sync threads (pump loops, Dispatcher control thread, connection-pool retry jitter). Each of these sites explicitly blocks a sync-only thread — there is nothing to yield to — so the async primitive just allocates a Task, registers a TimerQueue timer, and parks on a ManualResetEventSlim to do what Thread.Sleep does with a single OS primitive.

Benefits:

Sites changed

Six from the issue:

  • src/Paramore.Brighter.ServiceActivator/Reactor.cs:123 — broken-circuit retry
  • src/Paramore.Brighter.ServiceActivator/Reactor.cs:131 — channel-failure retry
  • src/Paramore.Brighter.ServiceActivator/Reactor.cs:155 — empty-channel pause
  • src/Paramore.Brighter.ServiceActivator/Dispatcher.cs:342Start() spin-wait for DS_RUNNING
  • src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageGatewayConnectionPool.cs:151 — connect-retry jitter
  • src/Paramore.Brighter.MessagingGateway.RMQ.Sync/PullConsumer.cs:78 — pull-consumer pause

One additional found during audit of src/ for the same anti-pattern:

  • src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs:105TryReceive(topic, timeout) sync polling loop

Also dropped using System.Threading.Tasks; from the three files where it became unused after the substitution.

Proactor.cs and all genuine await Task.Delay(...) paths are unchanged — those correctly yield on the async pump. Test-code uses of Task.Delay(...).Wait() are out of scope (they run on the test-runner thread, not dedicated pump threads).

Test plan

  • Paramore.Brighter.ServiceActivator builds clean on all TFMs (netstandard2.0, net8.0, net9.0, net10.0) — verified locally.
  • Paramore.Brighter.MessagingGateway.RMQ.Sync builds clean on all TFMs — verified locally.
  • Paramore.Brighter.MessagingGateway.MsSql builds clean on all TFMs (including net462) — verified locally.
  • Existing ServiceActivator / RMQ.Sync / MsSql test suites pass on CI.
  • Monitor whether When_A_Message_Dispatcher_Shuts_A_Connection remains stable in CI after removing the TimerQueue from the Reactor pause path.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@iancooper
Copy link
Copy Markdown
Member

Thanks @thomhurst. You are right, mostly these are deliberate and it may be better to express that over panic folks seeing a GetAwaiter().GetResult().

codescene-delta-analysis[bot]

This comment was marked as outdated.

…in sync pumps

These sites run on dedicated LongRunning threads or the Dispatcher
control thread and explicitly block a sync thread — there is nothing
to yield to. Task.Delay().GetAwaiter().GetResult() / .Wait() allocates
a Task, registers a TimerQueue timer, and parks on a
ManualResetEventSlim to do what Thread.Sleep does with a single OS
primitive.

Switching to Thread.Sleep removes unnecessary allocations and
TimerQueue registrations from hot pump loops, and eliminates the
TimerQueue as a variable when diagnosing pump-shutdown hangs.

Sites changed:
- Reactor.cs: broken-circuit retry, channel-failure retry, empty-channel pause
- Dispatcher.cs: Start spin-wait for DS_RUNNING
- RmqMessageGatewayConnectionPool.cs: connect-retry jitter
- RMQ.Sync/PullConsumer.cs: pull-consumer pause

Proactor.cs and other async paths (await Task.Delay) are left unchanged.

Closes BrighterCommand#4073
MsSqlMessageQueue<T>.TryReceive(topic, timeout) is a sync polling loop
calling the sync TryReceive(topic) overload. The Task.Delay().GetAwaiter().GetResult()
between polls is the same anti-pattern as BrighterCommand#4073 — no async path to yield to,
just unnecessary Task allocation and TimerQueue registration.

Not listed in BrighterCommand#4073 but found during audit of src/ for similar patterns.
Related to BrighterCommand#4073.
@thomhurst thomhurst force-pushed the fix/4073-sync-sleep branch from 1431f68 to 26487ee Compare April 25, 2026 13:13
codescene-delta-analysis[bot]

This comment was marked as outdated.

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 e86491f into BrighterCommand:master Apr 26, 2026
6 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace sync-over-async Task.Delay().GetAwaiter().GetResult() with Thread.Sleep in sync pumps

3 participants