Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Abu Ashraf Masnun: Exploring Asyncio - uvloop, sanic and motor

$
0
0

The asyncio package was introduced in the standard library from Python 3.4. The package is still in provisional stage, that is backward compatibility can be broken with future changes. However, the Python community is pretty excited about it and I know personally that many people have started using it in production. So, I too decided to try it out. I built a rather simple micro service using the excellent sanic framework and motor (for accessing mongodb). uvloop is an alternative event loop implementation written in Cython on top of libuv and can be used as a drop in replacement for asyncio’s event loop. Sanic uses uvloop behind the scene to go fast.

In this blog post, I would quickly introduce the technologies involved and then walk through some sample code with relevant explanations.

What is Asyncio? Why Should I Care?

In one of my earlier blog post - Async Python: The Different Forms of Concurrency, I have tried to elaborate on the different ways to achieve concurrency in the Python land. In the last part of the post, I have tried to explain what asyncio brings new to the table.

Asyncio allows us to write asynchronous, concurrent programs running on a single thread, using an event loop to schedule tasks and multiplexing I/O over sockets (and other resources). The one line explanation might be a little complex to comprehend at a glance. So I will break it down. In asyncio, everything runs on a single thread. We use coroutines which can be treated as small units of task that we can pause and resume. Then there is I/O multiplexing - when our tasks are busy waiting for I/O, an event loop pauses them and allows other tasks to run. When the paused tasks finish I/O, the event loop resumes them. This way even a single thread can handle / serve a large number of connections / clients by effectively juggling between “active” tasks and tasks that are waiting for some sort of I/O.

In general synchronous style, for example, when we’re using thread based concurrency, each client will occupy a thread and when we have a large number of connections, we will soon run out of threads. Though not all of those threads were active at a given time, some might have been simply waiting for I/O, doing nothing. Asyncio helps us solve this problem and provides an efficient solution to the concurrency problem.

While Twisted, Tornado and many other solutions have existed in the past, NodeJS brought huge attention to this kind of solution. And with Asyncio being in the standard library, I believe it will become the standard way of doing async I/O in the Python world over time.

What about uvloop?

We talked about event loop above. It schedules the tasks and deals with various events. It also manages the I/O multiplexing using the various options offered by the operating system. In simple words - the event loop is very critical and the central part of the whole asyncio operations. The asyncio package ships with an event loop by default. But we can also swap it for our custom implementations if we need/prefer. uvloop is one such event loop that is very very fast. The key to it’s success could be partially attributed to Cython. Cython allows us to write codes in Python like syntax while the codes perform like C. uvloop was written in Cython and it uses the famous libuv library (also used by NodeJS).

If you are wondering if uvloop’s performances are good enough reason to swap out the default event loop, you may want to read this aricle here - uvloop: Blazing fast Python networking or you can just look at this following chart taken from that blog post:

Yes, it can go faster than NodeJS and catch up to Golang. Convinced yet? Let’s talk about Sanic!

Sanic - Gotta go fast!

Sanic was inspired by the above article I talked about. They used uvloop and httptools too (referenced in the article). The framework provides a nice, Flask like syntax along with the async / await syntax from Python 3.5.

Please Note:uvloop still doesn’t work on Windows properly. Sanic uses the default asyncio event loop if uvloop is not available. But this probably doesn’t matter because in most cases we deploy to linux machines anyway. Just in case you want to try out the performance gains on Windows, I recommend you use a VM to test it inside a Linux machine.

Motor

Motor started off as an async mongodb driver for Tornado. Motor = Mongodb + Tornado. But Motor now has pretty nice support for asyncio. And of course we can use the async / await syntax too.

I guess we have had brief introductions to the technologies we are going to use. So let’s get started with the actual work.

Setting Up

We need to install sanic and motor using pip.

pip install sanic
pip install motor

Sanic should also install it’s dependencies including uvloop and ujson along with others.

Set uvloop as the event loop

We will swap out the default event loop and use uvloop instead.

import asyncio
import uvloop

asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

Simple as that. We import asyncio and uvloop. We set the event loop policy to uvloop’s event loop policy and we’re done. Now asyncio will use uvloop as the default event loop.

Connecting to Mongodb

We will be using motor to connect to our mongodb. Just like this:

from motor.motor_asyncio import AsyncIOMotorClient

mongo_connection = AsyncIOMotorClient("<mongodb connection string>")

contacts = mongo_connection.mydatabase.contacts

We import the AsyncIOMotorClient and pass our mongodb connection string to it. We also point to our target collection using a name / variable so that we can easily (and directly) use that collection later. Here mydatabase is the db name and contacts is the collection name.

Request Handlers

Now we will dive right in and write our request handlers. For our demo application, I will create two routes. One for listing the contacts and one for creating new ones. But first we must instantiate sanic.

from sanic import Sanic
from sanic.response import json

app = Sanic(__name__)

Flask-like, remember? Now that we have the app instance, let’s add routes to it.

@app.route("/")
async def list(request):
    data = await contacts.find().to_list(20)
    for x in data:
        x['id'] = str(x['_id'])
        del x['_id']

    return json(data)


@app.route("/new")
async def new(request):
    contact = request.json
    insert = await contacts.insert_one(contact)
    return json({"inserted_id": str(insert.inserted_id)})

The routes are simple and for the sake of brevity, I haven’t written any error handling codes. The list function is async. Inside it we await our contacts to arrive from the database, as a list of 20 entries. In a sync style, we would use the find method directly but now we await it.

After we have the results, we quickly iterate over the documents and add id key and remove the _id key. The _id key is an instance of ObjectId which would need us to use the bson package for serialization. To avoid complexity here, we just convert the id to string and then delete the ObjectId instance. The rest of the document is usual string based key-value pairs (dict). So it should serialize fine.

In the new function, we grab the incoming json payload and pass it to the insert_one method directly. request.json would contain the dict representation of the json request. Check out this page for other request data available to you. Here, we again await the insert_one call. When the response is available, we take the inserted_id and send a response back.

Running the App

Let’s see the code first:

loop = asyncio.get_event_loop()

app.run(host="0.0.0.0", port=8000, workers=3, debug=True, loop=loop)

Here we get the default event loop and pass it to app.run along with other obvious options. With the workers argument, we can set how many workers we want to use. This allows us to spin up multiple workers and take advantages of multiple cpu cores. On a single core machine, we can just set it to 1 or totally skip that one.

The loop is optional as well. If we do not pass the loop, sanic will create a new one and set it as the default loop. But in our case, we have connected to mongodb using motor before the app.run function could actually run. Motor now already uses the default event loop. If we don’t pass that same loop to sanic, sanic will initialize a new event loop. Our database access and sanic server will be on two different event loops and we won’t be able to make database calls. That is why we use the get_event_loop function to retrieve the current default event loop and pass it to sanic. This is also why we set uvloop as the default event loop on top of the file. Otherwise we would end up with the default loop (that comes with asyncio) and sanic would also have to use that. Initializing uvloop at the beginning makes sure everyone uses it.

Final Code

So here’s the final code. We probably should clean up the imports and bring them up on top. But to relate to the different steps, I kept them as is. Also as mentioned earlier, the code has no error handling. We should write proper error handling code in all serious projects.

import asyncio
import uvloop

asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())



from motor.motor_asyncio import AsyncIOMotorClient

mongo_connection = AsyncIOMotorClient("<connection string>")

contacts = mongo_connection.mydatabase.contacts


from sanic import Sanic
from sanic.response import json

app = Sanic(__name__)


@app.route("/")
async def list(request):
    data = await contacts.find().to_list(20)
    for x in data:
        x['id'] = str(x['_id'])
        del x['_id']

    return json(data)


@app.route("/new")
async def new(request):
    contact = request.json
    insert = await contacts.insert_one(contact)
    return json({"inserted_id": str(insert.inserted_id)})


loop = asyncio.get_event_loop()

app.run(host="0.0.0.0", port=8000, workers=3, debug=True, loop=loop)

Now let’s try it out?

Trying Out

I have saved the above code as main.py. So let’s run it.

python main.py

Now we can use curl to try it out. Let’s first add a contact:

curl -X POST -H "Content-Type: application/json" -d '{"name": "masnun"}' "http://localhost:8000/new"

We should see something like:

{"inserted_id":"582ceb772c608731477f5384"}

Let’s verify by checking / -

curl -X GET "http://localhost:8000/"

If everything goes right, we should see something like:

[{"id":"582ceb772c608731477f5384","name":"masnun"}]

I hope it works for you too! :-)

If you have any feedback or suggestions, please feel free to share it in the comments section. I would love to disqus :-)


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>