Status: Superseded by ADR-303 — its D1-D5 are D8-D12 there Date: September 22, 2026 Target Version: Nikaia 0.0.165 Builds: #90’s second step — the socket layer that keeps registrations rather than answering one readiness question at a time Narrows: ADR-038 D3’s readiness half — the mechanism, not the surface Related: ADR-121 D1 and D4 (the bell, and readiness as something awaitable), ADR-296 D12 (measure before choosing a shape, and this is what that is for), ADR-297 D6 (the drain this had to keep)
A readiness wait was a [worker::Op]. The descriptor was duplicated, handed to
an I/O worker over a channel, and the worker blocked in the poller for the
whole of the wait.
io-workers is 1 by default — ADR-038 D4’s one I/O thread
always runs. So a wait that had not answered blocked every other wait in the
process.
That is not a slowness; it is a ceiling on the default configuration.
Measured before anything was written: two readiness waits at once, the second on
a pipe that already had a byte in it. The second did not get a turn in two
seconds. A server waiting on accept while a connection waits on read is that
shape exactly, so an HTTP server could not have been built on it.
wait, and arming happens on the calling thread.Registering is an epoll_ctl. It does not block, so there is nothing to hand to
anybody: the caller arms and gets a future. When the kernel answers, the thread
fills the slot, wakes the waker and rings the bell.
Nothing occupies a thread while it waits for something that has not happened, which is the whole of what was wrong.
ADR-296 D12 asks for a measurement before a shape is chosen. It was taken, and it moved the design:
| per wait | |
|---|---|
| a poller built per wait, which is what the worker did | 7.4 µs |
one poller, add and delete around each wait |
2.5 µs |
| a registration kept and re-armed | 1.2 µs |
| the whole round trip through a worker | 35 µs |
#90’s own words for this step were keep registrations, and keeping them is worth 6.2 µs of 35. The hop was worth 28. So the step is not what the line said it was, and the line was written before anything had been measured — which is the case that rule exists for.
After: rt::io::waiting, which is what a server uses, goes from 35.4 µs
to 6.6 µs. rt::io::wait, the blocking one nothing hot uses, goes from 35.3
to 29.2 — it is a condvar round trip with the readiness thread and stays one.
Runtime::pending counts them and ADR-297 D6’s drain waits them
out. That is not a new promise: it is the one the worker path made, kept through
a different mechanism. The park asks is anything outstanding before it sleeps,
and the drain asks it before it lets a program go.
Waker does not reach a parked executor.ADR-121 D1’s bell is what wakes a thread asleep in
io_uring_enter or on the condvar, and neither watches an alarm. So the
readiness thread rings it as a worker did, and the waker it also wakes is for
the executor that honours one.
Dropping the future deletes the registration. For a socket that is a connection the program stopped caring about, and it is the case a server meets most.
nikaia serve [dir].worker::Op::Readiness and poll_one are gone. The worker enum is back
to what it is for: operations that do something, where blocking a thread is
the work rather than the waiting.net::Connection and the registry that buys less than this step did.net passes one; ADR-289 D13’s caps are the server’s.All of it. crates/nikaia-std/src/rt/readiness.rs, both rt::io surfaces
routed through it, Op::Readiness and poll_one deleted, pending and the
drain carrying registrations, and
a_wait_that_never_answers_does_not_block_one_that_would in
crates/nikaia-std/tests/sockets.rs — which is the two-second stall, written
down so it cannot come back.