Feature Description
Add an internal scheduler service quantum so a worker periodically services
external submissions and ready I/O even while it remains continuously runnable.
Problem Statement
worker_thread::get_next_task() currently pops the owner-local Chase-Lev deque
first and drains the MPSC inbox only after that deque becomes empty. Likewise,
poll_io_when_idle() is reached only when no runnable task exists. A coroutine
that repeatedly yields can therefore keep the local deque non-empty and defer
both a remote submission and an already-ready I/O completion for the full
duration of that runnable backlog.
The existing scheduler_service_benchmark reproduces this deterministically:
with a 5 ms local yield backlog on current main, both service probes complete
only after roughly the whole backlog interval (about 5.07 ms in local smoke
runs). This is scheduler service starvation, not eventfd syscall latency.
Proposed Solution
Reuse the worker's existing count of completed main-loop task dispatches and,
at fixed internal quanta:
- recheck and drain external submissions before choosing another local task;
- if the worker I/O context has pending operations, perform one non-blocking
poll(0ms) to deliver already-ready completions; and
- keep the service schedule independent of whether work came from the local
deque or was stolen.
Paired measurements selected an external-inbox quantum of 256 completed
worker-loop task dispatches and a pending-I/O quantum of 16,384 dispatches.
Keep this as an internal policy in this change; no public scheduler
configuration is required.
API Design
No public API change.
The enforceable contract is a cooperative worker-loop dispatch-count bound:
once user code returns to the scheduler, persistent runnable work cannot
suppress a service opportunity for more than the selected number of further
main-loop dispatches. It is not a wall-clock latency guarantee because one
coroutine dispatch may run arbitrary user code before suspending. A backend
poll may also synchronously deliver a batch of completion resumptions before
control returns to the worker loop; those inline resumes do not consume the
dispatch budget.
Alternatives Considered
Service only when the local deque becomes empty
This is the current behavior and permits unbounded starvation under a
persistent yield/reschedule loop.
Poll I/O on every task resume
This minimizes service delay but adds a backend syscall whenever long-lived I/O
is pending, imposing excessive overhead on CPU-heavy runnable workloads.
Wall-clock-only polling
Reading the clock and enforcing a time interval can cap cooperative wall-clock
delay more directly, but it complicates the hot path and still cannot constrain
non-yielding user code. A small task-count quantum is simpler and deterministic;
time-based adaptation can be considered separately if measurements justify it.
Publicly configurable quantum
This exposes scheduler policy and compatibility obligations before a stable
default has been established. Benchmark and ship an internal default first.
Use Cases
- A single worker serving sockets while CPU-side coroutines remain runnable.
- Cross-thread task submission to a worker running a local yield loop.
- Latency-sensitive timers or poll completions sharing a worker with sustained
cooperative computation.
Implementation Considerations
- Complexity: Medium; the service point interacts with local LIFO order,
inbox publication, backend completion resumption, affinity, and shutdown.
- Breaking Changes: No.
- Dependencies: None.
- Performance Impact: Expected large service-latency reduction with a small,
explicitly measured throughput cost. The CPU-only path should not make I/O
syscalls when the worker has no pending operations.
- Preserve owner-local continuation routing, work stealing, affinity, active I/O
pinning, resize/drain behavior, and exactly-once resumption.
- Cover epoll and io_uring; non-blocking polls must have equivalent scheduling
semantics across both backends.
Compatibility
- C++ Standard: C++20, unchanged.
- Platform: Linux, unchanged.
- Backward Compatibility: Public API and task ownership are unchanged; only
scheduling fairness under sustained local runnable work improves.
Related Work
Acceptance Criteria
- Add deterministic regression coverage proving persistent local runnable work
cannot indefinitely starve a remote inbox task or ready I/O completion.
- Exercise both epoll and io_uring where io_uring is available.
- Report paired
scheduler_service_benchmark latency and CPU-only
reschedule/yield throughput for the selected quantum versus main.
- Run the full unit suite plus scheduler-focused ASAN and TSAN validation.
- Document the cooperative service-bound semantics and benchmark trade-off.
Checklist
Priority
Feature Description
Add an internal scheduler service quantum so a worker periodically services
external submissions and ready I/O even while it remains continuously runnable.
Problem Statement
worker_thread::get_next_task()currently pops the owner-local Chase-Lev dequefirst and drains the MPSC inbox only after that deque becomes empty. Likewise,
poll_io_when_idle()is reached only when no runnable task exists. A coroutinethat repeatedly yields can therefore keep the local deque non-empty and defer
both a remote submission and an already-ready I/O completion for the full
duration of that runnable backlog.
The existing
scheduler_service_benchmarkreproduces this deterministically:with a 5 ms local yield backlog on current
main, both service probes completeonly after roughly the whole backlog interval (about 5.07 ms in local smoke
runs). This is scheduler service starvation, not eventfd syscall latency.
Proposed Solution
Reuse the worker's existing count of completed main-loop task dispatches and,
at fixed internal quanta:
poll(0ms)to deliver already-ready completions; anddeque or was stolen.
Paired measurements selected an external-inbox quantum of 256 completed
worker-loop task dispatches and a pending-I/O quantum of 16,384 dispatches.
Keep this as an internal policy in this change; no public scheduler
configuration is required.
API Design
No public API change.
The enforceable contract is a cooperative worker-loop dispatch-count bound:
once user code returns to the scheduler, persistent runnable work cannot
suppress a service opportunity for more than the selected number of further
main-loop dispatches. It is not a wall-clock latency guarantee because one
coroutine dispatch may run arbitrary user code before suspending. A backend
poll may also synchronously deliver a batch of completion resumptions before
control returns to the worker loop; those inline resumes do not consume the
dispatch budget.
Alternatives Considered
Service only when the local deque becomes empty
This is the current behavior and permits unbounded starvation under a
persistent yield/reschedule loop.
Poll I/O on every task resume
This minimizes service delay but adds a backend syscall whenever long-lived I/O
is pending, imposing excessive overhead on CPU-heavy runnable workloads.
Wall-clock-only polling
Reading the clock and enforcing a time interval can cap cooperative wall-clock
delay more directly, but it complicates the hot path and still cannot constrain
non-yielding user code. A small task-count quantum is simpler and deterministic;
time-based adaptation can be considered separately if measurements justify it.
Publicly configurable quantum
This exposes scheduler policy and compatibility obligations before a stable
default has been established. Benchmark and ship an internal default first.
Use Cases
cooperative computation.
Implementation Considerations
inbox publication, backend completion resumption, affinity, and shutdown.
explicitly measured throughput cost. The CPU-only path should not make I/O
syscalls when the worker has no pending operations.
pinning, resize/drain behavior, and exactly-once resumption.
semantics across both backends.
Compatibility
scheduling fairness under sustained local runnable work improves.
Related Work
not change how often a busy worker services its queues or I/O backend.
Acceptance Criteria
cannot indefinitely starve a remote inbox task or ready I/O completion.
scheduler_service_benchmarklatency and CPU-onlyreschedule/yield throughput for the selected quantum versus
main.Checklist
Priority