Skip to content

Socket: nif_select_write/2 - #2422

Open
petermm wants to merge 19 commits into
atomvm:release-0.7from
petermm:nif_select_write/2
Open

petermm wants to merge 19 commits into
atomvm:release-0.7from
petermm:nif_select_write/2

Conversation

@petermm

@petermm petermm commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

little bit of mess with these PRs - so lets land a single one

fixes a plethora of issues with httpd etc.

this is the (original) work from #2345 (that PR was a git history rewrite of my work) progressed to PR readiness, also relates to #2416

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

Comment thread tests/test-enif.c Fixed
Comment thread tests/test-enif.c Fixed
harmon25 and others added 16 commits November 6, 2026 17:33
Signed-off-by: harmon25 <dougwright1@gmail.com>
Make socket:send/2 retry partial stream sends and wait for write readiness when the native send path reports eagain. This keeps gen_tcp callers on the OTP-like success path instead of leaking {ok, Rest} or transient backpressure.

Add write-select plumbing for BSD and lwIP, including distinct read/write select state and lwIP tcp_sent/tcp_poll wakeups. Use the same readiness wait for SSL WANT_WRITE and make distribution/EPMD send failures explicit instead of dropping data.

Signed-off-by: Peter M <petermm@gmail.com>
Signed-off-by: Peter M <petermm@gmail.com>
Blocking sends require read and write readiness to coexist on one socket.
Sharing their owner lets a writer replace a reader, misroutes close
notifications, and cancels both directions when one selector exits.

Give each direction its own owner, monitor and reference, initialized
for both opened and accepted sockets. Implement ERL_NIF_SELECT_CANCEL
so selector death cancels only its direction, disposes its pending
message, and leaves the other direction armed.

Retain socket resources while lwIP write/error events are queued and
detach callbacks on close. Notify established-socket waiters of terminal
errors through direction-specific abort messages, independently of the
connect trap.

Keep distribution output event-driven so backpressure cannot block
incoming traffic or ticks. Preserve pending frame suffixes and queue
ticks behind them to maintain byte ordering.

Send empty binaries through the native socket path so closed sockets
still fail and connected UDP sockets transmit zero-length datagrams.

Add regressions for accepted-socket close, writer death, both selector
registration orders, exact-data backpressure recovery, cancellation
cleanup, and empty UDP sends. Move the changelog entry to beta.1.

See also: atomvm#2345
Signed-off-by: Peter M <petermm@gmail.com>
A cancelled selection retains its event for reuse. If the operating
system later assigns the same descriptor to another resource, dispatch
can find the inactive registration first and silently lose readiness
for the new socket.

Match only registrations that are open and armed for a reported
direction. Extend the cancellation regression to verify that another
resource using the same event receives readiness and both resources
are released.

Evaluate the test heap reservations outside assert expressions so
assert macro expansion does not introduce duplicate unevaluated calls
into the allocation-budget analysis.

Signed-off-by: Peter M <petermm@gmail.com>
lwIP can free a TCP PCB before task-context error handling runs.
Queuing raw PCBs or treating listeners as connections can therefore
corrupt memory. Taking the VM socket lock from a callback would also
invert the task-context lock order. A full wake queue must not turn an
accepted payload into a failed enqueue and cause its resource reference
to be released twice.

Invalidate reset PCBs in the lwIP synchronization domain and retain
socket resources for deferred work without resurrecting dying resources.
Keep notification and monitor cleanup in task context. Use stable
pending-accept slots to preserve pre-accept EOF and discard reset
children without closing the listener.

Retain critical work independently of the bounded payload queue. Leave
refused TCP buffers with lwIP, make RP2 wakeups durable, and report
payload acceptance independently of global wake hints. Share the RP2
CAS lock across translation units and protect reference updates from
interrupts in single-scheduler builds as well.

Detach callbacks according to the PCB type and guard PCB access with
the lwIP lock. Preserve bytes accepted by tcp_write when tcp_output
fails so callers cannot duplicate them on retry. Release replaced
selector monitors and inactive BSD events on close, and provide
read-only selection cancellation for the public receive wrappers.

Signed-off-by: Peter M <petermm@gmail.com>
Receive and accept error cleanup currently stops both selections on a
socket. A read-side EOF can consequently strand an independent writer
that is waiting for its peer to drain buffered data.

Cancel only the read selection in these cleanup paths. Keep the public
send contract as ok or an error; document that sendto/3 is unchanged.

Exercise the public receive path with a blocked public send in both
selector registration orders. Fill the socket until backpressure is
observed rather than relying on a fixed payload size, then verify that
half-close preserves the writer and draining delivers every queued byte
and the final marker. Keep close and writer-death coverage alongside
this regression.

The half-close case times out with the old socket wrapper and passes
with read-only cleanup. Update the changelog with the user-visible
half-close behavior.

Signed-off-by: Peter M <petermm@gmail.com>
sendto/3 currently exposes transient backpressure instead of waiting
for acceptance like the default OTP call. Retrying a partial result as
a new datagram would also change packet boundaries.

Wait for write readiness and retry the original payload and destination.
Keep empty datagrams on the native path and return partial results
without resending their tails. Back off retries for 10 ms because raw
lwIP reports UDP write readiness immediately even after ERR_MEM.

