I’ve recently had the pleasure of writing some services for a project
I’m working on and I went with
asyncio and Mozilla’s
If Python 3.4 has one killer feature I think it’s
combines the rock-solid design of Twisted with Python 3’s native
generator protocols. This is exactly what Python has been good at over
the years: taking good ideas and patterns and abstracting them into
the language. In the case of
asyncio it removes Twisted’s cumbersome
APIs and replaces it with work-a-day Python code. If you ever thought
generators and coroutines were cool then
asyncio is a prime example
of why they are so amazing.
If you haven’t checked it out already I highly recommend looking into
Mozilla’s circus. It’s a
process and socket manager that comes with some handy management
tools out of the box. It works nicely with
asyncio and is a
fantastic tool if you write services in Python.
To demonstrate how great this is we’re going to create a trivial echo
server today and run it under
circus. You’ll see how we can keep our
script as simple as possible while using
circus to manage our
processes and sockets. We’ll also take a quick peak at the management
tools that come with
circus out of the box.
In order to get started I recommend creating a
Python 3.4 and installing
circus from the master branch on the
project’s repository. At the time of this writing there is a critical
patch needed to make this work on the version of Python we’re using
that hasn’t made it into the official releases yet so the usual
caveats about experimentation and not running this in production
Install like so:
$ pip install git+git://github.com/mozilla-services/circus.git
Our server will simply echo back whatever the client sends and close
the connection. I have adapted it here from the
documentation to work with
circus and demonstrate some good habits
for writing effective services in Python:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
import asyncio import functools import logging import os import signal import socket import sys logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO")) log = logging.getLogger(__name__) class EchoServer(asyncio.Protocol): def connection_made(self, transport): peername = transport.get_extra_info("peername") log.info("connection from %s", peername) self.transport = transport def data_received(self, data): log.info("data received: %s", data.decode()) self.transport.write(data) # close the socket self.transport.close() loop = asyncio.get_event_loop() def ask_exit(signame): print("Received %s: exiting..." % signame) loop.stop() for signame in ("SIGINT", "SIGTERM"): loop.add_signal_handler(getattr(signal, signame), functools.partial(ask_exit, signame)) fd = int(sys.argv[-1:]) # get the socket fd from circus socket = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) coro = loop.create_server(EchoServer, sock=socket) server = loop.run_until_complete(coro) log.info("Serving on: %s", server.sockets.getsockname()) try: loop.run_forever() finally: server.close() loop.close()
The only thing that might be surprising if you haven’t seen it before
is the call to
socket.fromfd. Sockets are really just special kinds
of files in Unix-land (“everything is a file”). We can refer to them
by their file descriptor and that is precisely what we’re doing here.
circus will manage the configuration of the socket and binding it
for us and the OS will handle load balancing for us. That means less
code for us: win.
So you name your file
echoserver.py and you test it out by running
it in your shell from your terminal. You can see the log output. You
can connect to it from a telnet client and echo a line. Great!
$ echo "Hello, world!\n" | nc 127.0.0.1 8000 Hello, world!
You should also be able to see the connection in the logs if you’ve set the appropriate log level.
But now you’ve just landed your Series A and your investors want you
echoserver.py to web scale. It’s time to step up.
Once you have
circus installed in your environment you can write a
configuration file called
[watcher:echoserver] cmd = /home/me/.venvs/circus/bin/python echoserver.py $(circus.sockets.echoserver) use_sockets = True warmup_delay = 0 numprocesses = 4 [socket:echoserver] host = 127.0.0.1 port = 8000 [env:echoserver] LOG_LEVEL = DEBUG
You can run your application with:
$ circusd --daemon echoserver.ini
It’s super-easy from here to add more watchers and even manage
circusd via your init process or process supervisor of choice. Once
it’s running the fun doesn’t stop there. You also get handy tools for
managing your circus.
If everything is up and running it should find the running
process and connect to it. Get help by typing
help to see the
available commands. This is really nice if you structure your service
as a group of processes as you have a little shell from where you can
turn on and off individual components and check on how things are
But you also get a web console! And statsd integration! And you can control other programs like Redis too! See the circus documentation for more information.
The library support for
asyncio is growing fast (thanks in part to
the close influence of Twisted allowing porting libraries from there
to be straight-forward). I’m using
asyncio_mongo in my project. You can certainly write more
sophisticated applications and services on this stack.
I want to thank Tarek Ziade, Benoit Chesneau, Mozilla, and all of the contributors that made building Circus possible. You’ve made my life easier.
And thanks of course to the amazing Python community for forging ahead
with Python 3 and developing
Server programming just became fun again.