Skip to content

fix(runtime): report TCP broadcast write failures with -1 and errno#2671

Open
slepp wants to merge 2 commits into
mainfrom
fix/2657-tcp-broadcast-error-semantics
Open

fix(runtime): report TCP broadcast write failures with -1 and errno#2671
slepp wants to merge 2 commits into
mainfrom
fix/2657-tcp-broadcast-error-semantics

Conversation

@slepp

@slepp slepp commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Why

hew_tcp_broadcast_except silently swallowed per-recipient write failures: a dead peer, a failed stream clone, or a partial write left the loop with continue and the function still returned a success-shaped recipient count. The try_broadcast_except wrapper in std/net/net.hew already declared an n < 0 → NetError path, but the runtime could never produce a negative return, so callers had no way to observe delivery failures.

What

  • runtime: every eligible recipient is still attempted (best-effort delivery preserved), but the first failure's message and OS errno are now recorded. The failure data is built as owned values inside the TCP table lock and emitted to the thread-local error sink after the lock is released. The return is tri-state: -1 when any eligible recipient failed (errno recorded), 0 when there were zero eligible recipients, delivered count otherwise. All three failure branches (stream clone, body write, newline write) feed the same accounting.
  • std/net: doc comments on try_broadcast_except / broadcast_except now describe the tri-state contract and the failure mapping; no behaviour change in the wrappers — the existing NetError path simply fires now.

Test

Five new regression tests in hew-runtime/src/transport.rs, run in isolated processes where they mutate process-global state (SIGPIPE disposition, RLIMIT_NOFILE):

  • broadcast_all_recipients_failed_returns_minus_one_with_errno — deterministic peer RST, asserts -1 + non-zero errno
  • broadcast_partial_failure_returns_minus_one_and_still_delivers — one dead + one healthy peer; healthy peer receives the full frame, return is -1
  • broadcast_clone_failure_records_errno_and_message — fd-limit-induced clone failure, asserts the clone-branch message prefix + errno
  • broadcast_zero_eligible_recipients_returns_zero — pins 0 (clean no-op) as distinct from -1, no errno recorded
  • broadcast_success_returns_delivered_count — happy-path count unchanged

Each failure assertion (== -1 + recorded errno) is unproducible by the old code, which could only return recipients >= 0.

Quality Checklist

  • PR title, body, and commit messages avoid internal-only orchestration/model vocabulary
  • No new .ok()? or unwrap_or_default() in codegen without // JUSTIFIED comment
  • New allocations have cleanup paths for sync, async, and actor shutdown contexts
  • Serialization changes include round-trip encode/decode tests (n/a — no serialization changes)
  • New runtime features have WASM implementation or // WASM-TODO(#NNN): marker, and new hew_* exports are classified in scripts/jit-symbol-classification.toml (no new exports; existing symbol's return semantics documented)
  • No duplicated logic — checked for existing helpers before adding new ones (errno recording mirrors hew_tcp_write)

Out of scope

  • Broadcast writes still happen under the global TCP table lock; moving the write loop off-lock is a broader change to the table's ownership model.
  • A partial body write can leave half a frame in a recipient's stream (pre-existing; the failure is now at least reported).
  • hew_tcp_write, hew_tcp_close, and the reactor paths are untouched.

Fixes #2657

slepp added 2 commits July 15, 2026 13:09
hew_tcp_broadcast_except silently skipped per-recipient clone and write
failures: when every eligible write failed it returned 0, and the stdlib
lifted that to Ok(0) — indistinguishable from having no recipients at
all. Callers could acknowledge or discard messages under a false-success
result.

Delivery stays best-effort: every eligible recipient is still attempted
so one dead peer never denies the healthy ones. The function now records
the first failing operation's message and OS errno in the shared
thread-local (the same set_last_error_with_errno channel hew_tcp_write
uses, emitted after the table lock is released) and returns -1 when any
eligible recipient failed. Zero eligible recipients remains a clean 0,
and try_broadcast_except's existing n < 0 mapping to NetError now
actually fires. Failed streams are deliberately not evicted from the
connection table: removal belongs to hew_tcp_close and the reactor
detach path, and a transient backpressure failure must not change
connection lifecycle.

Regression tests cover all-recipients-fail, partial failure (healthy
peer still receives the message, broadcast still reports -1), the
delivered count with an excluded connection, zero eligible recipients,
and a deterministic clone failure via RLIMIT_NOFILE.

Fixes #2657
broadcast_except and try_broadcast_except now surface the runtime's
fail-closed broadcast contract: the delivered count on full success, 0
when there is nothing to deliver to, and -1 (mapped to NetError by
try_broadcast_except) when any eligible connection's write failed while
healthy connections were still delivered to. No logic change —
try_broadcast_except's errno-to-NetError mapping already handled a
negative result; the runtime just never produced one for per-recipient
failures before.
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.

runtime: report TCP broadcast write failures explicitly

1 participant