Skip to content

cluster: make cluster/fork work under WASIX (fork IPC + reuseport scheduling) - #115

Open
Arshia001 wants to merge 8 commits into
mainfrom
wasix-cluster-reuseport
Open

cluster: make cluster/fork work under WASIX (fork IPC + reuseport scheduling)#115
Arshia001 wants to merge 8 commits into
mainfrom
wasix-cluster-reuseport

Conversation

@Arshia001

Copy link
Copy Markdown
Member

Split out of #111.

Makes Node's cluster (and child_process.fork IPC) functional under WASIX:

  1. Bump libuv-wasix: plain read() for IPC streams — the IPC read path was stubbed to ENOSYS under __wasi__, killing every fork/cluster message channel. Fork IPC now works end-to-end (online handshake, bidirectional process.send, listen negotiation).
  2. Bump libuv-wasix: enable SO_REUSEPORTuv__sock_reuseport failed UV_ENOTSUP before consulting the runtime, though the whole path below works (wasix-libc → sock_set_opt_flag → wasmer applies it to the host socket before bind). (libuv-wasix changes: wasix: enable SO_REUSEPORT in uv__sock_reuseport wasix-org/libuv#9)
  3. Native reuseport scheduling strategy (TCP + UDP) — WASIX cannot pass listen handles between processes (no SCM_RIGHTS), so both upstream scheduling strategies are unusable there. Instead, workers bind their own UV_TCP_REUSEPORT/UV_UDP_REUSEPORT handles and the host kernel balances connections. Implemented in src/edge_cluster_wasix.cc (project policy keeps lib/ byte-identical to upstream) as an embedded script that replaces the worker-side cluster._getServer; no primary-side changes. Compile-time gated to WASIX.
  4. Skip-list cleanupWASIX_SKIP_CLUSTER_FORK_TESTS is now empty: every cluster/fork test passes in the wasix lanes. The two tests that exec external guest binaries (test-http-chunk-problem, test-http-full-response) move to the scaled-timeout slow bucket (cold wasmer cache compiles on CI runners), not the skip list.

Known limitation: _getServerData/_setServerData is not round-tripped through the primary (TLS session ticket keys stay per-worker).

Verified under WASIX: raw TCP connections balance 13/11 across two workers, UDP echo distributes 11/5, full wasix quickjs suite 1686 passed / 0 failed.

🤖 Generated with Claude Code

Arshia001 and others added 8 commits July 15, 2026 11:07
The IPC read path was stubbed to ENOSYS under __wasi__, killing every
child_process.fork / cluster.fork message channel on first readiness
(stream error -> fd closed -> sends fail EPIPE). With the fix, fork IPC
message channels work end-to-end under WASIX: worker online handshake,
process.send in both directions, and cluster's listen negotiation all
function. Cluster serving still needs connection handle-passing (or a
reuseport-style strategy) and js-firekylin stays skipped on WASIX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
uv__sock_reuseport failed UV_ENOTSUP under __wasi__ before the runtime was
consulted, even though the whole path below works (wasix-libc maps
SO_REUSEPORT to sock_set_opt_flag; wasmer applies it to the host socket
before bind). Verified under WASIX: same-port listeners in one process and
across forked processes, EADDRINUSE still enforced without the flag,
40 connections balanced 18/22 across two worker processes, and Node's
test-dgram-reuseport.js passes.

This provides the primitive for the reuseport-based cluster scheduling
strategy that would let cluster-served apps (js-firekylin) run on WASIX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WASIX cannot pass listen handles between processes (no SCM_RIGHTS), which
made both of Node cluster's scheduling strategies unusable there: round
robin passes every accepted connection to a worker, and the shared-handle
mode passes the listen handle itself. SO_REUSEPORT, however, works end to
end under WASIX (wasix-libc -> sock_set_opt_flag -> wasmer applies it to
the host socket), with genuine kernel-level connection balancing across
forked guest processes.

Add a third scheduling strategy for WASIX. Project policy keeps the Node
lib/ tree byte-identical to upstream, so it lives in native code
(src/edge_cluster_wasix.cc) and is installed from EdgeRuntime before the
main builtin executes: in WASIX cluster workers (NODE_UNIQUE_ID still
present at that point), an embedded script replaces the worker-side
cluster._getServer — an exported, replaceable property — so TCP port
listens bind their own UV_TCP_REUSEPORT handle instead of asking the
primary for one, and report the 'listening' act for the primary's
bookkeeping. No primary-side changes are needed at all: the primary never
learns a handle key, so its registry and cleanup paths are untouched.
UDP, fd, and pipe listens keep the upstream path, as does everything on
native targets (compile-time gate).

Known limitation inherited from the child-only shape: _getServerData/
_setServerData is not round-tripped through the primary (e.g. TLS session
ticket keys stay per-worker).

Verified under WASIX: cluster workers balance raw TCP connections 13/11,
cluster 'listening' events fire in the primary, and js-firekylin
(ThinkJS, cluster-served) passes the framework test. Regressions green:
js-firekylin native (round robin unchanged), js-hedgedoc/js-rssmonster
WASIX, js-svelte native.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With fork IPC (libuv-wasix plain read) and the cluster reuseport strategy
in place, 11 of the 17 tests in WASIX_SKIP_CLUSTER_FORK_TESTS pass and are
removed from the skip list, including TCP cluster serving
(test-http-server-drop-connections-in-cluster, test-tls-ticket-cluster)
and the child_process fork/messaging tests
(test-diagnostics-channel-process, the domain and http fork harnesses).

Two entries were misfiled and move to their real groups:
test-http-client-with-create-connection fails on a unix-socket listen
(unix-socket group) and test-crypto-secure-heap fails on OpenSSL secure
heap (crypto group). What remains cluster-specific is UDP cluster listens,
which still go through shared-handle passing, plus the known_issues
negative test whose error-swallowing path (exit 0 on non-success worker
messages) engages now that fork IPC delivers messages — the upstream known
issue is not observable under WASIX.

Full wasix quickjs suite locally: 1681 passed, 0 failed
(baseline before: 1671 with the old skip list).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UDP cluster listens went through shared-handle passing and were the last
cluster capability broken under WASIX. The worker-side override now also
covers udp4/udp6 port listens via dgram._createSocketHandle with
UV_UDP_REUSEPORT; the kernel distributes datagrams between the workers by
source hash (flows pin to a worker) instead of shared-socket delivery.

Two contract details surfaced by the upstream tests:
- _getServer callbacks must stay asynchronous (an IPC round trip
  upstream); the override now defers cb via process.nextTick, which
  test-dgram-cluster-close-during-bind's close-during-bind window depends
  on.
- dgram passes the raw bind() arguments through: options.port can be
  null, undefined, or the bind callback function (socket.bind(cb)); all
  of those mean an ephemeral-port listen per the
  bind([port][, address][, callback]) signature.

WASIX_SKIP_CLUSTER_FORK_TESTS is now empty: every cluster/fork test in
the wasix lanes passes, including the known_issues negative test (back to
failing-as-expected: with reuseport the port-0 rebind scenario behaves
deterministically again). Verified: cluster UDP echo distributes 11/5
across two workers; full wasix quickjs suite 1686 passed / 0 failed;
js-firekylin green on WASIX and native.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test spawns an external cksum binary in the guest; the first exec
cold-downloads and LLVM-compiles wasmer/coreutils, which exceeds the
per-test timeout on CI runners with an empty wasmer cache. It passes
locally with a warm cache, so this is an environment cost, not a
cluster/fork or subprocess capability gap. Filed under the
subprocess-shell group. (CI wasix suite was otherwise green:
1685 passed / 1 failed.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same class as test-http-chunk-problem: the test execs ab through a shell,
and on CI runners with an empty wasmer cache the first external exec
cold-downloads and compiles wasmer/bash + wasmer/coreutils, exceeding the
per-test timeout. Locally the test self-skips gracefully ('problem
spawning ab') because the warm-cached shell starts fast enough. These two
are the only external-binary tests among the recent cluster/fork unskips;
the remaining nine are node-child-only and passed CI twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ipping

test-http-chunk-problem (spawns cat) and test-http-full-response (execs ab
through a shell) rely on guest binaries that ARE available (wasmer/bash,
wasmer/coreutils); their CI timeouts came from the first exec cold-
downloading and compiling those packages, not from a capability gap. Give
them the scaled timeout (WASIX_SLOW_TESTS, 12x) and keep the coverage
rather than skipping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant