Skip to content

fix: don't run DedicatedThreadExecutor continuations inline on the dedicated thread - #6898

Merged
thomhurst merged 1 commit into
mainfrom
fix/dedicated-thread-continuation-inlining
Sep 27, 2026
Merged

thomhurst merged 1 commit into
mainfrom
fix/dedicated-thread-continuation-inlining

Conversation

@thomhurst

@thomhurst thomhurst commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Problem

Windows modularpipeline has failed on every run since #6886 (e.g. runs 36239899462, 36280870460, 36313464262). The failing test is TUnit.Engine.Tests.ExpectedStateTests.Pass, in both AOT and reflection modes, because STAThreadTests.Without_STA fails about 13 times per run:

AssertionException: Expected to be equal to MTA
but received STA

Cause

#6886 moved TaskCompletionSource completion out of the message pump and into the dedicated thread's finally block. That block runs after SynchronizationContext has been restored to null. Before #6886, the dedicated context was still current when the TCS completed, so the runtime would not inline awaiting continuations. With no context, the engine's await continuation now runs inline on the STA thread. The engine then keeps executing on that thread and runs later non-STA tests there.

Fix

Create the TCS with TaskCreationOptions.RunContinuationsAsynchronously. The CleanUp-before-completion ordering from #6886 stays as it was.

