Feel free to skip this section. All I do here is explain why I even worked on this problem.
One of the bigger challenges I have faced while writing Pia.jl is that Julia’s standard library does not provide timeouts for I/O operations whatsoever, particularly including TCP & UDP Sockets. This is likely a direct consequence of the usage of the underlying library: libuv — which in itself does not seem to provide such timeouts. However, it is Julia’s integration which poses a serious problem.
After some experimentation, I discovered that Julia does not immediately read all available data on a TCP socket. Only once you manually trigger a read on said socket is all available data read into an internal buffer. On top of that, there is no interface to merely peek for available bytes without consuming, nor can we poll. We have to read.
Being forced to read means being forced to block. Asynchronicity can be achieved by two main tools these days, benefits & drawbacks depending on your specific use case: Event Loops or Threads. Event loops are mainly designed to linearize otherwise convoluted deferred-callback-based code, simplifying it into a single function rather than dozens. Whereas threads allow true parallelism with each concurrently running thread able to conduct complex computations simultaneously.
Seeing our use case, employing Tasks is the wiser choice. Moreover, Julia does not allow killing tasks. Generally, it is ill-advised to kill threads, anyways, as this can leave the killed thread improperly cleaned up and leak resources.
In a real world application, blindly blocking for data until an untrusted client sends more is fatally naïve. It would be easy to connect once, never send data, even disconnect again while your Julia program is still waiting. Even if it doesn’t immediately halt your application, the eternally sleeping task is an unusable system resource. Such an attack is an insanely simple and efficient exploit. Do this enough and your Julia program will eventually fully exhaust your system’s resources, resulting in a DOS (Denial of Service).
Timeouts, obviously. It’s very common practice and even part of the TCP specification in the form of keepalive packets. But timeouts are not directly implemented in Julia’s tasks.
Our only solution is rather lower level:
Base.schedule. In concept, we reawaken the task prematurely, forcing the blocking task to reevaluate its blocking condition. However,
schedule can also pass a value into the task. In itself, passing a value into the sleeping task has comparatively limited applications. But a keyword argument
error allows us to, instead, throw this value in the waiting task. We shall leverage this!
Your solution, finally, shall look similar to this:
payload_task = @async begin
endtimeout_task = @async begin
schedule(payload_task, "cancel", error=true)
After 5 seconds,
"cancel" is thrown in
payload_task during its call to
sleep. This is propagated up to
wait(payload_task) in the form of a
TaskFailedException. If the task were to succeed, we could
fetch(payload_task) in order to retrieve its return value.
To keep things completely clean and overly perfectionistic, you could also
schedule(timeout_task, "cancel", error=true) at the end of
payload_task, freeing up its resources and preventing it from attempting to schedule a task which has already finished. The latter even throws an error, but as we never
wait(timeout_task) this error is not bubbled to our application and does not interrupt it. Still, if you’re like me, you like things clean.
Of course, this is not a particularly beautiful solution, and the entire second half is boilerplate. We could wrap this in a convenient function which takes care of everything for us — and I have! My library ExtraFun.jl exports two functions
with_timeout which you can use as such:
using ExtraFuntask = with_cancel() do
println("I should never be reached")
endtask = with_timeout(5) do
println("I should timeout after 5s")
cancel is another function exported by my library. It is not part of
This neatly wraps up your asynchronous code in just a few lines. Unfortunately, though, this is incompatible with Julia’s
@sync macro, meaning you will have to manually
wait for every such task. Even if it were, you’d still need the
Task handles these functions return to either
fetch their results.