The asyncio loop engine (CPython >= 3.4, uWSGI >= 2.0.4)

Warning

Status: EXPERIMENTAL, lot of implications, especially in respect to the WSGI standard

The asyncio plugin exposes a loop engine built on top of the asyncio CPython API (https://docs.python.org/3.4/library/asyncio.html#module-asyncio).

As uWSGI is not callback based, you need a suspend engine (currently only the ‘greenlet’ one is supported) to manage the WSGI callable.

Why not map the WSGI callable to a coroutine?

The reason is pretty simple: this would break WSGI in every possible way. (Let’s not go into the details here.)

For this reason each uWSGI core is mapped to a greenlet (running the WSGI callable).

This greenlet registers events and coroutines in the asyncio event loop.

Callback vs. coroutines

When starting to playing with asyncio you may get confused between callbacks and coroutines.

Callbacks are executed when a specific event raises (for example when a file descriptor is ready for read). They are basically standard functions executedin the main greenlet (and eventually they can switch back control to a specific uWSGI core).

Coroutines are more complex: they are pretty close to a greenlet, but internally they work on Python frames instead of C stacks. From a Python programmer point of view, coroutines are very special generators. Your WSGI callable can spawn coroutines.

Building uWSGI with asyncio support

An ‘asyncio’ build profile is available in the official source tree (it will build greenlet support too).

  1. CFLAGS="-I/usr/local/include/python3.4" make PYTHON=python3.4 asyncio

or

  1. CFLAGS="-I/usr/local/include/python3.4" UWSGI_PROFILE="asyncio" pip3 install uwsgi

be sure to use Python 3.4+ as the Python version and to add the greenlet include directory to CFLAGS (this may not be needed if you installed greenlet support from your distribution’s packages).

The first example: a simple callback

Let’s start with a simple WSGI callable triggering a function 2 seconds after the callable has returned (magic!).

  1. import asyncio
  2.  
  3. def two_seconds_elapsed():
  4. print("Hello 2 seconds elapsed")
  5.  
  6. def application(environ, start_response):
  7. start_response('200 OK', [('Content-Type','text/html')])
  8. asyncio.get_event_loop().call_later(2, two_seconds_elapsed)
  9. return [b"Hello World"]

Once called, the application function will register a callable in the asyncio event loop and then will return to the client.

After two seconds the event loop will run the function.

You can run the example with:

  1. uwsgi --asyncio 10 --http-socket :9090 --greenlet --wsgi-file app.py

—asyncio is a shortcut enabling 10 uWSGI async cores, enabling you to manage up to 10 concurrent requests with a single process.

But how to wait for a callback completion in the WSGI callable?We can suspend our WSGI function using greenlets (remember our WSGI callable is wrapped on a greenlet):

  1. import asyncio
  2. import greenlet
  3.  
  4. def two_seconds_elapsed(me):
  5. print("Hello 2 seconds elapsed")
  6. # back to WSGI callable
  7. me.switch()
  8.  
  9. def application(environ, start_response):
  10. start_response('200 OK', [('Content-Type','text/html')])
  11. myself = greenlet.getcurrent()
  12. asyncio.get_event_loop().call_later(2, two_seconds_elapsed, myself)
  13. # back to event loop
  14. myself.parent.switch()
  15. return [b"Hello World"]

And we can go even further abusing the uWSGI support for WSGI generators:

  1. import asyncio
  2. import greenlet
  3.  
  4. def two_seconds_elapsed(me):
  5. print("Hello 2 seconds elapsed")
  6. me.switch()
  7.  
  8. def application(environ, start_response):
  9. start_response('200 OK', [('Content-Type','text/html')])
  10. myself = greenlet.getcurrent()
  11. asyncio.get_event_loop().call_later(2, two_seconds_elapsed, myself)
  12. myself.parent.switch()
  13. yield b"One"
  14. asyncio.get_event_loop().call_later(2, two_seconds_elapsed, myself)
  15. myself.parent.switch()
  16. yield b"Two"

Another example: Futures and coroutines

You can spawn coroutines from your WSGI callable using the asyncio.Task facility:

  1. import asyncio
  2. import greenlet
  3.  
  4. @asyncio.coroutine
  5. def sleeping(me):
  6. yield from asyncio.sleep(2)
  7. # back to callable
  8. me.switch()
  9.  
  10. def application(environ, start_response):
  11. start_response('200 OK', [('Content-Type','text/html')])
  12. myself = greenlet.getcurrent()
  13. # enqueue the coroutine
  14. asyncio.Task(sleeping(myself))
  15. # suspend to event loop
  16. myself.parent.switch()
  17. # back from event loop
  18. return [b"Hello World"]

Thanks to Futures we can even get results back from coroutines…

  1. import asyncio
  2. import greenlet
  3.  
  4. @asyncio.coroutine
  5. def sleeping(me, f):
  6. yield from asyncio.sleep(2)
  7. f.set_result(b"Hello World")
  8. # back to callable
  9. me.switch()
  10.  
  11.  
  12. def application(environ, start_response):
  13. start_response('200 OK', [('Content-Type','text/html')])
  14. myself = greenlet.getcurrent()
  15. future = asyncio.Future()
  16. # enqueue the coroutine with a Future
  17. asyncio.Task(sleeping(myself, future))
  18. # suspend to event loop
  19. myself.parent.switch()
  20. # back from event loop
  21. return [future.result()]

A more advanced example using the aiohttp module (remember to pip install aiohttp it, it’s not a standard library module)

  1. import asyncio
  2. import greenlet
  3. import aiohttp
  4.  
  5. @asyncio.coroutine
  6. def sleeping(me, f):
  7. yield from asyncio.sleep(2)
  8. response = yield from aiohttp.request('GET', 'http://python.org')
  9. body = yield from response.read_and_close()
  10. # body is a byterray !
  11. f.set_result(body)
  12. me.switch()
  13.  
  14.  
  15. def application(environ, start_response):
  16. start_response('200 OK', [('Content-Type','text/html')])
  17. myself = greenlet.getcurrent()
  18. future = asyncio.Future()
  19. asyncio.Task(sleeping(myself, future))
  20. myself.parent.switch()
  21. # this time we use yield, just for fun...
  22. yield bytes(future.result())

Status

  • The plugin is considered experimental (the implications of asyncio with WSGI are currently unclear). In the future it could be built by default when Python >= 3.4 is detected.
  • While (more or less) technically possible, mapping a WSGI callable to a Python 3 coroutine is not expected in the near future.
  • The plugin registers hooks for non blocking reads/writes and timers. This means you can automagically use the uWSGI API with asyncio. Check the https://github.com/unbit/uwsgi/blob/master/tests/websockets_chat_asyncio.py example.