fix(wasix): de-flake udp-large-recv and stream-tcp-writev-partial tests - #6789
fix(wasix): de-flake udp-large-recv and stream-tcp-writev-partial tests#6789Arshia001 wants to merge 4 commits into
Conversation
Both socket integration tests were flaky under nextest (issue #6785), timing out / returning wrong values under load, primarily on macOS. udp-large-recv relied on reliable delivery of a single 20480-byte UDP datagram over loopback. UDP is best-effort, so a dropped datagram left the blocking recvfrom to wait out the socket's 30s read timeout and return -1. Retry the exchange a few times with a poll()-bounded wait (wasix-libc does not wire SO_RCVTIMEO), skip (exit 0) if no datagram is ever delivered, and only fail on truncation/sharding/corruption when a datagram does arrive. stream-tcp-writev-partial relied on the peer's RST landing in the window between two back-to-back internal send() calls of a single writev, which is inherently racy (returns first-iovec len, full len, or -1 depending on timing). Rewrite it to exercise the same "return bytes already transferred" fd_write contract via a deterministic short write: a tiny first iovec followed by an oversized second iovec that can never fully fit the send buffer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates two WASIX socket integration tests to reduce flakiness under nextest/concurrent load, without changing runtime behavior. It aims to avoid long hangs on dropped UDP datagrams and to make the TCP writev partial-write assertion deterministic.
Changes:
udp-large-recv: retry send/receive attempts withpoll()-bounded waits and skip when no datagram is ever delivered.stream-tcp-writev-partial: rework the test to provoke a partialwritev()via non-blocking I/O and an oversized second iovec instead of racing a peer close/RST.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| lib/wasix/tests/wasm_tests/socket/udp-large-recv/main.c | Adds bounded polling + retry/skip behavior to avoid hangs on dropped large UDP datagrams. |
| lib/wasix/tests/wasm_tests/socket/stream-tcp-writev-partial/main.c | Rewrites the test to target a deterministic short write path for writev partial-success behavior. |
| int pr = poll(&pfd, 1, RECV_TIMEOUT_MS); | ||
| if (pr <= 0 || (pfd.revents & POLLIN) == 0) { | ||
| // Timed out waiting for the datagram (dropped in transit); retry. | ||
| fprintf(stderr, "attempt %d: poll timed out (pr=%d revents=0x%x)\n", | ||
| attempt, pr, pfd.revents); | ||
| continue; | ||
| } |
| ssize_t nread = recvfrom(receiver, recvbuf, sizeof(recvbuf), 0, NULL, NULL); | ||
| if (nread < 0) { | ||
| fprintf(stderr, "attempt %d: recvfrom failed (errno %d: %s)\n", attempt, | ||
| errno, strerror(errno)); | ||
| continue; | ||
| } |
| if (written <= (ssize_t)FIRST_IOV_LEN || | ||
| written >= (ssize_t)(FIRST_IOV_LEN + (size_t)SECOND_IOV_LEN)) { | ||
| fprintf(stderr, | ||
| "expected writev to return %d bytes after peer close, got %zd " | ||
| "errno=%d (%s)\n", | ||
| FIRST_IOV_LEN, written, errno, strerror(errno)); | ||
| "expected partial writev in (%d, %zu), got %zd errno=%d (%s)\n", | ||
| FIRST_IOV_LEN, FIRST_IOV_LEN + (size_t)SECOND_IOV_LEN, written, |
The stream-tcp-writev-partial rewrite exercises fd_write's partial-return contract through the short-write branch, but not the exact `Err(_) if sent > 0 => break` arm the original regression fixed. That arm requires a later per-iovec send() to error after an earlier iovec already succeeded, which cannot be triggered deterministically over real host sockets (it races an asynchronous RST landing between two back-to-back sends of one writev). Add a test-only mock VirtualNetworking whose TCP socket succeeds on the first send() and returns ECONNRESET afterwards, and drive a small C guest (connect + writev of two iovecs) against it via the existing runtime-config test harness. writev must return the first iovec length. Verified it fails (returns -1) when the partial-return arm is removed. No application code is changed; async-trait is added as a dev-dependency for the mock impl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added a follow-up commit that closes the coverage gap noted above.
The new
|
…ull writev Incorporate PR review feedback so the de-flaked tests can still catch regressions instead of masking them: - udp-large-recv: distinguish a dropped datagram (poll() timeout, pr==0) from a genuine error. Retry on EINTR, treat pr==0 as a drop, but fail hard on any other poll() error or unexpected revents, and on a recvfrom() error after POLLIN (readiness reported then recv fails is a real bug, not a drop). - stream-tcp-writev-partial: if the host's socket buffers accept the whole oversized write, we cannot force a short write; treat that as an environment limitation and skip rather than false-fail. Still fail on written < 0 (the regression). Drop the ExpectedStdout directive since the skip path no longer prints it; assert via exit code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks — addressed all three review points in bc1cf3f:
Re-verified: happy paths pass on both libc variants, the udp skip path still exits 0 (~8s) on forced drops, and the writev mock test still fails when the |
| ssize_t nsent = sendto(sender, sendbuf, PAYLOAD_SIZE, 0, | ||
| (struct sockaddr*)&addr, sizeof(addr)); | ||
| if (nsent != PAYLOAD_SIZE) { | ||
| // e.g. macOS rejects datagrams larger than net.inet.udp.maxdgram with | ||
| // EMSGSIZE. That is not the behaviour under test, so treat it as a | ||
| // dropped attempt. | ||
| fprintf(stderr, "attempt %d: sendto returned %zd (errno %d: %s)\n", | ||
| attempt, nsent, errno, strerror(errno)); | ||
| continue; | ||
| } |
| char* big = malloc(SECOND_IOV_LEN); | ||
| if (big == NULL) { | ||
| fprintf(stderr, "malloc failed\n"); | ||
| close(client); | ||
| close(server); | ||
| return 1; | ||
| } | ||
| memset(big, 'w', SECOND_IOV_LEN); |
- udp-large-recv: only treat expected best-effort/oversize/transient sendto() failures (EMSGSIZE/ENOBUFS/EAGAIN/EWOULDBLOCK/EINTR) as a dropped attempt; any other errno now fails the test instead of being skipped, so a real sendto regression (EBADF/EINVAL/...) cannot be masked. A short non-negative datagram send is also treated as a bug. - stream-tcp-writev-partial: skip (exit 0) if the 64 MiB allocation fails rather than hard-failing, so constrained environments don't reintroduce flakiness; drop the memset since only the returned byte count is asserted. - Apply clang-format (Google style) to satisfy the Code lint CI job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the re-review in 2bf9566 (also fixed the clang-format CI failure in the same commit):
Re-verified all three tests pass on both libc variants and the udp skip path still exits 0 (~8s) under forced drops. clang-format (Google style) now passes locally. |
| } | ||
| })); | ||
|
|
||
| tests.push(libtest_mimic::Trial::test( |
There was a problem hiding this comment.
Don't like this (apparently second one) special-case. If have have a special need from the harness, we should introduce a new directive and annotate the corresponding expectations in the test-case.
| @@ -0,0 +1,175 @@ | |||
| //! A mock networking backend used by the `writev_partial_send_error` test. | |||
| //! | |||
| //! It hands out a TCP socket whose first `try_send` succeeds in full and whose | |||
There was a problem hiding this comment.
Well, this only covers one particular scenario - it's not any generic mocking harness. How much do we benefit from the specific scenario?
|
For unblocking of the CI on |
Summary
Fixes the two flaky socket integration tests reported in #6785. Both flaked under
nextest(worse on macOS and under concurrent load) — no runtime code is changed, only the tests.udp-large-recvThe test sent a single 20480-byte UDP datagram over loopback and did one blocking
recvfrom. UDP is best-effort: a dropped datagram leftrecvfromto wait out the socket's default 30s read timeout and return-1(ETIMEDOUT) — the "gets stuck ~1 min then fails" symptom. This is common on macOS (defaultnet.inet.udp.maxdgramis 9216, and loopback drops large datagrams under load) and under nextest's concurrent-process load.Now it:
poll(POLLIN, 1s)(wasix-libc doesn't wireSO_RCVTIMEO, sopoll()is used, matching theudp-readinesstest);sendto(e.g. macOSEMSGSIZEon oversized datagrams) as a dropped attempt;stream-tcp-writev-partialThe test relied on the peer's RST landing in the tiny window between the two back-to-back internal
send()calls of a singlewritev. That transition is an asynchronous RST round-trip, so depending on timing the syscall returned the first-iovec length (pass), the full length, or-1— inherently racy. VirtualSO_SNDBUF/SO_RCVBUFtuning is a no-op on host sockets, so the buffer can't be shrunk to make the boundary controllable either.Rewritten to exercise the same
fd_writecontract — "return bytes already transferred instead of failing the whole syscall" — via a deterministic short write: client is non-blocking, server never reads, andwritev([5 bytes, 64 MB])sends the small iovec in full and short-writes the oversized one, so the loop returns a partial total. A whole-syscall failure (the regression) would surface as-1and fail the assertion.Caveat: this guards the
local_sent != buf.len() => breakbranch rather than the exactErr(_) if sent > 0 => breakarm the original regression fixed — same user-visible contract, different line. Deterministically hitting the error arm from guest code isn't possible (it needs an async error mid-syscall); guarding that specific line would require a runtime-side unit test against a mockVirtualTcpSocket.Testing
udp-large-recv: happy path passes; forced persistent drops skip in ~8s (exit 0, no 30s hang); forced short delivery fails as expected. Verified ondefaultandv2026-05-12.1libc (cranelift).stream-tcp-writev-partial: 6/6 deterministic passes on both libc variants; confirmed it produces a genuine multi-MB partial.Closes #6785
🤖 Generated with Claude Code