Reject oversized IPv4 UDP payloads before conversion to lwIP packet
lengths, and preserve BSD sendto errors instead of reporting them as
closed sockets. Extend the UDP tests for empty and iodata packets,
oversized rejection, and sends on closed sockets.

Signed-off-by: Peter M <petermm@gmail.com>
Socket backpressure leaves ticks queued instead of completing their
send. Treating outgoing activity as proof of peer liveness lets a
silent connection reset its timeout indefinitely.

Renew the tick timeout only when the receive counter changes, and
report pending controller bytes so ticks are not queued behind blocked
output. Keep a peer alive when it continues sending traffic, matching
OTP keepalive behavior.

Exercise a real TCP distribution handshake with blocked output,
outgoing-only activity, and inbound ticks arriving at the deadline.
Verify silence disconnects after four ticks and queued ticks stay
bounded.

See also: atomvm#2424

Signed-off-by: Peter M <petermm@gmail.com>
Align TCP writes with available send capacity while preserving empty
sends. Keep select-cancellation setup and mailbox operations running
when assertions are disabled.

Signed-off-by: Peter M <petermm@gmail.com>
Preserve real send errors and bytes accepted before a reset or full
shutdown. Defer closed notifications until queued TCP payloads have
reached task context, and avoid accessing a freed PCB while draining.

Pace TCP allocation retries with sent or poll callbacks. Report UDP
allocation errors directly because immediate readiness cannot signal
memory recovery and the platform timeout pool cannot support retries.

Add directional write cancellation without disturbing an independent
reader, including shared ownership. Validate selection states before
replacing monitors and notify closed selections with their references.

Exercise public native socket operations and real OS errors. Evaluate
test setup, mailbox and cleanup operations outside assertions so they
still execute when NDEBUG is defined.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
Registration can change the event list before marking the cached count
dirty. Always recount while holding the list lock so allocation and
population use the same snapshot.

Publish the rebuilt count before unlocking to avoid overwriting a
concurrent registration dirty marker and losing its wakeup.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
Expose send/3 and sendto/4 with timeout, nowait and caller reference
forms. Use one monotonic deadline across retries and return the unsent
suffix when a stream send fails after making progress.

Cancel only write readiness on timeout so an independent reader keeps
its original selection, even when both directions share an owner.
Preserve datagram boundaries instead of retrying a partial suffix.

Cover timeout recovery, partial results, closed sockets and datagram
boundaries through public APIs. Replace silence-based backpressure
assumptions with explicit readiness, handshakes and byte accounting.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
A blocking send in a socket server prevents receive and close requests
from completing under backpressure. Use nowait sends and resume queued
output on write readiness while preserving FIFO order.

Release pending send callers on close, abort or controlling-process
death, monitor passive owners, and return closed when send races a
normally stopped server. Receive cleanup cancels only read selection.

Test receive and close responsiveness during an in-progress TCP send,
ordered draining, and UDP error recovery with intact packet boundaries.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
A node can disconnect before its ALIVE2 response is sent. Close that
registration socket on send failure without crashing epmd, changing
existing registrations or consuming a creation value.

Use the public registration protocol to verify failure recovery. The
deterministic response-write shutdown uses OTP socket registry and
monitor APIs, so that fault injection runs only on BEAM.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
Explain timeout and asynchronous send results, unsent stream suffixes,
directional cancellation and preserved datagram boundaries. Document
raw-lwIP UDP allocation errors rather than promising automatic retry.

Record the send APIs, responsive socket servers, native lifetime and
poll fixes, and epmd recovery under the upcoming release.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
@petermm
petermm force-pushed the nif_select_write/2 branch from de4ce2a to 8c0b058 Compare November 6, 2026 17:46
Live socket owners cancel selections from resource-down callbacks during
context destruction. Keep platform services alive for those callbacks,
then stop platform event producers before dismantling shared event lists.

Mark directly destroyed contexts killed under the scheduler-list lock so
late platform notifications cannot requeue a dying context.

Add a live read/write owner shutdown regression and changelog entry.
The regression reports eight Valgrind invalid-access contexts before the
ordering fix and none after it.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
Completed socket operations retained their timer workers until expiry.
Expiry bursts overloaded the timer manager during backpressured receives
under Valgrind, causing new timer registrations to time out.

Cancel accept and receive timers on completion, abort and termination.
Allocate timers only after successful selection, and leave infinite
receives without the old artificial 30-second timeout.

Add a regression for timer cleanup and receive recovery after timeout,
and record the fix in the changelog.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
Module teardown freed the import pointer array but leaked its owned
unresolved and resolved function descriptors. Free those allocations,
including native-code descriptors, without freeing borrowed BIF or NIF
registry entries.

Keep the import count in the module so cleanup does not read BEAM data
that another module may already have freed. Record the count before
allocating descriptors to support partially initialized tables.

Strict Valgrind checks cover genTCP, native-code imports, allocation
failures and repeated code:load_binary teardown with compressed literals.

Signed-off-by: Peter Madsen-Mygdal <petermm@gmail.com>
@petermm
petermm force-pushed the nif_select_write/2 branch from 2dd4ae0 to 0f8bb30 Compare November 7, 2026 07:50
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.

3 participants