Skip to content

find_free_port can return ports that collide with outbound traffic on busy hosts (EADDRINUSE at TCPStore bind) #517

Description

@CAICAIIs

Bug

find_free_port() (areno/engine/protocol.py) hands out ports that can be
re-taken by outbound traffic between the check and the TCPStore bind, so
distributed worker startup fails on busy hosts with a spurious
DistNetworkError: ... EADDRINUSE.

Root cause

def find_free_port() -> int:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind(("127.0.0.1", 0))          # OS picks lowest free ephemeral port
        return int(sock.getsockname()[1])    # ... then the socket is closed

bind(0) allocates from the kernel ephemeral range (32768–60999 on Linux) and
the reservation socket is closed immediately. Two things then race before the
worker's TCPStore server binds the same port:

  1. Consecutive calls return the same port. bind(0)close()
    bind(0) reuses the just-freed lowest ephemeral port, so the train and
    rollout clusters can be handed the identical port in one startup (two
    find_free_port() call sites: protocol.py cluster start and
    backend.py rollout cluster).
  2. Outbound connections steal the freed port. Any new outbound connection
    in the same process (model hub downloads, metrics, etc.) can take the
    just-freed port as its source port; after that connection closes, the port
    sits in TIME-WAIT, and a subsequent TCPStore bind fails with
    EADDRINUSE (no SO_REUSEADDR).

Observed on a shared 8×A100 node: 4 consecutive areno train startups
failed
with EADDRINUSE on ports 39625 / 34127 / 58051 / 49989, each
reported as freshly "free" by find_free_port. ss at failure time showed
the chosen port in TIME-WAIT as the source of an outbound connection
(e.g. 192.168.200.17:43171 → 192.168.200.17:15333).

Repro conditions

  • Linux host with any outbound connection activity around worker startup
    (model download, metrics, or other users' traffic on a shared box);
  • or any run that creates more than one cluster (train + rollout partitions),
    where two find_free_port() calls race for the same ephemeral port.

A local patch that makes find_free_port pick a random port outside the
ephemeral range (20000–30000), verify it is free with a bind probe, and keep
returned ports distinct within the process fixes both failure modes; all
subsequent training runs on the same busy host succeeded.

Suggested fix

  • allocate from a fixed non-ephemeral range instead of bind(0);
  • keep a process-local set of already-returned ports;
  • optionally retry the TCPStore bind on EADDRINUSE as a belt-and-braces
    fallback.

Happy to open a PR with the fix plus a CPU test that asserts consecutive calls
return distinct ports and that the chosen port is bindable.

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