Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: Async view support #7260

Closed
tomchristie opened this issue Apr 7, 2020 · 35 comments
Closed

Discussion: Async view support #7260

tomchristie opened this issue Apr 7, 2020 · 35 comments

Comments

@tomchristie
Copy link
Member

tomchristie commented Apr 7, 2020

Given Django 3.1's upcoming support for async views, it's worth us talking though if there are useful points of async support in REST framework, and what they would be if so.

I'm going to prefix this by starting with a bit of expectation setting... Django 3.1's async view support is a really impressive bit of foundational work, but there's currently limitations to where it's actually valuable, given that the ORM isn't yet async-capable.

One thing that'd be really helpful to this discussion would be concrete use-cases where folks demonstrate an actual use-case where they've used or would use an async view in Django, together with the motivation, and demonstrable improvements vs. sticking with a regular sync view.

We'd also want to scoping this down to the most minimal possible starting point.

In particular, what would we need to change in order to support this?...

@api_view(['GET'])
async def my_view(request):
    ...

The @api_view decorator is implemented on top of the APIView class based view, so an even more minimal question is: what would we need to change in order to support something like this?...

class MyView(AsyncAPIView):
    async def get(self, request):
        ...

There's a whole bunch of different things to consider there, eg...

  • The authentication/permissions is likely to be a sync ORM operation at the moment.
  • The throttling is likely to be a sync cache operation at the moment.
  • Does Django support reading the request body as an async operation? How would we need to tie this in with the parsing.

But let's just put those aside for the moment.

Django's upcoming docs for 3.1 async views mentions...

For a class-based view, this means making its __call__() method an async def
(not its __init__() or as_view()).

So here's some even simpler questions:

  • What does a Django 3.1 async CBV look like?
  • What would using REST framework's Request explicitly within a Django async view look like (rather than wrapping with @api_view). What provisos are there on operations on the request instance that are currently sync?
  • What would using REST framework's Response explicitly within a Django async view look like (rather than wrapping with @api_view). Are there any provisos on sync operations there?
@encode encode deleted a comment from Skorpyon Apr 9, 2020
@encode encode deleted a comment from mrocklin Apr 9, 2020
@encode encode deleted a comment from Skorpyon Apr 9, 2020
@tomchristie
Copy link
Member Author

tomchristie commented Apr 9, 2020

I'm dropping those comments not because they're problematic, but because I think they're derailing the issue somewhat. I don't really want to have a conversation here about the relative merits of opting for an async-native framework. There's great options there and growing support for a wider async ecosystem which encode is also playing it's part in developing.

Django is starting to gain some built-in async support. That's useful in some limited cases, and it'd be worthwhile for REST framework to track Django's async support as it grows, even if that comes with some significant caveats at this point in time.

@Kub-AT
Copy link

Kub-AT commented May 26, 2020

Btw. maybe https://github.com/hishnash/djangochannelsrestframework can be an simple inspiration

@johnthagen
Copy link
Contributor

johnthagen commented Jun 8, 2020

Would ❤️ some kind of native, official websocket story for DRF that allows reuse of existing DRF components.

This would help position DRF as an option for more real-time bi-directional use cases.

@pettermk
Copy link

This one is not strictly for views, but the async view would be a precondition for this to be possible. I've been wanting for some time to be able to use async in my serializers, to speed it up when I have highly IO bound calls in my custom serializer fields. Example

class CarSerializer(serializers.ModelSerializer):
    engine = serializers.SerializerMethodField()
    class Meta:
        model = Car
        fields = ('make', 'model', 'engine')

    async def get_engine(self, obj):
        return get_engine_from_manufacturer(obj.make, obj.model) # Calls slow external API for getting more data

For this simple example we would not gain a lot, but some times there are many such custom fields, or even some times I am serializing many cars in a nested serializer. In these cases I imagine there would be a lot to gain.

@manfre
Copy link

manfre commented Aug 26, 2020

Here's a few more use cases that gain a benefit from varying degrees of async support:

  • Service with a non-relational backend (e.g. DynamoDB) that avoid the ORM async complexity/issues
  • Scatter gather API frontends to other services (no DB of its own)
  • Post save signals for caching, cache invalidation, messaging (kafka, sqs, etc)

@shawnngtq
Copy link

Expressing my interest to see this happen!

I have seen the benchmark for fastapi and DRF, and fastapi is faster.

Would love to stick to DRF as I am comfortable with Django and don't have a lot of time to learn fastapi to quickly deliver a good performing API.

@hadim
Copy link

hadim commented Sep 2, 2020

I am also very interested in async support in Django and DRF. Our use case is pretty common I believe. We make IO network calls within our Django models or serializers to an external API.

We try to perform most of those calls in Celery but given that those calls are not computationally intensive, that would be simpler to keep them within Django itself.

@carltongibson
Copy link
Collaborator

carltongibson commented Sep 2, 2020

@hadim You should be able to do this already with a Django 3.1 view. Use httpx to make the network calls.

You don't even need to use ASGI. Just define an async def view and run under WSGI as normal, and Django takes care of the rest.

This seems to be the core use-case.

It's not clear what DRF needs to add? 🤔

@TheBlusky
Copy link
Contributor

TheBlusky commented Sep 2, 2020

@carltongibson I'm not sure @api_view décorator and methods in ViewSet with @action decorator can be defined as async out of the box. That's why there might be some work in order to implement async within DRF for those views.

@johnthagen

This comment has been minimized.

@carltongibson
Copy link
Collaborator

@TheBlusky ah, yes ViewSet... fair point.

@TheBlusky
Copy link
Contributor

Is there any work on this subject ? Is there any way to help on this matter ?

@carltongibson
Copy link
Collaborator

@TheBlusky you could experiment with what's required to get an APIView working here. That's the first step I think.

@cochiseruhulessin
Copy link

cochiseruhulessin commented Sep 20, 2020

We have a use case of downloading a file from a cloud storage provider and then passing it on to the client, async would also be a good fit for this.

A good starting point would be the APIView in implementing this feature. Basically:

  • APIView __call__ must be async.
  • Decorators like action must check if the decorated function is async and return appropriately.
  • Async serializers?
  • There is also some low-level work such as ensure that the Request object also has async methods for receiving the response body.

@gbaca
Copy link

gbaca commented Oct 9, 2020

One thing that'd be really helpful to this discussion would be concrete use-cases where folks demonstrate an actual use-case where they've used or would use an async view in Django, together with the motivation, and demonstrable improvements vs. sticking with a regular sync view.

My use case for this is a flow whereby RPC calls are made from my Django Server to RabbitMQ and all the way back to Django and the response is sent to the client who made the Django HTTP call. These RPC messages could take some time to complete and we ideally don't want our application to be blocking while waiting for the RPC reply to come into Django. We would love to use the power of DRF alongside non-blocking async capable API endpoints and all the community-packages that work on top of DRF, such as DRF Access Policy. DRF Access Policy would allow us to control access permissions while maintaining our application as async.

@alanjds
Copy link
Contributor

alanjds commented Oct 16, 2020

To be clear: right now (sex out 16 19:41:37 -03 2020), DRF provides sync-only views, even on master. And there is no branch yet to bring async view support.

Had I understood this correctly?

@xordoquy
Copy link
Collaborator

Only thoughts at this point.
@tomchristie you might want to edit the initial post and take the APIView as a starting point to remove the extra complexity of the api_view decorator.

@JonasKs
Copy link

JonasKs commented Oct 30, 2020

Any status on this? Django3.1 and async is still awesome for non-ORM views and the APIView class would be nice to have.

@Tobeyforce
Copy link

+1 for async support in DRF. Should be of highest priority!

@mosi-kha
Copy link

+1

@xordoquy
Copy link
Collaborator

Could we please have some comments that adds something to the discussion instead of spams ?
If it is the highest priority to you, you probably have experience about it and should be able to help and contribute.
That does not need to be big.
Now please, let's add value here instead of noise.

@alanjds
Copy link
Contributor

alanjds commented Nov 24, 2020

Ok. There is an specific ":+1:" button on Github UI if somebody wants to +1. Somebody could had missed that and this does not mean a spam. That said, I suggest new comments with "+1" to be removed and we can chill from now on.

Back to the main question. I tried to @async_to_sync on the view I wanted async, then dug a bit before giving up and getting back to my main card on the company.

Is acceptable to just allow the CBV methods to be async defs and the internal dispatcher decide to call async_to_sync if needed?

I think this is better than forcing the methods to be sync and handle this on their own, as on future I expect the async methods to be more common than the sync ones.

@xordoquy
Copy link
Collaborator

Seeing that Django's own CBV don't have anything async, I still fail to see what's needed on DRF at the moment.
Has someone an example of Django's CBV supporting async ?
I think it should be the starting point before even getting to DRF because APIView inherits from django.views.generic.View.

@JonasKs
Copy link

JonasKs commented Nov 24, 2020

Status on CBV in Django can be found on the forums. There's a PR to his fork here.

@tomchristie
Copy link
Member Author

Once there's support for CBVs in Django's master branch I'd be happy to take a look at this on our side.

@Char2sGu
Copy link

Char2sGu commented Dec 10, 2020

Here is my naive implementation of async CBVs in DRF, I hope it can help.

    from rest_framework.response import Response
    from rest_framework import status

    from asgiref.sync import sync_to_async
    import asyncio as aio


    class AsyncMixin:
        """Provides async view compatible support for DRF Views and ViewSets.

        This must be the first inherited class.

            class MyViewSet(AsyncMixin, GenericViewSet):
                pass
        """
        @classmethod
        def as_view(cls, *args, **initkwargs):
            """Make Django process the view as an async view.
            """
            view = super().as_view(*args, **initkwargs)

            async def async_view(*args, **kwargs):
                # wait for the `dispatch` method
                return await view(*args, **kwargs)
            async_view.csrf_exempt = True
            return async_view

        async def dispatch(self, request, *args, **kwargs):
            """Add async support.
            """
            self.args = args
            self.kwargs = kwargs
            request = self.initialize_request(request, *args, **kwargs)
            self.request = request
            self.headers = self.default_response_headers

            try:
                await sync_to_async(self.initial)(
                    request, *args, **kwargs)  # MODIFIED HERE

                if request.method.lower() in self.http_method_names:
                    handler = getattr(self, request.method.lower(),
                                    self.http_method_not_allowed)
                else:
                    handler = self.http_method_not_allowed

                # accept both async and sync handlers
                # built-in handlers are sync handlers
                if not aio.iscoroutinefunction(handler):  # MODIFIED HERE
                    handler = sync_to_async(handler)  # MODIFIED HERE
                response = await handler(request, *args, **kwargs)  # MODIFIED HERE

            except Exception as exc:
                response = self.handle_exception(exc)

            self.response = self.finalize_response(
                request, response, *args, **kwargs)
            return self.response


    class AsyncCreateModelMixin:
        """Make `create()` and `perform_create()` overridable.

        Without inheriting this class, the event loop can't be used in these two methods when override them.

        This must be inherited before `CreateModelMixin`.

            class MyViewSet(AsyncMixin, GenericViewSet, AsyncCreateModelMixin, CreateModelMixin):
                pass
        """
        async def create(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data)
            await sync_to_async(serializer.is_valid)(
                raise_exception=True)  # MODIFIED HERE
            await self.perform_create(serializer)  # MODIFIED HERE
            headers = self.get_success_headers(serializer.data)
            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

        async def perform_create(self, serializer):
            await sync_to_async(serializer.save)()


    class AsyncDestroyModelMixin:
        """Make `destroy()` and `perform_destroy()` overridable.

        Without inheriting this class, the event loop can't be used in these two methods when override them.

        This must be inherited before `DestroyModelMixin`.

            class MyViewSet(AsyncMixin, GenericViewSet, AsyncDestroyModelMixin, DestroyModelMixin):
                pass
        """
        async def destroy(self, request, *args, **kwargs):
            instance = await sync_to_async(self.get_object)()  # MODIFIED HERE
            await self.perform_destroy(instance)  # MODIFIED HERE
            return Response(status=status.HTTP_204_NO_CONTENT)

        async def perform_destroy(self, instance):
            await sync_to_async(instance.delete)()  # MODIFIED HERE

    # other mixins can be created similarly

@mongakshay
Copy link

mongakshay commented Dec 18, 2020

is DRF supporting async views with async ORM Calls, yet ?

@Char2sGu
Copy link

No. DRF is a Django app, we can only wait for Django to support that.

@Ariki
Copy link

