modlwip: non-blocking socket blocks in send when out of memory
Port, board and/or hardware
Not port-specific — the code is the generic lwip path in extmod/modlwip.c, shared by every MICROPY_PY_LWIP port (rp2, stm32, mimxrt, esp8266, …). Measured on an OpenMV RT1062 (i.MX RT1062, cyw43 WiFi).
MicroPython version
master: extmod/modlwip.c, lwip_tcp_send, the ERR_MEM for (;;) loop (around L1091–1110) that waits with poll_sockets() under a 10 s cap. The same loop is present on the v1.28.0 line, where it waits with mp_hal_delay_ms(50); the numbers below were measured on a v1.28.0-based OpenMV fork.
Reproduction
- A non-blocking
socket(setblocking(False)) writing to a peer whose receive window backs up, on a port whoseMEM_SIZE/MEMP_NUM_TCP_SEGpool is small enough to exhaust under the unacked queue whiletcp_sndbufstill reports room. select.select([], [sock], [])returns the socket writable (POLLOUT readstcp_sndbuf).sock.write(buf)then blocks insidelwip_tcp_sendinstead of returningEAGAIN.
The trigger is tcp_write returning ERR_MEM while tcp_sndbuf is non-zero — the byte counter and the segment/heap pool are different resources and disagree under memory pressure.
Expected behaviour
sock.write() on a non-blocking socket returns EAGAIN, as it already does from the
tcp_sndbuf == 0 path in the same function. The caller retries; no data is lost, because
tcp_write queues zero bytes on ERR_MEM.
Observed behaviour
sock.write() blocks up to the 10 s cap. The ERR_MEM loop has no socket->timeout == 0
check, so a non-blocking socket waits there. The loop documents this itself:
// Note: if the socket is non-blocking, then this code will actually block until
// there's enough memory to do the write, but by this stage, we have already
// committed to being able to write the data.
The "committed" rationale does not hold for a non-blocking socket: lwip_tcp_send has returned nothing to the caller yet, and the commitment rests on tcp_sndbuf while the failure comes from the segment/heap pool. On a single-threaded event loop, the block stalls every other task. Measured on the OpenMV RT1062 serving an MJPEG stream to a reader throttled to 20 KB/s: the
worst single write() reached 1553086 us and 54 events exceeded 100 ms over a 320 s run.
Additional Information
A guard in the ERR_MEM loop returning EAGAIN when socket->timeout == 0 — after tcp_output(), before the wait — cut the worst write() to 3484 us and the over-100 ms events to 0 on the same test, with no change for a blocking socket. Happy to open a PR against master if the direction is agreeable.
Code of Conduct
Yes, I agree.
Source: micropython/micropython