Tests

  • New DedicatedThreadExecutorTests.Continuation_DoesNotRunOnDedicatedThread. It fails without the fix and passes with it.
  • DedicatedThreadExecutorTests: 9/9 pass.
  • TUnit.TestProject /*/*/STAThreadTests/*: 1515/1515 pass on Windows, net10.0.
  • Issue6361InstanceMethodDataSourceIsolationTests: pass. This test showed as a 4-minute [slow] test in the failing CI runs.

Summary by CodeRabbit

  • Bug Fixes
    • Task continuations now run asynchronously after dedicated-thread execution completes, rather than running inline on the dedicated thread. This helps keep continuation work from blocking that thread during task completion and cleanup. Tests
    • Added coverage verifying that an awaiting continuation runs on a different thread from the execution delegate.

…dicated thread

#6886 moved TaskCompletionSource completion out of the message pump into the
thread's finally block, after the SynchronizationContext is restored to null.
With no context in place, awaiting continuations became eligible to run inline,
so the engine resumed on the STA thread and ran later tests (e.g.
STAThreadTests.Without_STA) there. This broke the Windows engine tests.

Create the TaskCompletionSource with RunContinuationsAsynchronously.
@thomhurst
thomhurst deployed to Pull Requests September 27, 2026 11:31 — with GitHub Actions Active
@thomhurst
thomhurst deployed to Pull Requests September 27, 2026 11:31 — with GitHub Actions Active
@thomhurst
thomhurst deployed to Pull Requests September 27, 2026 11:31 — with GitHub Actions Active
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T11:33:22.155087Z 3802e87 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7535fc87-a26c-40b1-8b30-964151df5f60

📥 Commits

Reviewing files that changed from the base of the PR and between 0d44237 and 3802e87.

📒 Files selected for processing (2)
  • src/TUnit.Core/Executors/DedicatedThreadExecutor.cs
  • tests/TUnit.UnitTests/DedicatedThreadExecutorTests.cs

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

ExecuteAsync now schedules completion continuations asynchronously. A unit test checks that the awaiting continuation runs on a different thread from the test delegate.

Changes

Dedicated thread continuation behavior

Layer / File(s) Summary
Asynchronous completion and thread assertion
src/TUnit.Core/Executors/DedicatedThreadExecutor.cs, tests/TUnit.UnitTests/DedicatedThreadExecutorTests.cs
ExecuteAsync configures its completion source to run continuations asynchronously. The new test checks that the awaiting continuation runs on a different thread from the test delegate.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: glennawatson

Merge Risk: ⚪ Minimal · up to 3802e

This change prevents await continuations from running inline on the dedicated thread after cleanup, and its regression test exercises that path. No merge-blocking risk is evident.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 3802e

The change keeps test execution and cleanup on the dedicated thread while preventing awaiting code from continuing inline on that thread. No new security exposure was identified, but broader security coverage is incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The identified effect is confined to completion continuations of dedicated-thread executor invocations and their awaiting test-engine code; no changed cross-system dependency is evidenced.

Trust Boundaries and Controls

  • inferred — The inspected path continues to execute the caller-supplied action through the existing dedicated-thread mechanism. Asynchronous continuation dispatch does not itself create an additional authorization or trust transition.

Resilience and Maintainability Implications

  • inferred — Completing only after cleanup and dispatching continuations asynchronously reduces the risk that subsequent test-engine work resumes inline on the dedicated thread. Safety of arbitrary derived hooks under concurrent reuse is not established by this change.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing DedicatedThreadExecutor continuations from running inline on the dedicated thread.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the thread-bound queue,
Then hops where continuations run anew.
The task completes; the thread moves on,
No inline hop before the dawn.
The test confirms the paths divide,
And ears perk up with taskful pride.

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Verdict: LGTM — small, well-targeted fix with a regression test that actually reproduces the bug.

Correctness

The root-cause analysis in the description checks out against the code. In DedicatedThreadExecutor.ExecuteAsync, CompleteTest(tcs, ...) is invoked from the dedicated thread's finally block (DedicatedThreadExecutor.cs:50), after ExecuteAsyncActionWithMessagePump's own finally has already restored SynchronizationContext.Current to whatever it was before (DedicatedThreadExecutor.cs:164). With a plain TaskCompletionSource<object?>(), SetResult/SetException runs continuations synchronously on the calling thread when there's no context to post through — so the engine's await continuation resumed inline on the dedicated (STA) thread and kept running subsequent tests there. Constructing the TCS with TaskCreationOptions.RunContinuationsAsynchronously forces the continuation onto the thread pool instead, which is exactly the fix needed, and it doesn't disturb the CleanUp-before-completion ordering from #6886.

This is also consistent with the existing pattern already in the same file: DedicatedThreadSynchronizationContext.Send (line 346) already creates its internal TCS with RunContinuationsAsynchronously for the same reason. Good that the fix follows the convention already established a few lines away.

Test

Continuation_DoesNotRunOnDedicatedThread is a solid regression test — it captures the dedicated thread's identity from inside the action and asserts the continuation after await isn't running on that same Thread instance. Per the PR description it fails without the fix and passes with it, which is the right bar for this kind of concurrency bug.

Minor, non-blocking observation

Not something to fix in this PR, but worth a mental note for later: this bug class (a TaskCompletionSource completed from a non-pool thread, with continuations able to run inline unless RunContinuationsAsynchronously is set) exists in a few other spots in the engine — e.g. AsyncConvert.cs:223, RunHelpers.cs:19, and ConstraintKeyScheduler.cs:62 still use the default TCS options, while others (TimeoutHelper.cs, ObjectLifecycleService.cs, NotInParallelLock.cs, TestRunner.cs) already opt in. If any of those completion sites are ever called from a dedicated/background thread rather than always from the pool, the same class of bug could reappear. A small shared helper (e.g. a TaskCompletionSourceHelpers.CreateAsync<T>() that defaults to RunContinuationsAsynchronously) could make 'don't run continuations inline' the default for engine hot-path TCS usage instead of something each call site has to remember — but that's a broader refactor, not something this PR needs to take on.

@greptile-apps

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Changes how test continuations schedule on dedicated threads.

The PR appears safe to merge.

Summary

The PR makes executor task continuations run asynchronously so the engine does not continue on a dedicated or STA thread after a test finishes.

  • Preserves cleanup-before-completion ordering.
  • Adds a regression test for continuation thread affinity.

Reviews (1) · Last reviewed commit: "fix: don't run DedicatedThreadExecutor c..."

@thomhurst
thomhurst enabled auto-merge (squash) September 27, 2026 11:44
@thomhurst
thomhurst merged commit 5908e02 into main Sep 27, 2026
21 checks passed
@thomhurst
thomhurst deleted the fix/dedicated-thread-continuation-inlining branch September 27, 2026 11:59

This branch was successfully deployed

1 active deployment
Pull Requests — 3802e876 Deployed Sep 27, 2026 by thomhurst via modularpipeline (macos-latest) #19524
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