Django-rest-framework: Discussion : prise en charge de la vue asynchrone

Créé le 7 avr. 2020  ·  35Commentaires  ·  Source: encode/django-rest-framework

Compte tenu de la prise en charge à venir de Django 3.1 pour les vues asynchrones, cela vaut la peine que nous discutions s'il existe des points utiles de prise en charge asynchrone dans le cadre REST, et ce qu'ils seraient le cas échéant.

Je vais préfixer cela en commençant par un peu de paramètre d'attente... La prise en charge de la vue asynchrone de Django 3.1 est un travail de base vraiment impressionnant, mais il y a actuellement des limites là où elle est réellement utile, étant donné que l'ORM n'est pas t encore compatible asynchrone.

Une chose qui serait vraiment utile pour cette discussion serait des cas d'utilisation concrets où les gens démontrent un cas d'utilisation réel où ils ont utilisé ou utiliseraient une vue asynchrone dans Django, ainsi que la motivation et les améliorations démontrables par rapport au maintien avec une vue de synchronisation régulière.

Nous voudrions également réduire cela au point de départ le plus minimal possible.

En particulier, que faudrait-il changer pour soutenir cela ?...

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

Le décorateur @api_view est implémenté au-dessus de la vue basée sur les classes APIView , donc une question encore plus minime est : que devrions-nous changer pour prendre en charge quelque chose comme ça ?...

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

Il y a tout un tas de choses différentes à considérer là-bas, par exemple...

  • L'authentification/les autorisations sont probablement une opération ORM de synchronisation pour le moment.
  • La limitation est probablement une opération de cache de synchronisation pour le moment.
  • Django prend-il en charge la lecture du corps de la requête en tant qu'opération asynchrone ? Comment aurions-nous besoin de lier cela avec l'analyse.

Mais laissons cela de côté pour le moment.

La documentation à venir de Django pour

Pour une vue basée sur les classes, cela signifie faire de sa méthode __call__() un async def
(pas son __init__() ou as_view() ).

Voici donc quelques questions encore plus simples :

  • A quoi ressemble un CBV asynchrone Django 3.1 ?
  • À quoi ressemblerait l'utilisation explicite du Request du framework REST dans une vue asynchrone Django (plutôt que d'envelopper avec @api_view). Quelles conditions y a-t-il pour les opérations sur l'instance de demande qui sont actuellement synchronisées ?
  • À quoi ressemblerait l'utilisation explicite du Response du framework REST dans une vue asynchrone Django (plutôt que d'envelopper avec @api_view). Existe-t-il des clauses restrictives sur les opérations de synchronisation ?

Commentaire le plus utile

+1 pour la prise en charge asynchrone dans DRF. Devrait être de la plus haute priorité !

Tous les 35 commentaires

Je laisse tomber ces commentaires non pas parce qu'ils sont problématiques, mais parce que je pense qu'ils font dérailler quelque peu le problème. Je ne veux pas vraiment avoir une conversation ici sur les mérites relatifs d'opter pour un framework natif asynchrone. Il existe d'excellentes options et une prise en charge croissante d'un écosystème asynchrone plus large dont l'encodage joue également un rôle dans le développement.

Django commence à acquérir une prise en charge asynchrone intégrée. C'est utile dans certains cas limités, et il serait utile que le framework REST suive la prise en charge asynchrone de Django au fur et à mesure de sa croissance, même si cela s'accompagne de mises en garde importantes à ce stade.

D'ailleurs. peut-être que https://github.com/hishnash/djangochannelsrestframework peut être une simple inspiration

Serait :heart: une sorte d'histoire de socket Web natif et officiel pour DRF qui permet la réutilisation des composants DRF existants.

Cela aiderait à positionner DRF comme une option pour plus de cas d'utilisation bidirectionnels en temps réel.

Celui-ci n'est pas strictement réservé aux vues, mais la vue async serait une condition préalable pour que cela soit possible. Je voulais depuis un certain temps pouvoir utiliser l'async dans mes sérialiseurs, pour l'accélérer lorsque j'ai des appels fortement liés aux E/S dans mes champs de sérialiseur personnalisés. Exemple

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

Pour cet exemple simple, nous ne gagnerions pas beaucoup, mais parfois, il existe de nombreux champs personnalisés de ce type, ou même parfois je sérialise de nombreuses voitures dans un sérialiseur imbriqué. Dans ces cas, j'imagine qu'il y aurait beaucoup à gagner.

Voici quelques autres cas d'utilisation qui tirent parti de divers degrés de prise en charge asynchrone :

  • Service avec un backend non relationnel (par exemple DynamoDB) qui évite la complexité/les problèmes asynchrones ORM
  • Scatter rassemble les interfaces d'API vers d'autres services (pas de base de données propre)
  • Poster des signaux de sauvegarde pour la mise en cache, l'invalidation du cache, la messagerie (kafka, sqs, etc.)

Exprimant mon intérêt à voir cela se produire!

J'ai vu la référence pour fastapi et DRF, et fastapi est plus rapide.

J'adorerais m'en tenir à DRF car je suis à l'aise avec Django et je n'ai pas beaucoup de temps pour apprendre fastapi afin de fournir rapidement une API performante.

Je suis également très intéressé par le support asynchrone dans Django et DRF. Notre cas d'utilisation est assez courant je crois. Nous effectuons des appels réseau IO au sein de nos modèles ou sérialiseurs Django vers une API externe.

Nous essayons d'effectuer la plupart de ces appels dans Celery, mais étant donné que ces appels ne nécessitent pas de calculs intensifs, il serait plus simple de les conserver dans Django lui-même.

@hadim Vous devriez déjà pouvoir le faire avec une vue Django 3.1. Utilisez httpx pour passer les appels réseau.

Vous n'avez même pas besoin d'utiliser ASGI. Définissez simplement une vue def asynchrone et exécutez-la normalement sous WSGI, et Django s'occupe du reste.

Cela semble être le cas d'utilisation principal.

Ce n'est pas clair ce que DRF doit ajouter ? ??

@carltongibson Je ne suis pas sûr que le @api_view et les méthodes dans ViewSet avec le décorateur @action puissent être définis comme async prêts @api_view emploi. C'est pourquoi il pourrait y avoir du travail afin d'implémenter l'async dans DRF pour ces vues.

@TheBlusky Je pense que vous vouliez mentionner @carltongibson. ??

@TheBlusky ah, oui ViewSet... point juste.

Existe-t-il des travaux sur ce sujet ? Y a-t-il un moyen d'aider à ce sujet?

@TheBlusky, vous pouvez expérimenter ce qui est nécessaire pour qu'un APIView fonctionne ici. C'est la première étape je pense.

Nous avons un cas d'utilisation consistant à télécharger un fichier à partir d'un fournisseur de stockage en nuage, puis à le transmettre au client, async serait également un bon choix pour cela.

Un bon point de départ serait le APIView dans la mise en œuvre de cette fonctionnalité. Essentiellement:

  • APIView __call__ doit être asynchrone.
  • Les décorateurs comme action doivent vérifier si la fonction décorée est asynchrone et renvoyer correctement.
  • Sérialiseurs asynchrones ?
  • Il existe également des travaux de bas niveau tels que s'assurer que l'objet Request dispose également de méthodes asynchrones pour recevoir le corps de la réponse.

Une chose qui serait vraiment utile pour cette discussion serait des _cas d'utilisation concrets_ où les gens démontrent un cas d'utilisation _réel_ où ils ont utilisé ou utiliseraient une vue asynchrone dans Django, ainsi que la motivation et les améliorations démontrables par rapport au maintien avec une vue de synchronisation régulière.

