Description
I was trying to wrap a C library in Nims async capabilities. It all seemed to work fine, but when I tried to run more than one action at the same time it locked up. After some investigation I've concluded that it is caused by some subtle undocumented behaviour, potentially just a straight up bug. Consider the following example (only works on Linux, or other systems with async stdin):
import asyncdispatch
addTimer(5000, false, proc (fd: AsyncFD): bool =
echo "ping"
false
)
stdin.getFileHandle.AsyncFD.register
addRead(stdin.getFileHandle.AsyncFD, proc (fd: AsyncFD): bool =
echo stdin.readLine
true
)
addRead(stdin.getFileHandle.AsyncFD, proc (fd: AsyncFD): bool =
echo stdin.readLine
true
)
runForever()
When running the program this will happily print "ping" every five seconds. If you write something to the terminal in echos it back out, but then it stops sending the pings. What is happening here is that the second callback is blocking on trying to read stdin. The execution looks something like this:
timerCallback()
"ping"
timerCallback()
"ping"
# user writes "hello" and hits enter
readCallback1()
"hello"
readCallback2()
# This is now blocking on reading stdin
As we can see the second callback is also run when the standard input is ready for reading, even though the first handler exhausted the handle.
The related code is found in asyncdispatch:1265:1276 and reads (at the time of writing):
# curList is the list of callbacks, newList is the new list of callbacks after this run
var eventsExtinguished = false
for cb in curList:
if eventsExtinguished:
newList.add(cb)
elif not cb(fd):
# Callback wants to be called again.
newList.add(cb)
# This callback has returned with EAGAIN, so we don't need to
# call any other callbacks as they are all waiting for the same event
# on the same fd.
# We do need to ensure they are called again though.
eventsExtinguished = true
If the callback handler returns false
, meaning that it should not be run again, the rest of the callbacks are simply added to the new queue and will not be run until further reads are possible. However if the handler returns true
as in the example above, meaning that the handler should not be removed it will continue processing the other callbacks. The relevant documentation for addRead
is:
Be sure your callback cb returns true, if you want to remove watch of read notifications, and false, if you want to continue receiving notifications.
No mention of this exhaustion-handling at all. This should either be documented, or it should be fixed so that only one of the callbacks are ever called for a single event. At least on POSIX the selector should just immediately fire again if there is still data to be read.