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 whose MEM_SIZE / MEMP_NUM_TCP_SEG pool is small enough to exhaust under the unacked queue while tcp_sndbuf still reports room.
select.select([], [sock], []) returns the socket writable (POLLOUT reads tcp_sndbuf).
sock.write(buf) then blocks inside lwip_tcp_send instead of returning EAGAIN.
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.
Port, board and/or hardware
Not port-specific — the code is the generic
lwippath inextmod/modlwip.c, shared by everyMICROPY_PY_LWIPport (rp2, stm32, mimxrt, esp8266, …). Measured on an OpenMV RT1062 (i.MX RT1062, cyw43 WiFi).MicroPython version
master:extmod/modlwip.c,lwip_tcp_send, theERR_MEMfor (;;)loop (around L1091–1110) that waits withpoll_sockets()under a 10 s cap. The same loop is present on the v1.28.0 line, where it waits withmp_hal_delay_ms(50); the numbers below were measured on a v1.28.0-based OpenMV fork.Reproduction
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_writereturningERR_MEMwhiletcp_sndbufis 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 returnsEAGAIN, as it already does from thetcp_sndbuf == 0path in the same function. The caller retries; no data is lost, becausetcp_writequeues zero bytes onERR_MEM.Observed behaviour
sock.write()blocks up to the 10 s cap. TheERR_MEMloop has nosocket->timeout == 0check, so a non-blocking socket waits there. The loop documents this itself:
The "committed" rationale does not hold for a non-blocking socket:
lwip_tcp_sendhas returned nothing to the caller yet, and the commitment rests ontcp_sndbufwhile 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: theworst single
write()reached 1553086 us and 54 events exceeded 100 ms over a 320 s run.Additional Information
A guard in the
ERR_MEMloop returningEAGAINwhensocket->timeout == 0— aftertcp_output(), before the wait — cut the worstwrite()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 againstmasterif the direction is agreeable.Code of Conduct
Yes, I agree.