Saturday 3 March 2012

pyuv experimentation

The appeal of libuv, and therefore it's Python binding pyuv, is that it provides asynchronous access to a wide range of functionality.  I don't think there is any documentation for libuv itself, if you want to understand the nuances of how to use it, you probably need to jump back and forwards between C source files interpreting the use which has been made of various macros.  The pyuv documentation however, provides a decent overview of the functionality available and how it can be used for Python.

Unfortunately, there is still a degree of libuv source code reading involved, working out what happens in various situations. In this situation, I was curious what happened when the server disconnected a client that was trying to read from its accepted connection.

Server Code

The code starts by locating or creating a loop, which is responsible for managing the events and callback dispatching related to the IO that is performed with respect to that loop. Next a timer is created, this in theory every 20th of a second calls a function. In reality, it is a hack to work around the fact that you can't directly specify a maximum timeout to the run_once method. Instead, the timeout enables the run_once to exit rather than blocking indefinitely.

Next, a TCP connection is created. It is set to be allowed to reuse the same port without waiting, bound to a listening address and then the loop is run until the callback indicating the listen operation occurs.
import pyuv
loop = pyuv.Loop.default_loop()
timer = pyuv.Timer(loop)
timer.start(lambda *args: None, 0.0, 0.05)
listen_socket = pyuv.TCP(loop)
listen_socket.nodelay(True)
listen_socket.bind(("0.0.0.0", 3000))
had_listen_callback = False
def listen_callback(*args):
    global had_listen_callback
    print "listen_callback", args
    had_listen_callback = True

listen_socket.listen(listen_callback, 5)
while not had_listen_callback:
    timer.again()
    loop.run_once()
The listen callback is called everytime there is an incoming connection so that it can be accepted and handled however. The code now proceeds to accept the connection, write token data to it, and then to wait until the data has been sent.
incoming_socket = pyuv.TCP(loop)
listen_socket.accept(incoming_socket)
had_write_callback = False
def write_callback(*args):
    global had_write_callback
    print "write_callback", args
    had_write_callback = True

incoming_socket.write("DATA", write_callback)

while not had_write_callback:
    timer.again()
    loop.run_once()
And finally, once the data has been sent, the socket is closed.
had_close_callback = False
def close_callback(*args):
    global had_close_callback
    had_close_callback = True

incoming_socket.close(close_callback)
while not had_close_callback:
    timer.again()
    loop.run_once()
Client Code

The client code has the same boilerplate to begin with, but then tries to connect to the listening address, and waits for the connection to be established.
import pyuv
loop = pyuv.Loop.default_loop()
timer = pyuv.Timer(loop)
timer.start(lambda *args: None, 0.0, 0.05)
client_socket = pyuv.TCP(loop)
had_connect_callback = False
def connect_callback(*args):
    global had_connect_callback
    print "connect_callback", args
    had_connect_callback = True

client_socket.connect(("127.0.0.1", 3000), connect_callback)
while not had_connect_callback:
    timer.again()
    loop.run_once()
Next the client starts reading, and prints out the arguments everytime it receives data in it's callback.
had_read_callback = False
def read_callback(*args):
    global had_read_callback
    print "read_callback", args
    had_read_callback = True

client_socket.start_read(read_callback)
while True:
    timer.again()
    loop.run_once()
Results

The answer to the question of what happens when the client is disconnected mid-read, is that it receives a data value of None and an error value of UV_EOF. Pretty simple actually.

My main reason for playing with pyuv, is to hopefully replace the monkey-patching framework that is stacklesslib, with one that can cover a wider variety of thread blocking functionality. The existing Stackless-compatible socket module is based on asyncore and by extension it wraps the existing Python networking functionality in an asynchronous manner, and it can handle the more complicated functionality like makefile and ioctl because of this.

Unfortunately, libuv and by extension pyuv, abstracts away or does not provide access to much of the standard API. This means a lot more work if those aspects are to be emulated. Sure, it might be an easier approach to simply not implement them, but that loses a lot of the benefit of writing a monkey-patched module. If the replacement socket module provides makefile, then it is compatible with a wider range of other standard library modules.

I've written the most straightforward part of a socket module for Stackless based on pyuv, and it can be seen within a Google hosted project here.