Should we check for expired deadlines before performing socket operations?
yorickpeterse opened this issue · comments
Socket operations use the blocking
function to perform non-blocking operations that may raise a EWOULDBLOCK error:
Lines 27 to 76 in 6861608
We support for deadlines as to prevent such operations from hanging forever. In its current form, we don't check for the remaining time until after encountering an EWOULDBLOCK. This means that if a socket operation were to never block, you'd be able to perform the operation indefinitely. For example, if you read()
in a loop such that the read never blocks, and the deadline is set to e.g. 5 seconds in the future, you'd be able to keep running the loop for much longer. This can be illustrated using the following example:
import std.net.ip (IpAddress)
import std.net.socket (UdpSocket)
import std.process (sleep)
import std.stdio (STDOUT)
import std.time (Duration)
class async Main {
fn async main {
let sock = UdpSocket.new(ip: IpAddress.v4(0, 0, 0, 0), port: 9999).or_panic(
'failed to set up the socket',
)
let bytes = ByteArray.new
let out = STDOUT.new
sock
.send_string_to('test', ip: IpAddress.v4(0, 0, 0, 0), port: 9999)
.or_panic('failed to send the message')
sock.socket.timeout_after = Duration.from_secs(0)
sleep(Duration.from_millis(50))
sock.receive_from(bytes, size: 32).or_panic('failed to read the data')
out.write_bytes(bytes)
}
}
According to the deadline set, the receive_from
should time out as the sleep
ensures there's no remaining time. Instead, the call returns the data successfully.
Fixing this requires checking for the expiration before every operation in addition to the check/suspend performed after an EWOULDBLOCK. This means we have to obtain the current monotonic clock time, and compare this against the deadline, increasing the cost of socket operations.
I'm inclined to keep the current behaviour but improve the documentation, meaning deadlines/timeouts only apply when the socket actually needs to wait, as that's generally what timeouts are meant for (as in, to prevent waiting forever for data to arrive).
On the other hand, this could in result in code running for longer than anticipated. I'm not sure yet what the best option is here.
It seems Python doesn't apply timeouts until a socket actually blocks/waits:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 9999))
sock.sendto(b'test', 0, ('0.0.0.0', 9999))
sock.settimeout(0.000001)
print(sock.recvfrom(32)) # works fine, no timeout
print(sock.recvfrom(32)) # times out, as no data is available