Skip to content

add reuse_port option to enable SO_REUSEPORT on TcpServer - #525

Open
Lfan-ke wants to merge 2 commits into
moonbitlang:mainfrom
Lfan-ke:reuse-port
Open

add reuse_port option to enable SO_REUSEPORT on TcpServer#525
Lfan-ke wants to merge 2 commits into
moonbitlang:mainfrom
Lfan-ke:reuse-port

Conversation

@Lfan-ke

@Lfan-ke Lfan-ke commented Jul 29, 2026

Copy link
Copy Markdown

TcpServer exposes reuse_addr (SO_REUSEADDR) but has no way to set SO_REUSEPORT. SO_REUSEPORT lets several sockets, typically one per worker process, bind the same address at the same time, with the kernel spreading incoming connections across them. It's the usual basis for a prefork server (nginx's reuseport, the multi-worker mode of uvicorn/gunicorn), and SO_REUSEADDR alone can't express it.

This adds a reuse_port? : Bool = false parameter to TcpServer, mirroring the existing reuse_addr exactly:

  • socket.c: moonbitlang_async_allow_reuse_port, guarded by #ifdef SO_REUSEPORT so it compiles to a no-op where the option doesn't exist.
  • ffi.mbt / ffi.wasm.mbt: the matching binding; the wasm side is a no-op, since wasm has no multi-process model.
  • tcp.mbt: the option is applied before bind, and gated off on Windows just like reuse_addr.

SO_REUSEPORT is available on Linux (3.9+), the BSDs and macOS; on Windows and wasm the option is ignored.

The added native-only test binds two servers to the same port at once, which only succeeds with SO_REUSEPORT, and checks that connections are served across both.

Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
@Guest0x0

Copy link
Copy Markdown
Collaborator

In fact SO_REUSEPORT is only useful on Linux. On MacOS/BSD, although the option SO_REUSEPORT exists, it does not have the Linux style load balancing semantic. Instead, which socket will receive a new incoming connection is undefined (your test has only one server listening when new connection arrives, so it does not cover the load balancing semantic). IMO there are two design options here:

  1. reuse_port? : Bool just mean setting SO_REUSEPORT on the socket, whatever that means in the underlying OS
  2. reuse_port? : Bool is a Linux-only option for load balancing on the same listen address via multiple sockets. In this case perhaps it need an alternative name to avoid confusion

(1) sounds bad to me. Currently moonbitlang/async tries to avoid exposing underlying OS detail and provide API based on high level semantic whenever possible. (2) is still platform specific, but at least it has much cleaner semantic.

BTW it would be very helpful if you could share your use case that requires SO_REUSEPORT, because AFAIK moonbitlang/async does not currently support non-exec forking. A lot of things probably just won't work after forking.

…ame to reuse_port_lb

Per review: SO_REUSEPORT only load-balances connections on Linux; on the BSDs
and MacOS which socket accepts a connection is unspecified. Enable it only on
Linux, rename the option to reuse_port_lb to signal the load-balancing intent,
and document the exec-based multi-worker use case it serves. The test now runs
only on Linux, where the shared-port bind is meaningful.

Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
@Lfan-ke

Lfan-ke commented Aug 4, 2026

Copy link
Copy Markdown
Author

Thanks, that's a fair distinction. I went with your option (2).

The use case: I'm building a small ASGI/HTTP server on top of moonbitlang/async (a uvicorn analog). For multi-worker deployment on Linux the plan is a supervisor that execs N worker processes; each worker binds the same host:port with SO_REUSEPORT and the kernel load-balances incoming connections across them. It's exec-based, not fork-based — each worker opens and binds its own socket independently, so nothing has to survive a fork and there's no fd to share. That's exactly why the option is enough on its own; it doesn't need async to support non-exec forking.

Given that, I've reworked the PR to your option (2):

  • The option now takes effect only on Linux (where SO_REUSEPORT actually distributes connections); on the BSDs/MacOS — where which socket accepts a connection is unspecified — it's ignored, same as Windows/wasm.
  • Renamed reuse_portreuse_port_lb to signal the load-balancing semantic rather than the raw socket option, and rewrote the doc comment to spell out the Linux-only behavior and the exec-based worker model.
  • The test is now #cfg(platform="linux"), since the two-servers-one-port bind only holds where the option is live.

Happy to pick a different name if you'd prefer something else — reuse_port_lb was just the clearest thing I could think of that keeps the SO_REUSEPORT association while making the intent obvious.

// `reuse_port_lb` only takes effect on Linux (that is where SO_REUSEPORT
// load-balances), so the shared-port bind below is exercised there; the test is
// compiled out elsewhere.
#cfg(platform="linux")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, #cfg(platform) only support windows currently. You can add a runtime guard @event_loot.platform is Linux else { return } check to the beginning of the test instead.

let server2 = @socket.TcpServer(addr, reuse_port_lb=true)
// Close the first server so the exchange below deterministically goes
// to server2, which is now the only socket listening on that port.
server1.close()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try if we can have a test with two servers listening concurrently to observe the load balancing behavior. To avoid unstable test result, we can only demand both servers can receive new connections, without asserting the order they receive new connections

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants