Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How long does the event_loop live in a Django>=3.1 async view

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?

like image 640
DanEEStar Avatar asked Dec 10 '20 13:12

DanEEStar


People also ask

What is async view in Django?

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.

What is synchronous and asynchronous in Django?

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.

Is Django blocking?

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.


1 Answers

Django (which does not provide an ASGI server) does not limit the lifetime of the event loop.

In the case of this question:

  • ASGI server: Uvicorn
  • Event loop: either the built-in asyncio event loop or 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).

    • Is that configurable?
      Yes, but the worker still exits at some point. It may also be killed for other reasons.

See the issue for yourself by specifying --limit-max-requests 2 and running the view twice:

uvicorn mysite.asgi:application --limit-max-requests 2

Graceful shutdown

Regardless of how the lifetime is limited, what we might care about is how to handle a shutdown.

How does Uvicorn do graceful 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)

How to make Uvicorn worker wait for your task?

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.

like image 180
aaron Avatar answered Nov 12 '22 12:11

aaron