Skip to content

A multishot accept can outrun the handle ceiling within one turn #77

Description

@proggeramlug

#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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions