Skip to content

Async Support

Introduction

Since version 3.1, Django ships with asynchronous view support. This allows you to run efficient concurrent views that are network- and/or I/O-bound.

pip install Django>=3.1 django-ninja

Asynchronous views work more efficiently when: - Calling external APIs over the network - Performing/awaiting database queries - Reading/writing to disk drives

Django Ninja fully embraces asynchronous views and makes them very easy to use.

Quick example

Code

Let's take an example. We have an API operation that does some work (for now, just sleeping for a given number of seconds) and returns a word:

import time

@api.get("/say-after")
def say_after(request, delay: int, word: str):
    time.sleep(delay)
    return {"saying": word}

To make this code run asynchronously, all you have to do is add the async keyword to the function (and use an async-aware library for the work - in our case we replace the standard library's sleep with asyncio.sleep):

import asyncio

@api.get("/say-after")
async def say_after(request, delay: int, word: str):
    await asyncio.sleep(delay)
    return {"saying": word}

Running

To run this code, you need an ASGI server like Uvicorn or Daphne. Let's use Uvicorn here, the example is below:

To install Uvicorn, use:

pip install uvicorn

Then start the server:

uvicorn your_project.asgi:application --reload

Note: replace your_project with your project's package name
The --reload flag is used to automatically reload the server whenever you make any changes to the code (do not use in production)

Note

You can run asynchronous views with manage.py runserver, but it does not play well with some libraries, so at this time (July 2020) it is recommended to use an ASGI server like Uvicorn or Daphne.

Testing

Open http://127.0.0.1:8000/api/say-after?delay=3&word=hello (delay=3) in your browser. After waiting 3 seconds, you should see the "hello" message.

Now let's flood this operation with 100 parallel requests:

ab -c 100 -n 100 "http://127.0.0.1:8000/api/say-after?delay=3&word=hello"

This will produce something like this:

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.1      1       4
Processing:  3008 3063  16.2   3069    3082
Waiting:     3008 3062  15.7   3068    3079
Total:       3008 3065  16.3   3070    3083

Percentage of the requests served within a certain time (ms)
  50%   3070
  66%   3072
  75%   3075
  80%   3076
  90%   3081
  95%   3082
  98%   3083
  99%   3083
 100%   3083 (longest request)

According to these numbers, our service is able to handle these 100 concurrent requests with only a little overhead.

To achieve the same concurrency with WSGI and synchronous operations, you would need to start about 10 workers with 10 threads each!

Mixing synchronous and asynchronous operations

Keep in mind that you can use both sync and async operations in your project, and Django Ninja will route them automatically:

@api.get("/say-sync")
def say_after_sync(request, delay: int, word: str):
    time.sleep(delay)
    return {"saying": word}

@api.get("/say-async")
async def say_after_async(request, delay: int, word: str):
    await asyncio.sleep(delay)
    return {"saying": word}

Elasticsearch example

Let's take a real-world use case. For this example, let's use a recent version of Elasticsearch that comes with async support:

pip install elasticsearch>=7.8.0

Now, instead of using the Elasticsearch class, use the AsyncElasticsearch class and await the result:

from ninja import NinjaAPI
from elasticsearch import AsyncElasticsearch


api = NinjaAPI()

es = AsyncElasticsearch()


@api.get("/search")
async def search(request, q: str):
    resp = await es.search(
        index="documents", 
        body={"query": {"query_string": {"query": q}}},
        size=20,
    )
    return resp["hits"]

Using the ORM

Currently, certain key parts of Django are not able to operate safely in an asynchronous environment because they have global state that is not coroutine-aware. These parts of Django are classified as "asynchronous-unsafe" and are protected from execution in an asynchronous environment. The ORM is the main example, but there are other parts that are protected in this way. Learn more about async safety in the Django official documentation.

So if you do this:

@api.get("/blog/{post_id}")
async def search(request, post_id: int):
    blog = Blog.objects.get(pk=post_id)
    ...

It will throw an error. Until the async ORM implementation is available, you can use the sync_to_async() adapter:

from asgiref.sync import sync_to_async

@sync_to_async
def get_blog(post_id):
    return Blog.objects.get(pk=post_id)

@api.get("/blog/{post_id}")
async def search(request, post_id: int):
    blog = await get_blog(post_id)
    ...

Or even shorter:

@api.get("/blog/{post_id}")
async def search(request, post_id: int):
    blog = await sync_to_async(Blog.objects.get)(pk=post_id)
    ...

There is a common pitfall: Django querysets are lazily evaluated (the database query happens only when you start iterating over them), so this will not work:

all_blogs = await sync_to_async(Blog.objects.all)()
# it will throw an error later when you try to iterate over all_blogs
...

Instead, force evaluation (with list):

all_blogs = await sync_to_async(list)(Blog.objects.all())
...

Since Django version 4.1, Django ships with asynchronous versions of ORM operations. These eliminate the need to use sync_to_async in most cases. The asynchronous operations have the same names as their synchronous counterparts, but prefixed with a. So using the example above, you can rewrite it as:

@api.get("/blog/{post_id}")
async def search(request, post_id: int):
    blog = await Blog.objects.aget(pk=post_id)
    ...

When working with querysets, use async for paired with a list comprehension:

all_blogs = [blog async for blog in Blog.objects.all()]
...

Learn more about the asynchronous ORM interface in the Django official documentation.

Success

Continue to the next section API versioning

Comments


本文总阅读量次