From version 3.1 Django supports async views. I have a Django app running on uvicorn. I'm trying to write an async view, which can handle multiple requests to itself concurrently, but have no success.
Common examples, I've seen, include making multiple slow I/O operations from inside the view:
async def slow_io(n, result):
await asyncio.sleep(n)
return result
async def my_view(request, *args, **kwargs):
task_1 = asyncio.create_task(slow_io(3, 'Hello'))
task_2 = asyncio.create_task(slow_io(5, 'World'))
result = await task_1 await task_2
return HttpResponse(result)
This will yield us "HelloWorld" after 5 seconds instead of 8 because requests are run concurrently.
What I want - is to concurrently handle multiple requests TO my_view. E.g. I expect this code to handle 2 simultaneous requests in 5 seconds, but it takes 10.
async def slow_io(n, result):
await asyncio.sleep(n)
return result
async def my_view(request, *args, **kwargs):
result = await slow_io(5, 'result')
return HttpResponse(result)
I run uvicorn with this command:
uvicorn --host 0.0.0.0 --port 8000 main.asgi:application --reload
Django doc says:
The main benefits are the ability to service hundreds of connections without using Python threads.
So it's possible.
What am I missing?
UPD: It seems, my testing setup was wrong. I was opening multiple tabs in browser and refreshing them all at once. See my answer for details.
CodePudding user response:
Your problem is that you write code like sync version. And you await every function result and only after this, you await next function.
You simple need use asyncio functions like gather
to run all tasks asynchronously:
import asyncio
async def slow_io(n, result):
await asyncio.sleep(n)
return result
async def my_view(request, *args, **kwargs):
all_tasks_result = await asyncio.gather(slow_io(3, 'Hello '), slow_io(5, "World"))
result = "".join(all_tasks_result)
return HttpResponse(result)
CodePudding user response:
Here is a sample project on Django 3.2 with multiple async views and tests. I tested it multiple ways:
- Requests from Django's test client are handled simultaneously, as expected.
- Requests to different views from single client are handled simultaneously, as expected.
- Requests to the same view from different clients are handled simultaneously, as expected.
What doesn't work as expected?
- Requests to the same view from single client are handled by one at a time, and I didn't expect that.
There is a warning in Django doc:
You will only get the benefits of a fully-asynchronous request stack if you have no synchronous middleware loaded into your site. If there is a piece of synchronous middleware, then Django must use a thread per request to safely emulate a synchronous environment for it.
Middleware can be built to support both sync and async contexts. Some of Django’s middleware is built like this, but not all. To see what middleware Django has to adapt, you can turn on debug logging for the django.request logger and look for log messages about “Synchronous middleware … adapted”.
So it might be some sync middleware causing the problem even in the bare Django. But it also states, that Django should create a thread per request in that case.
My best guess, that it's related to client-server connection and not to sync or async stack. Despite Google says that browsers create new connections for each tab due to security reasons, I think:
- Browser might keep the same connection for multiple tabs if the url is the same for economy reasons.
- Django creates async tasks or threads per connection and not per request, as it states.
Need to check it out.