Mon cas d'utilisation pour cela est un flux dans lequel les appels RPC sont effectués depuis mon serveur Django vers RabbitMQ et jusqu'à Django et la réponse est envoyée au client qui a effectué l'appel HTTP Django. Ces messages RPC peuvent prendre un certain temps et nous ne voulons idéalement pas que notre application se bloque en attendant que la réponse RPC arrive dans Django. Nous aimerions utiliser la puissance de DRF aux côtés des points de terminaison d'API non bloquants et asynchrone et de tous les packages communautaires qui fonctionnent sur DRF, tels que la politique d'accès DRF. La politique d'accès DRF nous permettrait de contrôler les autorisations d'accès tout en maintenant notre application asynchrone.

Pour être clair : _right now_ ( sex out 16 19:41:37 -03 2020 ), DRF fournit des vues de synchronisation uniquement, même sur master . Et il n'y a pas encore de branche pour prendre en charge la vue asynchrone.

Avais-je bien compris ?

Seulement des pensées à ce stade.
@tomchristie, vous voudrez peut-être modifier le message initial et prendre le APIView comme point de départ pour supprimer la complexité supplémentaire du décorateur api_view .

Un statut à ce sujet ? Django3.1 et async sont toujours géniaux pour les vues non-ORM et la classe APIView serait bien d'avoir.

+1 pour la prise en charge asynchrone dans DRF. Devrait être de la plus haute priorité !

+1

Pourrions-nous s'il vous plaît avoir des commentaires qui ajoutent quelque chose à la discussion au lieu de spams ?
S'il s'agit de la plus haute priorité pour vous, vous avez probablement de l'expérience à ce sujet et devriez être en mesure d'aider et de contribuer.
Cela n'a pas besoin d'être grand.
Maintenant, s'il vous plaît, ajoutons de la valeur ici au lieu du bruit.

D'accord. Il y a un bouton spécifique ":+1:" sur l'interface utilisateur de Github si quelqu'un veut +1. Quelqu'un aurait pu rater ça et cela ne veut pas dire un spam. Cela dit, je suggère de supprimer les nouveaux commentaires avec "+1" et nous pouvons désormais nous détendre.

Revenons à la question principale. J'ai essayé de @async_to_sync sur la vue que je voulais asynchrone, puis j'ai creusé un peu avant d'abandonner et de revenir à ma carte principale sur l'entreprise.

Est-il acceptable d'autoriser simplement les méthodes CBV à être des async def s et le répartiteur interne décide d'appeler async_to_sync si nécessaire ?

Je pense que c'est mieux que de forcer les méthodes à être synchronisées et à gérer cela par elles-mêmes, car à l'avenir, je m'attends à ce que les méthodes asynchrones soient plus courantes que les méthodes de synchronisation.

Voyant que le propre CBV de Django n'a rien d'asynchrone, je ne vois toujours pas ce qui est nécessaire sur DRF pour le moment.
Quelqu'un a-t-il un exemple de CBV de Django prenant en charge l'async ?
Je pense que cela devrait être le point de départ avant même d'arriver à DRF car APIView hérite de django.views.generic.View .

Le statut sur CBV dans Django peut être trouvé sur les forums . Il y a un PR à sa fourchette ici .

Une fois qu'il y aura un support pour les CBV dans la branche master de Django, je serais heureux d'y jeter un œil de notre côté.

