Repository navigation
Description
Found by the Copilot review on #2806 (the sweep that took test/'s hand-woven gates from 79 to 0). The finding is correct and was deliberately left out of that PR: these are not ManualResetEventSlim and so were never in the ratcheted inventory, and the change set was scoped to the 79 measured sites plus the guard's move to zero-tolerance. Filing it so it is not lost in a review thread.
The sites
test/MeshWeaver.Hosting.Test/IoPoolTest.cs holds two TaskCompletionSource-as-a-signal gates — the shape AGENTS.md lists in the same breath as ManualResetEventSlim ("a TaskCompletionSource as a gate"):
| line | site | note |
|---|---|---|
| ~317 | Invoke_caps_in_flight_at_the_pool_bound — var release = new TaskCompletionSource();, awaited as await release.Task inside cap pooled leaves |
🚨 no RunContinuationsAsynchronously, so release.SetResult() resumes every waiting leaf INLINE on the releasing thread — the exact inline-resumption defect ObservableToTaskBridgeGuard exists for |
| ~467 | Drain_joinsABlockingLeaf_evenWhileAnAsyncLeafIsRunning — var asyncGate = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);, returned as the async leaf's Task |
has the flag, so it is the milder of the two |
Neither has a finally: on Invoke_caps_in_flight_at_the_pool_bound, an assertion that throws before release.SetResult() leaves 20 pooled leaves waiting on a signal that never arrives.
The fix
AsyncSubject<T> + ObservableAwait.Await() — the one sanctioned bridge, which sets RunContinuationsAsynchronously itself:
var release = new AsyncSubject<Unit>();
…
await release.Await(); // in the pooled leaf, replacing `await release.Task`
…
try { … release.OnNext(Unit.Default); release.OnCompleted(); … }
finally { release.OnNext(Unit.Default); release.OnCompleted(); } // completing twice is a no-opKeep Await() without a cancellation token at the asyncGate site: the test's premise is that the async leaf is still in flight when the blocking join runs, so a ct-aware wait that unwinds on the drain's cancel would weaken what it measures.
Why it is not caught today
HandWovenGateRatchetGuard's regex matches the primitives that park a thread by name (SemaphoreSlim|ManualResetEventSlim|…|Monitor.Wait); a TaskCompletionSource used as a gate is structurally the same thing but has a hundred legitimate uses, so a name match would cry wolf. ObservableToTaskBridgeGuard already detects hand-rolled bridges structurally (a TCS settled from inside a subscription) — worth considering whether it can be extended to "a TCS awaited by a body the test later completes", or whether these two sites are simply fixed and the shape left to review.
Acceptance
- Both sites converted, releases in a
finally. dotnet build test/MeshWeaver.Hosting.Test/MeshWeaver.Hosting.Test.csproj -c Release -warnaserror→0 Error(s).IoPoolTestgreen by filter (it was 30 tests together with the two LiveQuery classes on test: the test tree holds zero hand-woven concurrency gates — 79 → 0, allow file deleted #2806).
Activity