#75 made reaching the handle ceiling backpressure rather than refusal: an operation whose completion creates a handle reserves its slot when it is submitted, and new_handle will not spend a slot another operation is holding. An accept the kernel has been asked to perform therefore always has somewhere to put its connection, and at the ceiling the submission is refused instead — the connection stays in the listener's backlog rather than being accepted and destroyed.
That guarantee is exact for a single-shot accept. It is one-connection-deep for a multishot one.
Why
A multishot accept stays armed and delivers many connections from one operation. The backend fills the turn's event vector up to events.capacity(), which is Config::events_per_turn, so one turn can deliver up to events_per_turn connections from a single armed operation. The operation holds one reservation and re-reserves after each delivery (Driver::attach_reserved), so it is protected for the next connection only. Once the loop is at its ceiling mid-batch, the remaining connections in that batch have no slot and complete with ResourceLimit — accepted by the kernel, then destroyed, which is the behaviour #75 set out to remove.
Single-shot accepts, which is what a host re-arming per connection does, are unaffected.
What it needs
The driver has to be able to say how many completions it can house this turn, which means a budget the backend honours:
fn poll(&mut self, timeout: Option<Duration>, budget: usize,
events: &mut Vec<Event<Self::Detached>>) -> Result<PollInfo>;
Each backend then caps its fill at budget.min(events.capacity() - events.len()) — Unix::run_ready already computes exactly that quantity from the vector's capacity, so the change is small per backend but touches all six. The driver would pass min(events_per_turn, handles.remaining() - reserved_handles) when a multishot accept is armed.
Two things to be careful about, which is why this was not folded into #75:
- Fairness. Shrinking the native budget starves reads, writes and timers on other handles in the same turn, not just accepts. DESIGN §10 rule 3 and its tl-i01b rationale record what happened last time the native step was made conditional: the fairness test failed after two seconds. A budget that throttles a listener must not throttle the rest of the loop, so it probably has to be per-source rather than per-turn.
- The no-spin rule (4a). A turn that declines to collect available work must still not degrade into a zero-timeout poll loop.
Acceptance
- A contract test that arms a multishot accept on a loop whose remaining capacity is below
events_per_turn, floods the listener, and asserts no accept completes with ResourceLimit — connections wait in the backlog instead.
- The existing fairness and no-spin contract tests stay green on all six backends.
#75 made reaching the handle ceiling backpressure rather than refusal: an operation whose completion creates a handle reserves its slot when it is submitted, and
new_handlewill not spend a slot another operation is holding. An accept the kernel has been asked to perform therefore always has somewhere to put its connection, and at the ceiling the submission is refused instead — the connection stays in the listener's backlog rather than being accepted and destroyed.That guarantee is exact for a single-shot accept. It is one-connection-deep for a multishot one.
Why
A multishot accept stays armed and delivers many connections from one operation. The backend fills the turn's event vector up to
events.capacity(), which isConfig::events_per_turn, so one turn can deliver up toevents_per_turnconnections from a single armed operation. The operation holds one reservation and re-reserves after each delivery (Driver::attach_reserved), so it is protected for the next connection only. Once the loop is at its ceiling mid-batch, the remaining connections in that batch have no slot and complete withResourceLimit— accepted by the kernel, then destroyed, which is the behaviour #75 set out to remove.Single-shot accepts, which is what a host re-arming per connection does, are unaffected.
What it needs
The driver has to be able to say how many completions it can house this turn, which means a budget the backend honours:
Each backend then caps its fill at
budget.min(events.capacity() - events.len())—Unix::run_readyalready computes exactly that quantity from the vector's capacity, so the change is small per backend but touches all six. The driver would passmin(events_per_turn, handles.remaining() - reserved_handles)when a multishot accept is armed.Two things to be careful about, which is why this was not folded into #75:
Acceptance
events_per_turn, floods the listener, and asserts no accept completes withResourceLimit— connections wait in the backlog instead.