I am playing around with the new async views from Django 3.1.
Some benefits I would love to have is to do some simple fire-and-forget "tasks" after the view already gave its HttpResponse
, like sending a push notification or sending an email.
I am not looking for solutions with third-party packages like celery!
To test this async views I used some code from this tutorial: https://testdriven.io/blog/django-async-views/
async def http_call_async():
for num in range(1, 200):
await asyncio.sleep(1)
print(num)
# TODO: send email async
print('done')
async def async_view(request):
loop = asyncio.get_event_loop()
loop.create_task(http_call_async())
return HttpResponse("Non-blocking HTTP request")
I started the django server with uvicorn.
When I make a request to this view it will immediately return the HTTP-response "Non-blocking HTTP request".
In the meantime and after the HTTP-response, the event loop continues happily to print the numbers up to 200 and then print "done".
This is exactly the behaviour I want to utilize for my fire-and-forget tasks.
Unfortunatly I could not find any information about the lifetime of the event loop running this code.
How long does the event loop live? Is there a timeout? On what does it depend? On uvicorn? Is that configurable?
Are there any resources which discuss this topic?
Django has support for writing asynchronous (“async”) views, along with an entirely async-enabled request stack if you are running under ASGI. Async views will still work under WSGI, but with performance penalties, and without the ability to have efficient long-running requests.
Django itself is synchronous. each HTTP request will be handled completely synchronously. However you have extensions like django-channels ( https://github.com/django/channels ) , which are asynchronous and are intended for web sockets / etc.
Show activity on this post. Also, this method won't technically be non-blocking as Django is not a non-blocking framework in most deployments, but it will let you do something after returning a response. Tornado will let you perform non-blocking requests.
Django (which does not provide an ASGI server) does not limit the lifetime of the event loop.
In the case of this question:
uvloop
(both do not limit their lifetime)For Uvicorn:
How long does the event loop live?
The event loop lives as long as the worker lives.
Is there a timeout?
No, tasks run until completion (unless the worker is unresponsive and is killed by Gunicorn).
On what does it depend? On Uvicorn?
Worker lifetime is mainly limited by --limit-max-requests
(Gunicorn: --max-requests
).
See the issue for yourself by specifying --limit-max-requests 2
and running the view twice:
uvicorn mysite.asgi:application --limit-max-requests 2
Regardless of how the lifetime is limited, what we might care about is how to handle a shutdown.
Let's see how Uvicorn worker (uvicorn.server.Server
) does graceful shutdown for requests.
uvicorn.protocols.http.h11_impl.H11Protocol#__init__: Store a reference to server tasks
# Shared server state self.server_state = server_state self.tasks = server_state.tasks
uvicorn.protocols.http.h11_impl.H11Protocol#handle_events: Add request task to tasks
self.cycle = RequestResponseCycle( ... ) task = self.loop.create_task(self.cycle.run_asgi(app)) task.add_done_callback(self.tasks.discard) self.tasks.add(task)
uvicorn.server.Server#shutdown: Wait for existing tasks to complete
if self.server_state.tasks and not self.force_exit: msg = "Waiting for background tasks to complete. (CTRL+C to force quit)" logger.info(msg) while self.server_state.tasks and not self.force_exit: await asyncio.sleep(0.1)
Let's piggyback on this by adding your tasks to the server tasks.
async def async_view(request):
loop = asyncio.get_event_loop()
# loop.create_task(http_call_async()) # Replace this
task = loop.create_task(http_call_async()) # with this
server_state = await get_server_state() # Add this
task.add_done_callback(server_state.tasks.discard) # Add this
server_state.tasks.add(task) # Add this
return HttpResponse("Non-blocking HTTP request")
import gc
from uvicorn.server import ServerState
_server_state = None
@sync_to_async
def get_server_state():
global _server_state
if not _server_state:
objects = gc.get_objects()
_server_state = next(o for o in objects if isinstance(o, ServerState))
return _server_state
Now, Uvicorn worker will wait for your tasks in a graceful shutdown.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With