Current implementation in data_received and datagram_received path is reading only once and then rely on the loop to call it again, which basically mean there will be epoll_wait or similar before we read again.
AI did research on other libraries:
Short version: there is no single universal strategy. The high-performance libraries split into two styles.
Operation-Based
Boost.Asio and Tokio mostly expose one receive operation as one completion.
For UDP:
For streams:
This style favors predictable latency/fairness and lets application code decide batching.
Callback/Handle-Based
libuv is more aggressive internally.
For streams:
- uv_read_start() says the read callback is made “several times until there is no more data to read or uv_read_stop() is called.” It also documents nread == 0 as equivalent to EAGAIN / EWOULDBLOCK. Source: libuv stream docs:
https://docs.libuv.org/en/latest/stream.html
For UDP:
What This Means For aiofastnet
For asyncio-compatible Python transports, the Boost/Tokio style is closer semantically: one readiness callback, one datagram_received for UDP. That matches what just fixed your regression.
For stream data_received, libuv’s strategy supports looping, but with a cap/fairness guard. I would not do unconditional drain-until-EAGAIN in Python/Cython if it allocates before the failing syscall. A better experiment is:
- UDP: one recv per readiness callback.
- Stream: maybe retry only if the previous read filled the whole buffer.
- Optional later: bounded read_batch_limit, e.g. 1, 4, 16, 32.
- Avoid allocating the Python bytes object before a speculative read that may return EAGAIN, if we ever reintroduce draining.
Current implementation in data_received and datagram_received path is reading only once and then rely on the loop to call it again, which basically mean there will be epoll_wait or similar before we read again.
AI did research on other libraries: