Asyncio Support

Behind the scenes, XUDD makes use of asyncio to do message passing. But XUDD has a nice interoperability layer where your actors can interface nicely with the rest of the asyncio ecosystem.

Like message passing with XUDD, asyncio makes heavy use of yield and yield from. However, the astute reader may notice that the way this is called is pretty different than XUDD’s message passing... this is because how yield from would work in XUDD was designed far before asyncio integration.

Nonetheless, the differences are not so big, and thanks to asyncio and XUDD’s clever interoperability layer, you can make use of a tremendous amount of asyncio features such as asynchronous network and filesystem communication, timer systems, and much more.

Asyncio by example

A simple IRC bot

For a good example of this, let’s look at this simple IRC bot (no need to follow it all, we’ll break it down):

"""
"""
from __future__ import print_function

import asyncio
import logging
import sys

from xudd.actor import Actor
from xudd.hive import Hive

_log = logging.getLogger(__name__)


IRC_EOL = b'\r\n'


class IrcBot(Actor):
    def __init__(self, hive, id,
                 nick, user=None,
                 realname="XUDD Bot 2",
                 connect_hostname="irc.freenode.net",
                 connect_port=6667):
        super().__init__(hive, id)

        self.realname = realname
        self.nick = nick
        self.user = user or nick

        self.connect_hostname = connect_hostname
        self.connect_port = connect_port

        self.authenticated = False
        self.reader = None
        self.writer = None

        self.message_routing.update(
            {"connect_and_run": self.connect_and_run})

    def connect_and_run(self, message):
        self.reader, self.writer = yield from asyncio.open_connection(
            message.body.get("hostname", self.connect_hostname),
            message.body.get("port", self.connect_port))

        self.login()
        while True:
            line = yield from self.reader.readline()
            line = line.decode("utf-8")
            self.handle_line(line)

    def login(self):
        _log.info('Logging in')
        lines = [
            'USER {user} {hostname} {servername} :{realname}'.format(
                        user=self.user,
                        hostname='*',
                        servername='*',
                        realname=self.realname
            ),
            'NICK {nick}'.format(nick=self.nick)]
        self.send_lines(lines)

    def send_lines(self, lines):
        for line in lines:
            line = line.encode("utf-8") + IRC_EOL
            self.writer.write(line)

    def handle_line(self, line):
        _log.debug(line.strip())


def main():
    logging.basicConfig(level=logging.DEBUG)

    # Fails stupidly if no username given
    try:
        username = sys.argv[1]
    except IndexError:
        raise IndexError("You gotta provide a username as first arg, yo")

    hive = Hive()
    irc_bot = hive.create_actor(IrcBot, username)

    hive.send_message(
        to=irc_bot,
        directive="connect_and_run")

    hive.run()


if __name__ == "__main__":
    main()

This bot, as written above, doesn’t do much... it just logs in and spits out all messages it receives to the log as debugging info.

Nonetheless, that might look daunting. From the main() method though, it’s obvious that the first thing done is to handle a connect_and_run method on the IRC bot (the handler of which just so happens to be connect_and_run(). So let’s look at that method in detail:

def connect_and_run(self, message):
    self.reader, self.writer = yield from asyncio.open_connection(
        message.body.get("hostname", self.connect_hostname),
        message.body.get("port", self.connect_port))

    self.login()
    while True:
        line = yield from self.reader.readline()
        line = line.decode("utf-8")
        self.handle_line(line)

This little snippet of code does almost the entirety of the busywork in this IRC bot. You can see two uses of yield from interfacing with asyncio here.

The first line sets up a simple socket connection. You can see that this uses “yield from” to be come back with the transport and protocol (reader and writer) objects once the connection is available. This is a standard asyncio method! (As you’ll notice, there’s nothing wrapped in a message in this case, because we’re not doing message passing between actors here.)

The next line calls self.login()... if we follow this method, we’ll notice this method itself calls self.send_lines(). This method interfaces with asyncio via self.writer.write(line), but since it does not wait on anything, it can call the writer without anything special happening.

Finally, the connect_and_run() enters a loop that runs forever... it waits for new data to come in and handles it. (In this case, “handling” it means simply logging the line... but we might do something more complex later!)

As you can see, the user of XUDD mostly can just call asyncio coroutines from a message handler and things should work.

(Note: if you need to call an asyncio coroutine from a subroutine of your message handler, this can be trickier... you will have to make sure that your subroutine is itself a coroutine and yield from that too! TODO: show an example.)