fix(coro): propagate targeted spawn affinity through task chains#1007
Conversation
|
Independent read-only review completed against exact head Review scope covered direct-Elio-await affinity propagation, symmetric-transfer/frame ownership, cancellation and exception paths, explicit affinity overrides, I/O-pin precedence, scheduler shutdown, and the sanitizer safety of the regression. No unresolved actionable findings remain. Two issues found during earlier review iterations were corrected before this head: the fix no longer asynchronously queues a borrowed directly-awaited child handle, and the regression now uses bounded-channel waiter publication plus a same-worker marker instead of a raw-handle gate. The final exact-head review found no contract, lifetime, sanitizer, compile, hang, teardown, or portability blockers. GitHub Actions Debug sanitizer jobs are still running. |
Description
Fix
scheduler::go_to()andscheduler::go_joinable_to()so their documented worker affinity remains continuous across the directly awaited Elio task chain that implements the spawned vthread.Previously, targeted spawn set affinity only on the scheduler-owned callable wrapper. The task returned by the callable had a separate execution context and therefore resumed with
NO_AFFINITYafter its first non-I/O suspension. Subsequent I/O could then bind to a different worker'sio_context.Direct Elio task awaits now inherit caller-requested affinity when the child has none. When the child finishes, its final caller-requested affinity is copied back to its Elio awaiter, so an explicit
clear_affinity()orset_affinity()in a deeper frame remains authoritative for the rest of the same logical vthread. I/O ownership remains operation-local and continues to take precedence througheffective_affinity().Type of Change
Related Issues
Closes #1006
Related to #1003
Changes Made
Core Changes
task<T>ortask<void>when the child has no affinity.go_to()andgo_joinable_to()throughtask_scope(), a value-returningchannel::recv()task, a cross-worker wake, and subsequent real socket I/O.clear_affinity(), andset_affinity(1, false)behavior.API Changes (if applicable)
None. This restores the existing documented vthread and targeted-spawn behavior.
Migration Guide (if breaking change)
Not applicable.
Testing
Unit Tests
The regression waits on a bounded channel and uses the channel's waiter-publication test hook. It then runs a marker on worker 0 before waking from worker 1, proving that the original
await_suspend()has returned before cross-worker resumption. One normal scheduler insertion is deliberately rejected so the test distinguishes affinity routing from local fallback. After the nested task chain returns, a cancellable socket receive verifies the owningio_context, completion worker, and post-I/O user affinity.Integration Tests
Sanitizer Testing
Test Results
Checklist
Code Quality
Documentation
Testing
Compatibility
Performance (if applicable)
Screenshots / Diagrams
Not applicable.
Additional Notes
The fix intentionally does not alias execution contexts across coroutine frames. Cancellation continues to use the existing one-way direct-await link, while operation-local I/O pins remain owned by the frame and awaitable that registered the operation. Only caller-requested affinity is carried across the logical vthread chain.
The first regression implementation used a custom raw-handle gate. TSAN correctly identified that the test could resume and destroy the frame while its
await_suspend()was still accessing the gate. The final test uses the channel publication hook plus a same-worker marker to establish the required happens-before ordering.Reviewer Guidance
Areas requiring special attention:
clear_affinity()/set_affinity()behavior without copying I/O ownership.await_suspend()frame.Questions for reviewers: