Skip to content

fix: Mediator Runner/Waker RunAsync awaits inner work (#4078)#4080

Merged
iancooper merged 3 commits intoBrighterCommand:masterfrom
thomhurst:fix/4078-mediator-runner-fire-and-forget
Apr 26, 2026
Merged

fix: Mediator Runner/Waker RunAsync awaits inner work (#4078)#4080
iancooper merged 3 commits intoBrighterCommand:masterfrom
thomhurst:fix/4078-mediator-runner-fire-and-forget

Conversation

@thomhurst
Copy link
Copy Markdown
Contributor

Summary

Fixes #4078.

Runner.RunAsync and Waker.RunAsync used Task.Factory.StartNew(async () => ...) without .Unwrap(). The outer Task<Task> signaled completion at the lambda's first await, so Task.WaitAll returned and "Finished" was logged while ProcessJobs / Wake was still running on the threadpool. Hosts relying on RunAsync returning to know when shutdown completes saw a false signal.

Changes

  • Runner.RunAsync / Waker.RunAsync now return Task and await Task.Run(() => ProcessJobs(ct), ct), so callers actually observe completion. Task.Run pins to TaskScheduler.Default, also guarding against the deadlock pattern reported in ServiceActivator Dispatcher/Performer can deadlock under non-default TaskScheduler #4071.
  • Removed redundant cancellationToken.ThrowIfCancellationRequested() calls after await and the pointless if (ct.IsCancellationRequested) ct.ThrowIfCancellationRequested() guard.
  • Moved the "Finished" log into a finally block so it fires on both clean exit and cancellation.
  • Updated 12 workflow tests: _runner.RunAsync(ct.Token);_ = _runner.RunAsync(ct.Token); await Task.Delay(100ms); to preserve the implicit settle window the bug was accidentally providing.

Test plan

  • dotnet test --filter "FullyQualifiedName~Workflows" -f net9.0 — 12/12 passing
  • dotnet build src/Paramore.Brighter.Mediator — clean

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@thomhurst thomhurst force-pushed the fix/4078-mediator-runner-fire-and-forget branch from 1613019 to 2ecd1ba Compare April 25, 2026 13:12
codescene-delta-analysis[bot]

This comment was marked as outdated.

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 for the fixes. All good stuff, just want to note that Mediator is a WiP, and so may change significantly

codescene-delta-analysis[bot]

This comment was marked as outdated.

…d#4078)

Task.Factory.StartNew(async () => ...) returned a Task<Task> whose outer
task completed at the lambda's first await, so Task.WaitAll signaled
"Finished" while ProcessJobs/Wake was still running. Switch RunAsync to
async Task and await Task.Run(() => ProcessJobs(ct), ct) so callers
actually observe completion. Pinning to TaskScheduler.Default also
guards against the deadlock pattern in BrighterCommand#4071.

Tests updated to fire-and-forget the now-Task-returning method with a
short settle delay before assertions.
Tests now await the runner to completion. For workflows that don't
re-enqueue mid-execution (the majority), close the channel after
scheduling so the runner exits cleanly when the queue drains. For
workflows that do re-enqueue (parallel split, blocking wait), keep
fire-and-forget but capture and observe the task at the end of the
test so OperationCanceledException isn't unobserved.

Also tightens the catch from Exception to OperationCanceledException
since that's the only exception the now-async path can surface.
@thomhurst thomhurst force-pushed the fix/4078-mediator-runner-fire-and-forget branch from e7a2f89 to 074af38 Compare April 26, 2026 14:15
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 3a05286 into BrighterCommand:master Apr 26, 2026
24 of 28 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.

Mediator Runner/Waker fire-and-forget: RunAsync returns before background work finishes

2 participants