Voici mon implémentation naïve des CBV asynchrones dans DRF, j'espère que cela pourra vous aider.

    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
        """
        <strong i="6">@classmethod</strong>
        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

DRF prend-il en charge les vues asynchrones avec les appels ORM asynchrones ?

Non. DRF est une application Django, nous ne pouvons qu'attendre que Django la supporte.

        <strong i="6">@classmethod</strong>
        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

J'ai également dû ajouter async_view.csrf_exempt = True pour que cela fonctionne dans mon cas (j'utilise django-oauth-toolkit).

Salut! ??

Juste pour vérifier s'il y a eu des progrès dans la prise en charge des vues asynchrones de Django 3.1 jusqu'à présent ?

Voici notre cas d'utilisation :
Nous construisons une API REST qui nécessite une intégration avec des systèmes distants SSH. Nous exécutons ~3-5 commandes pour chaque appel d'API et chaque commande prend actuellement ~15s. Cela nous laisse un temps de réponse d'environ 45-60 s qui pourrait être réduit à 15 s si nous pouvions exécuter toutes ces commandes en parallèle.

Nous voulons obtenir quelque chose de similaire au script suivant, mais lors d'un appel API. Les vraies commandes ont été remplacées par un sleep et 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())

Nous avons également quelques appels ORM à effectuer dans la vue avant et après l'exécution des commandes SSH.

Nous utilisons actuellement un viewsets.ViewSet .

Hé, notre cas d'utilisation serait :

Pile : Django, DRF, ReactJS

Nous organisons des réunions liées à la conformité des entreprises avec un suivi de la présence et des sondages en temps réel. Par conséquent, ce serait fantastique si, en utilisant DRF, nous pouvions permettre à un modérateur de réunion de parcourir les décisions/points de l'ordre du jour simplement en les publiant l'un après l'autre. l'élément publié apparaîtra via REST sur le navigateur des participants à la réunion et recueillera leur vote. instantanément le modérateur verrait les résultats arriver jusqu'à ce que tous les gens aient voté. avec la synchronisation, cela signifierait BEAUCOUP d'appels de contrôle fréquents, toujours avec quelques secondes de retard probablement.

@patroqueeet votre cas d'utilisation me fait penser aux websockets, je ne vois pas en quoi l'implémentation de vues asynchrones dans DRF aiderait 🤔

@patroqueeet votre cas d'utilisation me fait penser aux websockets, je ne vois pas en quoi l'implémentation de vues asynchrones dans DRF aiderait 🤔

ouais, nouveau domaine pour moi. je lis déjà toute la journée des docu/tutoriels de sockets web/vues asynchrones déjà... comme je n'ai pas encore de sockets web en place mais du code DRF géant, j'aurais aimé utiliser ce que nous avons et ne pas développer davantage l'écosystème des libs ...

@patroqueeet Ma solution pour les websockets + django est la suivante :
Django (backend de synchronisation) -> RabbitMQ (bus de messages) -> Aiohttp (backend asynchrone, principalement pour les connexions Websocket)

  1. Connexion de l'interface utilisateur à Aiohttp avec JWT ou cookie de session.
  2. Aiohttp kick API interne spéciale de Django pour l'authentification, réponse Djange avec instance utilisateur.
  3. Aiohtth stocke la paire [user_id, websocket_connection] en mémoire.
  4. Les fils de discussion de Django envoyant des messages à RabbitMq.
  5. Aiohttp écoute les files d'attente AMQP et obtient les messages.
  6. Aiohttp recherchant user_id dans la liste des canaux de socket Web ouverts et des messages push dans ces canaux.

Solution éprouvée par le temps et une bonne charge. En fait, un processus d'Aiohttp peut gérer des milliers de connexions WebSocket.

@Skorpyon a vérifié Aiohttp... très joli. mais après avoir lu environ 20 articles sur django/websocket/async et appris sur http, starlette, licorne, Daphne et al hier toute la journée. Je vais maintenant commencer à faire un prototypage rapide basé sur 3.1 de ce type ici https://alex-oleshkevich.medium.com/websockets-in-django-3-1-73de70c5c1ba (suivant la recommandation de @Crocmagnon) - J'aime le simplicité et je peux utiliser autant de ma pile que possible. probablement en utilisant des sérialiseurs DRF pour rendre et analyser le JSON... Voyons à quelle vitesse je peux échouer (me faisant apprendre encore plus vite) :)

Cette page vous a été utile?
0 / 5 - 0 notes