By Zed A. Shaw

Mongrel2 Python Library Working

I'm proud to announce a first cut at an actual Python library for Mongrel2 that is turning out to be quite sexy. It's most likely too rudimentary right now for serious work, but it is simple enough to get your head around and use, and is full featured. You can actually implement full HTTP or JSON handlers with it.

After fixing up a bunch of bugs today and getting the HTTP handler to properly deal with request bodies, I sat down and implemented a first cut at a Python library for writing handlers. It turns out that since ZeroMQ handles a bunch of the protocol crap for me, all I have to do is parse the format and I'm done. I finalized the format last night, so tonight is just implementing some simple code.

You can install it by grabbing the Mongrel2 source and then doing:

$ cd examples/python && sudo python setup.py install

That's it and then there's more stuff in the examples directory to play with if you want to explore more.

What It Looks Like

Take a look at your most basic HTTP handler:

from mongrel2 import handler
import json

sender_id = "82209006-86FF-4982-B5EA-D1E29E55D481"

conn = handler.Connection(sender_id, "tcp://127.0.0.1:9997",
                          "tcp://127.0.0.1:9996")
while True:
    print "WAITING FOR REQUEST"

    req = conn.recv()

    response = "<pre>\nSENDER: %r\nIDENT:%r\nPATH: %r\nHEADERS:%r\nBODY:%r</pre>" % (
        req.sender, req.conn_id, req.path, 
        json.dumps(req.headers), req.body)

    print response

    conn.reply_http(req, response)

Yep, that's it, and best of all, you can fire up as many of these as you want and they'll handle the requests all round robin with no problems. That's with no config changes in the running Mongrel2 server. I had to change up the type of 0MQ socket used, but otherwise worked just fine. You can see this run at the example I've got running.

The chat demo, while still being a little gross, is also much cleaner and uses the same library and format:

import simplejson as json
from mongrel2 import handler

sender_id = "82209006-86FF-4982-B5EA-D1E29E55D481"

conn = handler.Connection(sender_id, "tcp://127.0.0.1:9999",
                          "tcp://127.0.0.1:9998")
users = {}
user_list = []


while True:
    req = conn.recv_json()
    data = req.data

    if data["type"] == "join":
        conn.deliver_json(users.keys(), data)
        users[req.conn_id] = data['user']
        user_list = [u[1] for u in users.items()]
        conn.reply_json(req, {'type': 'userList', 'users': user_list})

    elif data["type"] == "disconnect":
        print "DISCONNECTED", req.conn_id

        if req.conn_id in users:
            data['user'] = users[req.conn_id]
            del users[req.conn_id]

        conn.deliver_json(users.keys(), data)
        user_list = [u[1] for u in users.items()]

    elif req.conn_id not in users:
        users[req.conn_id] = data['user']

    elif data['type'] == "msg":
        conn.deliver_json(users.keys(), data)

    print "REGISTERED USERS:", len(users)

Again, the only difference is that I'm using deliver to send one message to many connected sockets. Incidentally, this works for HTTP as well, so you can have a handler do a single message that targets many waiting connections. That's the basis of something like a keep-alive hack style HTTP long poll if you wanted to use it.

How It's Implemented

The simplicity of the design really shows when you look at the mongrel2.handler.Request class:

import json

def parse_netstring(ns):
    len, rest = ns.split(':', 1)
    len = int(len)
    assert rest[len] == ',', "Netstring did not end in ','"
    return rest[:len], rest[len+1:]

class Request(object):

    def __init__(self, sender, conn_id, path, headers, body):
        self.sender = sender
        self.path = path
        self.conn_id = conn_id
        self.headers = headers
        self.body = body

    @staticmethod
    def parse(msg):
        sender, conn_id, path, rest = msg.split(' ', 3)
        headers, rest = parse_netstring(rest)
        body, _ = parse_netstring(rest)

        headers = json.loads(headers)

        return Request(sender, conn_id, path, headers, body)

That is all the code needed to properly parse the format for Mongrel2 HTTP messages and it works whether the message comes from an HTTP socket or a JSSocket. It even will let you post JSON off HTTP and still handle the request just fine if you like.

The Connection class is just a little more complex, but not by much:

import zmq
import time
from mongrel2.request import Request
import json

CTX = zmq.Context()

HTTP_FORMAT = "HTTP/1.1 %(code)s %(status)s\r\n%(headers)s\r\n\r\n%(body)s"
MAX_IDENTS = 100

def http_response(body, code, status, headers):
    payload = {'code': code, 'status': status, 'body': body}
    headers['Content-Length'] = len(body)
    payload['headers'] = "\r\n".join('%s: %s' % (k,v) for k,v in
                                     headers.items())
    return HTTP_FORMAT % payload


class Connection(object):
    def __init__(self, sender_id, sub_addr, pub_addr):
        self.sender_id = sender_id

        reqs = CTX.socket(zmq.UPSTREAM)
        reqs.connect(sub_addr)

        resp = CTX.socket(zmq.PUB)
        resp.connect(pub_addr)
        resp.setsockopt(zmq.IDENTITY, sender_id)

        self.sub_addr = sub_addr
        self.pub_addr = pub_addr
        self.reqs = reqs
        self.resp = resp

    def recv(self):
        return Request.parse(self.reqs.recv())

    def recv_json(self):
        req = self.recv()
        req.data = json.loads(req.body)
        return req

    def send(self, conn_id, msg):
        self.resp.send(conn_id + ' ' + msg)

    def reply(self, req, msg):
        self.send(req.conn_id, msg)

    def reply_json(self, req, data):
        self.send(req.conn_id, json.dumps(data))

    def reply_http(self, req, body, code=200, status="OK", headers=None):
        self.reply(req, http_response(body, code, status, headers or {}))

    def deliver(self, idents, data):
        self.resp.send(' '.join(idents) + ' ' + data)

    def deliver_json(self, idents, data):
        self.deliver(idents, json.dumps(data))

    def deliver_http(self, idents, body, code=200, status="OK", headers=None):
        self.deliver(idents, http_response(body, code, status, headers or {}))

I cut out many of the documentation comments so it would fit in the blog, but this is all the code for handling requests and sending replies or doing deliveries.

I consider this a very good indicator that the Mongrel2 design is going well. The fact that I can crank out a tiny little bit of code like this and get a full backend handler system going that can handle async messages from arbitrary browser sockets is amazing.

Next Steps

The next thing is to make this more generally useful, probably with a WSGI handler or similar. Although WSGI will miss out on the deliver concept, it will make other web frameworks possible. I'll also want to get other languages working in this similar way so probably will do Ruby, C, and maybe C++ next. Not sure, but if someone is interested, feel free to just talk with about some protocol changes that might happen.

I've also got to get a handle on timeouts and larger request bodies. Right now if a handler dies and doesn't respond then the browser just sits there waiting. I'd rather have it do a useful kind of timeout and clean itself up.

And of course, implementing all the other things a growing web server needs. Like serving files correctly and better error responses.

And not crashing. That's kind of important too.

If you're excited about this, hop on the mongrel2@librelist.com mailing list and let us know what you think. Patches are always welcome.