Skip to content

modlwip: non-blocking socket blocks in send when out of memory #19704

Description

@srgg

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

  1. 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.
  2. select.select([], [sock], []) returns the socket writable (POLLOUT reads tcp_sndbuf).
  3. 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.

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