async-std icon indicating copy to clipboard operation
async-std copied to clipboard

Allow `UdpSocket` to perform "fire and forget" sends

Open codyps opened this issue 5 years ago • 1 comments

I'm writing a udp server using async-std. One thing I'm trying to ensure is that I avoid queuing any outbound udp packets within my application.

The basic pattern of the application is:

  1. single await recv loop, which then calls
  2. packet handling, which then sends a udp packet (a reply)

Typical sources of in-application queuing in rust async programs appear to be both explicit queues (if one has specific tasks handle sending for some reason) or the implicit queue formed when one uses task::spawn() to send a udp packet.

In the basic pattern above, it's not desirable to await in 2 because this would block the recv of all clients while waiting for the outbound packet to send (ie: we'd end up capping our rx through put). Right now, this appears to leave us with either: task::spawn() for each recv'd packet (probably inefficient and forming an implicit queue there) or task::spawn() when we want to send a packet in 2 (probably more efficient, but is creating a queue of outbound packets).

When approaching this problem in C, I'd normally have the sends essentially be "fire and forget": iow, make the udp socket fd non-blocking, but never register it for write readiness. Instead, just call send(etc) and ignore errors that indicate lack of write readiness (allowing the packet to be dropped, preventing queue formation within the application).

It seems like the most straight forward way to allow this pattern in async-std would be to allow obtaining a reference to the std::net::UdpSocket within async_std::net::UdpSocket, and then performing sends via calls on the inner structure. There are some downsides here: it would depend on having the socket in a non-blocking mode, and generally be somewhat sensitive to the underlying state of the socket. Alternately, async_std could expose a method on it's UdpSocket to allow this sendto_nonblocking() functionality.

Edit: for those looking for a workaround: creating a std::net::UdpSocket, then cloning it, and constructing the async_std::net::UdpSocket from one of the clones, keeping the other around to use for sending udp packets works. This does consume an extra file descriptor per UdpSocket that you want to bind to (though that may not be very many).

codyps avatar Sep 23 '20 21:09 codyps

@jmesmon I spent the last hour trying to reuse an async UdpSocket in two different tasks without rust complaining about borrowed values not living long enough. I am relieved that you shared your workaround, thank you.

let sync_socket = std::net::UdpSocket::bind(LOCAL_ADDRESS)?;
let sync_socket2 = sync_socket.try_clone()?;
let socket = async_std::netUdpSocket::from(sync_socket);
task::spawn(do_something(UdpSocket::from(sync_socket2.try_clone()?)));

QasimK avatar Sep 08 '21 22:09 QasimK