Ariki commented Dec 19, 2020

        @classmethod
        def as_view(cls, *args, **initkwargs):
            """Make Django process the view as an async view.
            """
            view = super().as_view(*args, **initkwargs)

            async def async_view(*args, **kwargs):
                # wait for the `dispatch` method
                return await view(*args, **kwargs)
            return async_view

I also had to add async_view.csrf_exempt = True to make it work in my case (I use django-oauth-toolkit).

@Crocmagnon
Copy link

Crocmagnon commented Feb 9, 2021

Hey there! 👋🏻

Just chiming in to check whether there have been any progress on supporting Django 3.1's async views so far?

Here's our use case:
We're building a REST API that requires integrating with SSH remote systems. We're running ~3-5 commands for each API call and each command currently takes ~15s. This leaves us with a response time of ~45-60s which could be boiled down to 15s if we were able to run all these commands in parallel.

We want to achieve something similar to the following script, but during an API call. The real commands were replaced by a sleep and ls:

import asyncio, asyncssh, time, os

USERNAME = os.getenv("POC_SSH_USERNAME")
PASSWORD = os.getenv("POC_SSH_PASSWORD")
HOST = os.getenv("POC_SSH_HOST")


async def main():
    start = time.time()
    folders = [".", "subfolder1", "subfolder2", "subfolder3"]
    tasks = []
    for folder in folders:
        tasks.append(task(folder))
    res = await asyncio.gather(*tasks)
    print(res)
    end = time.time()
    print(f"time: {end - start}")


async def task(folder):
    async with asyncssh.connect(HOST, username=USERNAME, password=PASSWORD) as conn:
        res = await conn.run(f"sleep 5; ls {folder}", check=True)
    return res


if __name__ == "__main__":
    asyncio.run(main())

We also have some ORM calls to make in the view before and after running the SSH commands.

We're currently using a viewsets.ViewSet.

@patroqueeet
Copy link

patroqueeet commented Mar 2, 2021

Hey, our use case would be:

Stack: Django, DRF, ReactJS

We are providing corporate compliance related meetings with presence tracking and real time polls. Hence It would be fantastic if using DRF we could enable a meeting moderator to step through decisions/agenda items just by publishing them one after the other. the published item would pop up through REST on meeting participants browser and collect their vote. instantly moderator would see the results come in till all people have voted. with sync this would mean A LOT of frequent check calls, still with some seconds of delay probably.

@Crocmagnon
Copy link

@patroqueeet your use case makes me think of websockets, I don’t see how implementing async views in DRF would help 🤔

@patroqueeet
Copy link

patroqueeet commented Mar 2, 2021

@patroqueeet your use case makes me think of websockets, I don’t see how implementing async views in DRF would help 🤔

yeah, new area to me. reading all day already docu/tutorials of web socket/async views already... as I don't have web sockets in place yet but giant DRF code, I would have liked to use what we have and not grow the ecosystem of libs further...

@Skorpyon
Copy link

Skorpyon commented Mar 2, 2021

@patroqueeet My solution for websockets + django is next:
Django (sync backend) -> RabbitMQ (message bus) -> Aiohttp (async backend, mainly for websocket connections)

  1. UI login to Aiohttp with JWT or session cookie.
  2. Aiohttp kick special internal API of Django for auth, Djange response with user instance.
  3. Aiohtth store pair [user_id, websocket_connection] in memory.
  4. Django's threads sending messages to RabbitMq.
  5. Aiohttp listen AMQP queues and obtain messages.
  6. Aiohttp looking for user_id in list of open websocket channels and push messages in this channels.

Solution proven by time and good load. Actually one process of Aiohttp may handle thousands of websocket connections.

@patroqueeet
Copy link

patroqueeet commented Mar 3, 2021

@Skorpyon checked Aiohttp... looking very nice. but after reading ~20 articles about django/websocket/async and learning about http, starlet, unicorn, Daphne et al yesterday all day long. I will now start doing a quick prototyping based on 3.1 from this guy here https://alex-oleshkevich.medium.com/websockets-in-django-3-1-73de70c5c1ba (following the recommendation from @Crocmagnon) - I like the simplicity and I can use as much of my stack as possible. probably using DRF serializers to render and parse the JSON... Let's see how fast I can fail (making me learn even faster) :)

@encode encode locked and limited conversation to collaborators Mar 3, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests