Skip to content

fix(#2809): the two IoPoolTest gates use the sanctioned bridge, and release in a finally - #2822

Merged
rbuergi merged 1 commit into
mainfrom
fix/2809-iopooltest-gates
Aug 30, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/2809-iopooltest-gates

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Two TaskCompletionSource-as-a-gate sites in test/MeshWeaver.Hosting.Test/IoPoolTest.cs — the shape AGENTS.md names in the same breath as ManualResetEventSlim. Found by the Copilot review on #2806 and deliberately left out of it, whose scope was the 79 measured ManualResetEventSlim sites plus the guard's move to zero-tolerance.

Why the ratchet cannot catch these

HandWovenGateRatchetGuard matches the primitives that park a thread by name (SemaphoreSlim | ManualResetEventSlim | ManualResetEvent | AutoResetEvent | CountdownEvent | Monitor.Wait). A TaskCompletionSource used as a gate is structurally the same thing, but the type has a hundred legitimate uses — a name match would cry wolf on all of them. So these needed a human finding, and the fix is worth landing rather than waiting for a guard to grow a rule it cannot express.

The two sites

site note
Invoke_caps_in_flight_at_the_pool_bound 20 pooled leaves parked on one signal, created without RunContinuationsAsynchronously — a single SetResult() resumed every waiting leaf inline on the test thread, the exact inline-resumption defect ObservableToTaskBridgeGuard exists for
Drain_joinsABlockingLeaf_evenWhileAnAsyncLeafIsRunning the async leaf's gate; carried the flag, so the milder of the two

Neither released in a finally. An assertion that threw before the release left the parked leaves waiting on a signal that never arrived — so the host hung, and an ordinary assertion failure with a perfectly good message was reported as a timeout.

The fix

AsyncSubject<T> + ObservableAwait.Await() — the one sanctioned bridge, which sets RunContinuationsAsynchronously itself — with the release in a finally at both sites. Completing twice is a no-op, so the happy path and the finally can both release without a flag.

No cancellation token on the asyncGate Await(), deliberately: that test's premise is that the async leaf is still in flight when the blocking join runs, so a ct-aware wait unwinding on the drain's cancel would weaken exactly what it measures.

Mutation-checked on the property that matters

Made an assertion fail before the release (Be(cap) → Be(cap + 99)):

MeshWeaver.Reactive.Assertions.AssertionException : Expected value to be 102, but found 3.
Failed!  - Failed: 1, Duration: 22 ms

22 ms and the real message — not a hang. Without the finally, that same mutation strands 20 pooled leaves and the assertion is reported as a timeout.

dotnet build test/MeshWeaver.Hosting.Test -c Release -warnaserror        0 Error(s)
dotnet test  test/MeshWeaver.Hosting.Test  (whole project)   269 passed, 0 failed, 9 s

Fixes #2809

🤖 Generated with Claude Code

https://claude.ai/code/session_014FhP7Jg65q8cAATnFfeaTK

…elease in a finally

test/MeshWeaver.Hosting.Test/IoPoolTest.cs held two TaskCompletionSource-as-a-gate
sites — the shape AGENTS.md names in the same breath as ManualResetEventSlim.
Found by the Copilot review on #2806 and deliberately left out of that PR, whose
scope was the 79 measured ManualResetEventSlim sites plus the guard's move to
zero-tolerance.

## Why the ratchet cannot catch them

HandWovenGateRatchetGuard matches the primitives that park a thread BY NAME
(SemaphoreSlim | ManualResetEventSlim | ManualResetEvent | AutoResetEvent |
CountdownEvent | Monitor.Wait). A TaskCompletionSource used as a gate is
structurally the same thing, but the type has a hundred legitimate uses, so a name
match would cry wolf. That is why these two needed a human finding rather than a
ratchet, and why the fix is worth landing rather than waiting for the guard to grow
a rule it cannot express.

## The two sites

  * `Invoke_caps_in_flight_at_the_pool_bound` — 20 pooled leaves parked on one
    signal, created WITHOUT RunContinuationsAsynchronously. A single SetResult()
    resumed every waiting leaf INLINE on the test thread: the exact inline-
    resumption defect ObservableToTaskBridgeGuard exists for.
  * `Drain_joinsABlockingLeaf_evenWhileAnAsyncLeafIsRunning` — the async leaf's
    gate. Carried the flag, so the milder of the two.

Neither released in a `finally`: an assertion that threw before the release left
the parked leaves waiting on a signal that never arrived, so the host HUNG and an
ordinary assertion failure was reported as a timeout.

## The fix

AsyncSubject<T> + ObservableAwait.Await() — the one sanctioned bridge, which sets
RunContinuationsAsynchronously itself — with the release in a `finally` at both
sites. Completing twice is a no-op, so the happy path and the finally can both
release without a flag.

No cancellation token on the asyncGate Await, deliberately: that test's premise is
that the async leaf is STILL in flight when the blocking join runs, so a ct-aware
wait unwinding on the drain's cancel would weaken exactly what it measures.

## Mutation-checked, on the property that matters

Made an assertion fail BEFORE the release (`Be(cap)` → `Be(cap + 99)`):

    MeshWeaver.Reactive.Assertions.AssertionException :
      Expected value to be 102, but found 3.
    Failed: 1, Duration: 22 ms

22 ms and the real message — not a hang. Without the finally that same mutation
strands 20 pooled leaves and the assertion is reported as a timeout.

    dotnet build test/MeshWeaver.Hosting.Test -c Release -warnaserror   0 Error(s)
    dotnet test  test/MeshWeaver.Hosting.Test (whole project)   269 passed, 0 failed

Fixes #2809

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FhP7Jg65q8cAATnFfeaTK
Copilot AI lite review requested due to automatic review settings August 30, 2026 18:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes two remaining “TaskCompletionSource-as-a-gate” patterns in IoPoolTest by switching to the sanctioned AsyncSubject<T> + ObservableAwait.Await() bridge and ensuring the release happens from a finally, so assertion failures don’t strand pooled workers and masquerade as timeouts.

Changes:

  • Replace TaskCompletionSource gate usage with AsyncSubject<T> plus ObservableAwait.Await() in two IoPoolTest cases.
  • Move gate release to finally blocks to prevent hangs when assertions fail before the release path.
  • Add a What’s New entry documenting the user-noticeable impact (tests reporting the real failure instead of hanging).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
test/MeshWeaver.Hosting.Test/IoPoolTest.cs Replaces the two TCS gate sites with AsyncSubject + Await() and guarantees release in finally.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-30-two-test-gates-stop-resuming-inline.md Adds a What’s New entry describing the fix and its effect on test failure reporting.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

787 tests  ±0   783 ✅ ±0   4m 34s ⏱️ +23s
  7 suites ±0     4 💤 ±0 
  7 files   ±0     0 ❌ ±0 

Results for commit c7104d3. ± Comparison against base commit 84bf70b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

943 tests  ±0   943 ✅ ±0   5m 6s ⏱️ -4s
  7 suites ±0     0 💤 ±0 
  7 files   ±0     0 ❌ ±0 

Results for commit c7104d3. ± Comparison against base commit 84bf70b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 085 tests  ±0   1 085 ✅ ±0   5m 32s ⏱️ -4s
    7 suites ±0       0 💤 ±0 
    7 files   ±0       0 ❌ ±0 

Results for commit c7104d3. ± Comparison against base commit 84bf70b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

3 013 tests  ±0   2 821 ✅ ±0   6m 9s ⏱️ +16s
    8 suites ±0     192 💤 ±0 
    8 files   ±0       0 ❌ ±0 

Results for commit c7104d3. ± Comparison against base commit 84bf70b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    8 files  ±0      8 suites  ±0   6m 8s ⏱️ +29s
1 527 tests ±0  1 526 ✅ ±0  1 💤 ±0  0 ❌ ±0 
2 210 runs  +1  2 209 ✅ +1  1 💤 ±0  0 ❌ ±0 

Results for commit c7104d3. ± Comparison against base commit 84bf70b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 171 tests  ±0   1 171 ✅ ±0   6m 45s ⏱️ - 1m 0s
    7 suites ±0       0 💤 ±0 
    7 files   ±0       0 ❌ ±0 

Results for commit c7104d3. ± Comparison against base commit 84bf70b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   44 files  ±0     44 suites  ±0   34m 15s ⏱️ ±0s
8 526 tests ±0  8 329 ✅ ±0  197 💤 ±0  0 ❌ ±0 
9 209 runs  +1  9 012 ✅ +1  197 💤 ±0  0 ❌ ±0 

Results for commit c7104d3. ± Comparison against base commit 84bf70b.

@rbuergi
rbuergi added this pull request to the merge queue Aug 30, 2026
Merged via the queue into main with commit ea44149 Aug 30, 2026
26 checks passed
@rbuergi
rbuergi deleted the fix/2809-iopooltest-gates branch September 13, 2026 10:33
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.

IoPoolTest holds two TaskCompletionSource-as-a-signal gates (one without RunContinuationsAsynchronously)

2 participants