Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .release-notes/5347.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Fix delivery of zero-byte UDP datagrams

UDP sockets now deliver zero-byte datagrams to `UDPNotify.received` with an
empty payload instead of treating them as an error. This preserves valid UDP
uses such as heartbeat, keepalive, and presence datagrams without changing TCP
zero-byte read handling.
75 changes: 75 additions & 0 deletions packages/net/_test.pony
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ actor \nodoc\ Main is TestList
test(_TestTCPProxy)
test(_TestTCPUnmute)
test(_TestTCPWritev)
test(_TestUDPZeroByteDatagram)

// Tests below exclude windows and are listed alphabetically
ifdef not windows then
Expand Down Expand Up @@ -155,6 +156,80 @@ class \nodoc\ iso _TestBroadcast is UnitTest
exclude this test by passing the --exclude="net/Broadcast" option.
""")

class \nodoc\ _TestUDPZeroByteDatagramReceiver is UDPNotify
let _h: TestHelper

new create(h: TestHelper) =>
_h = h

fun ref not_listening(sock: UDPSocket ref) =>
_h.fail_action("receiver listen")

fun ref listening(sock: UDPSocket ref) =>
_h.complete_action("receiver listen")

let ip = sock.local_address()
let h = _h
if ip.ip4() then
_h.dispose_when_done(
UDPSocket.ip4(UDPAuth(h.env.root),
recover _TestUDPZeroByteDatagramSender(h, ip) end))
else
_h.dispose_when_done(
UDPSocket.ip6(UDPAuth(h.env.root),
recover _TestUDPZeroByteDatagramSender(h, ip) end))
end

fun ref received(
sock: UDPSocket ref,
data: Array[U8] iso,
from: NetAddress)
=>
_h.complete_action("receiver receive")
_h.assert_eq[USize](0, data.size())
_h.complete(true)

class \nodoc\ _TestUDPZeroByteDatagramSender is UDPNotify
let _h: TestHelper
let _ip: NetAddress

new create(h: TestHelper, ip: NetAddress) =>
_h = h
_ip = ip

fun ref not_listening(sock: UDPSocket ref) =>
_h.fail_action("sender listen")

fun ref listening(sock: UDPSocket ref) =>
_h.complete_action("sender listen")
sock.write("", _ip)

fun ref received(
sock: UDPSocket ref,
data: Array[U8] iso,
from: NetAddress)
=>
_h.fail("sender received unexpected datagram")

class \nodoc\ iso _TestUDPZeroByteDatagram is UnitTest
"""
Test receiving an empty UDP datagram.
"""
fun name(): String => "net/UDPZeroByteDatagram"
fun label(): String => "unreliable-appveyor-osx"
fun exclusion_group(): String => "network"

fun ref apply(h: TestHelper) =>
h.expect_action("receiver listen")
h.expect_action("sender listen")
h.expect_action("receiver receive")

h.dispose_when_done(
UDPSocket(UDPAuth(h.env.root),
recover _TestUDPZeroByteDatagramReceiver(h) end))

h.long_test(TimeoutValue())

class \nodoc\ _TestTCP is TCPListenNotify
"""
Run a typical TCP test consisting of a single TCPListener that accepts a
Expand Down
15 changes: 10 additions & 5 deletions packages/net/udp_socket.pony
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,11 @@ actor UDPSocket is AsioEventNotify

fun ref _pending_reads() =>
"""
Read while data is available, guessing the next packet length as we go. If
we read 4 kb of data, send ourself a resume message and stop reading, to
avoid starving other actors.
Read while data is available, guessing the next packet length as we go.
After roughly 4 KB worth of read work, send ourself a resume message and
stop reading, to avoid starving other actors. Zero-byte datagrams count as
1 byte against this budget so a stream of empty datagrams cannot keep this
loop running forever.
"""
ifdef not windows then
try
Expand All @@ -306,15 +308,18 @@ actor UDPSocket is AsioEventNotify
@pony_os_recvfrom(_event, data.cpointer(), data.space(),
from) ?

if len == 0 then
if len == USize.max_value() then
_readable = false
return
end

data.truncate(len)
_notify.received(this, consume data, consume from)

sum = sum + len
// .max(1) charges 1 byte against the budget for a zero-byte
// datagram, preventing this loop from starving other actors on a
// stream of empty datagrams.
sum = sum + len.max(1)

if sum > (1 << 12) then
_read_again()
Expand Down
4 changes: 1 addition & 3 deletions src/libponyrt/lang/socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -1119,10 +1119,8 @@ PONY_API size_t pony_os_recvfrom(asio_event_t* ev, char* buf, size_t len,
if(recvd < 0)
{
if(errno == EWOULDBLOCK || errno == EAGAIN)
return 0;
return (size_t)-1;

pony_error();
} else if(recvd == 0) {
pony_error();
}

Expand Down