[...] applications will usually return an iterator (often
a generator-iterator) that produces the output in a block-by-block
fashion. These blocks may be broken to coincide with mulitpart
boundaries (for “server push”), or just before time-consuming
tasks (such as reading another block of an on-disk file). [...]
It means that all WSGI conforming servers should be able to send multipart http responses. WSGI clock application theoretically could be written like that:
def clock_demo(environ, start_response): start_response("200 OK", [('Content-type','text/plain')]) for i in range(100): yield "%s\n" % (datetime.datetime.now(),) time.sleep(1)
The problem is that way of programming just doesn’t work well. It’s not scalable, requires a lot of threads and can eat a lot of resources. That’s why the feature has been forgotten.
Until May 2008, when Christopher Stawarz reminded us this feature and proposed an enhancement to it. He suggested, that instead of blocking, like time.sleep(1), inside the code WSGI application should return a file descriptor to server. When an event happens on this descriptor, the WSGI app will be continued. Here’s equivalent of the previous code, but using the extension. With appropriate server this could be scalable and work as expected:
def clock_demo(environ, start_response): start_response("200 OK", [('Content-type','text/plain')]) sd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: for i in range(100): yield environ['x-wsgiorg.fdevent.readable'](sd, 1.0) yield "%s\n" % (datetime.datetime.now(),) except GeneratorExit: pass sd.close()
So I created a server that supports it: EvServer the Asynchronous Python WSGI Server
I did my best to implement the latest of the three versions of Chris proposal. The code is based on my hacked together implementation of a very similar project django-evserver, which was created way before the extension was invented and before I knew about the WSGI multipart feature.
EvServer is very small and lightweight , the core is about 1000 lines of Python code. Apparently, due to the fact that EvServer is using ctypes bindings to libevent, it’s quite fast.
I did a basic test to see how fast it is. The methodology is very dumb, I just measure the number of handled WSGI requests per second, so as a result I receive only the server speed. The difference is clearly visible:
|spawning with threads||1237|
|spawning without threads||2200|
|cherrypy wsgi server||1700|
So what really EvServer is?
What EvServer is not?
Admittedly using raw WSGI for regular web applications is a bit inconvenient. Fortunately decent web frameworks support passing iterators from the web application down to the WSGI server, throughout all the framework. On my list of frameworks that support iterators you can find: Django and Web.py.
Django 1.0 supports returning iterators from views. This is Django code for the clock example:
def django_clock(request): def iterator(): sd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: while True: yield request.environ['x-wsgiorg.fdevent.readable'](sd, 1.0) yield '%s\n' % (datetime.datetime.now(),) except GeneratorExit: pass sd.close() return HttpResponse(iterator(), mimetype="text/plain")
The problem is that this code is not going to work using the standard ./manage runserver development server. Fortunately, it’s very easy to integrate EvServer with Django, you only need to put that into settings.py:
INSTALLED_APPS = ( [...] 'django.contrib.sites', 'evserver', # <<< THIS LINE enables runevserver command)
Now you can test your app using ./manage runevserver.
Full source code for the example django application is in the EvServer examples directory.
From the 0.3 version Web.py supports returning iterators. You can see it in action here:
class webpy_clock: def GET(self, name): web.header('Content-Type','text/plain', unique=True) environ = web.ctx.environ def iterable(): sd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # any udp socket try: while True: yield environ['x-wsgiorg.fdevent.readable'](sd, 1.0) yield "%s\n" % (datetime.datetime.now(),) except GeneratorExit: pass sd.close() return iterable()
The full source code is included in EvServer example directory . You can run this code using command:
evserver --exec "import examples.framework_webpy; application = examples.framework_webpy.application"
I haven’t discussed any useful scenario yet, I’ll try to do that in the future post. I’m thinking of some interesting uses for EvServer – pushing the data to the browser using COMET.