inko-lang / inko

A language for building concurrent software with confidence

Home Page:http://inko-lang.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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:

fn blocking<T>(
state: &State,
mut process: ProcessPointer,
socket: &mut Socket,
interest: Interest,
deadline: i64,
mut func: impl FnMut(&mut Socket) -> io::Result<T>,
) -> io::Result<T> {
match func(socket) {
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
val => return val,
}
let poll_id = unsafe { process.thread() }.network_poller;
// We must keep the process' state lock open until everything is registered,
// otherwise a timeout thread may reschedule the process (i.e. the timeout
// is very short) before we finish registering the socket with a poller.
{
let mut proc_state = process.state();
// A deadline of -1 signals that we should wait indefinitely.
if deadline >= 0 {
let time = Timeout::until(deadline as u64);
proc_state.waiting_for_io(Some(time.clone()));
state.timeout_worker.suspend(process, time);
} else {
proc_state.waiting_for_io(None);
}
socket.register(state, process, poll_id, interest)?;
}
// Safety: the current thread is holding on to the process' run lock, so if
// the process gets rescheduled onto a different thread, said thread won't
// be able to use it until we finish this context switch.
unsafe { context::switch(process) };
if process.timeout_expired() {
// The socket is still registered at this point, so we have to
// deregister first. If we don't and suspend for another IO operation,
// the poller could end up rescheduling the process multiple times (as
// there are multiple events still in flight for the process).
socket.deregister(state);
return Err(io::Error::from(io::ErrorKind::TimedOut));
}
func(socket)
}

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