Skip to content

Async only exhausts handle when callback isn't removed #19764

Open
@PMunch

Description

@PMunch

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    AsyncEverything related to Nim's asyncDocumentationRelated to documentation content (not generation).

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions