perf(worker): unpark awaited control-frame writers from the winning drain pass
WriteAsync enqueued its frame and then contended unconditionally for the write lock, so a caller that lost the race stayed in WaitAsync until the winning drainer released — even though that winner writes, flushes, and completes the loser's control frame at the control-to-event class boundary, part-way through its pass. The boundary flush made the delivery point honest; the awaited task was still charged for the whole event backlog it had just been flushed ahead of. WriteAsync now awaits its own frame's completion racing the lock acquisition. Completion first: the caller returns at its frame's delivery point and the outstanding acquisition is detached, not dropped — a continuation drains whatever is queued and releases, so the lock is never acquired and silently held and a frame enqueued between the previous drainer's last dequeue and its release is still written. Lock first: drain as before. Cancellation keeps the WRK-22 tombstone semantics exactly, and a wait cancelled after the caller has already detached releases nothing (SemaphoreSlim hands no count to a wait it cancels), so no count leaks and no queued frame is stranded. A token that fires after the frame's completion won the race changes nothing — the frame was delivered. WriteBatchAsync deliberately keeps the plain wait-then-drain shape: its last completion resolves at the end-of-pass flush anyway. Three tests: the latency win (a control caller returning while the winning WriteBatchAsync event burst is demonstrably still blocked mid-pass), a mixed-priority concurrency soak pinning exactly-once writes and a single drainer, and the cancel-after-detach corner (a wrongly released count would surface as the drainer's own Release throwing SemaphoreFullException). edited on macOS, windev verification pending (plan Task 11). Verified here by compiling and running WorkerFrameWriter plus the writer suite against net10.0 in a scratch harness: 31/31 pass, and the two behaviour-pinning tests fail against the pre-change parked implementation.
This commit is contained in:
@@ -349,6 +349,9 @@ boundary, so the priority class governs the frame's delivery point rather than o
|
||||
its byte order. Getting the awaited-latency win too requires unparking the lock-race
|
||||
loser from the winner's pass — a change to the write-lock shape, recorded as a
|
||||
follow-up. One extra `FlushFileBuffers` per mixed pass is the accepted cost.
|
||||
That lock-parking was closed by `docs/plans/2026-08-17-deferred-closeout.md`
|
||||
Task 2, 2026-08-17: `WriteAsync` races its own frame's completion against the
|
||||
lock acquisition and detaches the wait it abandons.
|
||||
|
||||
**Task 11 — teardown ordering and unconditional fault observation.** Teardown disposes
|
||||
the session-owned transport first, then observes the read that dispose abandoned.
|
||||
|
||||
Reference in New Issue
Block a user