Comment construire une barre de progression pour le Web avec Django et Céleri

La complexité surprenante de faire quelque chose qui, à première vue, est ridiculement simple

Photo de Patrick Fore sur Unsplash

Les barres de progression sont l’un des composants les plus courants et les plus familiers de l’UI dans nos vies. Nous les voyons chaque fois que nous téléchargeons un fichier, installons un logiciel ou attachons quelque chose à un courrier électronique. Ils vivent dans nos navigateurs, sur nos téléphones et même sur nos téléviseurs.

Et pourtant, faire une bonne barre de progression est une tâche étonnamment complexe!

Dans cet article, je vais décrire tous les éléments permettant de créer une barre de progression de la qualité pour le Web. J'espère qu'à la fin, vous aurez une bonne compréhension de tout ce que vous devez créer vous-même.

Cet article décrit tout ce que j’ai dû apprendre (et certaines choses que je n’ai pas fait!) Pour faire céleri-progress, une bibliothèque qui facilite, espérons-le, l’ajout de barres de progression sans dépendance à vos applications Django / Celery.

Cela dit, la plupart des concepts de ce billet doivent être traduits dans toutes les langues et tous les environnements. Par conséquent, même si vous n’utilisez pas Python, vous pourrez probablement apprendre quelque chose de nouveau.

Pourquoi des barres de progression?

Cela peut sembler évident, mais pour l’écarter - pourquoi utilisons-nous des barres de progression?

La raison principale est de fournir aux utilisateurs des commentaires sur quelque chose qui prend plus de temps que d'habitude. Selon kissmetrics, 40% des personnes abandonnent un site web dont le téléchargement nécessite plus de 3 secondes! Et bien que vous puissiez utiliser quelque chose comme un bouton fléché pour aider à atténuer cette attente, un moyen éprouvé de communiquer avec vos utilisateurs en attendant que quelque chose se produise consiste à utiliser une barre de progression.

En règle générale, les barres de progression sont excellentes chaque fois que quelque chose prend plus de quelques secondes et vous pouvez raisonnablement estimer sa progression dans le temps.

Les barres de progression peuvent être utilisées pour montrer le statut de quelque chose et son résultat

Quelques exemples incluent:

  • Lorsque votre application est chargée pour la première fois (si le chargement est long)
  • Lors du traitement d'une importation de données volumineuse
  • Lors de la préparation d'un fichier pour le téléchargement
  • Lorsque l'utilisateur est dans une file d'attente, il attend que sa demande soit traitée

Les composants d'une barre de progression

Bon, avec ça, voyons comment construire ces choses!

C’est juste un petit bar qui se remplit sur un écran. Comment pourrait-il être compliqué?

En fait, tout à fait!

Les composants suivants font généralement partie de toute implémentation de barre de progression:

  1. Un frontal, qui comprend généralement une représentation visuelle de la progression et (éventuellement) un statut textuel.
  2. Un backend qui fera réellement le travail que vous souhaitez surveiller.
  3. Un ou plusieurs canaux de communication pour que le client final transfère le travail au serveur.
  4. Un ou plusieurs canaux de communication pour le serveur afin de communiquer les progrès au serveur.

Nous pouvons immédiatement voir une source inhérente de complexité. Nous voulons tous les deux travailler dans le backend et montrer que le travail se passe dans le backend. Cela signifie immédiatement que nous allons impliquer plusieurs processus qui doivent interagir les uns avec les autres de manière asynchrone.

Ces canaux de communication sont l’essentiel de la complexité. Dans un projet Django relativement standard, le navigateur frontal peut soumettre une demande HTTP AJAX (JavaScript) à l'application Web principale (Django). Cela pourrait ensuite transmettre cette demande à la file de tâches (Celery) via un courtier de messages (RabbitMQ / Redis). Ensuite, il faut que tout se passe à l'envers pour que les informations reviennent au début!

L'ensemble du processus pourrait ressembler à ceci:

La grande image de tout ce qui est nécessaire pour faire une bonne barre de progression

Explorons toutes ces composantes et voyons comment elles fonctionnent dans un exemple concret.

Le front end

Le front-end est certainement la partie la plus facile de la barre de progression. Avec seulement quelques petites lignes de HTML / CSS, vous pouvez rapidement créer une barre horizontale d'aspect décent à l'aide des attributs de couleur et de largeur de l'arrière-plan. Ajoutez un peu de JavaScript pour le mettre à jour et vous êtes prêt à partir!

Le backend

Le backend est également simple. Il s’agit essentiellement d’un code à exécuter sur votre serveur pour effectuer le travail que vous souhaitez suivre. C’est généralement écrit dans la pile d’applications que vous utilisez (dans ce cas, Python et Django). Voici une version trop simplifiée de ce à quoi le backend pourrait ressembler:

def do_work (self, list_of_work):
    pour work_item dans list_of_work:
        do_work_item (work_item)
    retour 'le travail est terminé'

Faire le travail

Donc, nous avons notre barre de progression avant et notre chef de travail. Et après?

Eh bien, nous n’avons en fait rien dit sur la manière dont ce travail sera lancé. Alors commençons par là.

La mauvaise façon de le faire: dans l'application Web

Dans un flux de travail ajax typique, cela fonctionnerait de la manière suivante:

  1. Le serveur frontal lance une demande à une application Web
  2. L'application Web fonctionne dans la demande
  3. L'application Web renvoie une réponse lorsque vous avez terminé

Dans une vue Django, cela ressemblerait à ceci:

def my_view (demande):
    faire du travail()
    return HttpResponse ('travail effectué!')

La mauvaise façon: appeler la fonction depuis la vue

Le problème ici est que la fonction do_work peut faire beaucoup de travail qui prend beaucoup de temps (sinon, cela n'aurait aucun sens d'ajouter une barre de progression pour cela).

Faire beaucoup de travail dans une vue est généralement considéré comme une mauvaise pratique pour plusieurs raisons, notamment:

  • Vous créez une expérience utilisateur médiocre, car les utilisateurs doivent attendre la fin des longues requêtes.
  • Vous ouvrez votre site à des problèmes de stabilité potentiels avec de nombreuses demandes de travail de longue durée (pouvant être déclenchées de manière malveillante ou accidentelle).

Pour ces raisons, entre autres, nous avons besoin d’une meilleure approche à cet égard.

La meilleure façon: les files de tâches asynchrones (alias Celery)

La plupart des infrastructures Web modernes ont créé des files d'attente de tâches asynchrones pour résoudre ce problème. En Python, le plus commun est le céleri. Dans Rails, il y a Sidekiq (entre autres).

Les détails entre ceux-ci varient, mais leurs principes fondamentaux sont les mêmes. Fondamentalement, au lieu de travailler dans une requête HTTP qui peut prendre un temps arbitraire - et être déclenché à une fréquence arbitraire - vous vous en tenez à une file d'attente et vous avez des processus en arrière-plan - souvent appelés ouvriers - qui récupèrent les travaux et les exécutent .

Cette architecture asynchrone présente plusieurs avantages, notamment:

  • Ne pas travailler longtemps dans les processus Web
  • Activation de la limitation du débit du travail effectué - le travail peut être limité par le nombre de processus de travail disponibles
  • Activation du travail sur des machines optimisées, par exemple des machines avec un nombre élevé de processeurs

La mécanique des tâches asynchrones

Les mécanismes de base d'une architecture asynchrone sont relativement simples et impliquent trois composants principaux: le ou les clients, le ou les agents et le courtier de messages.

Le client est principalement responsable de la création de nouvelles tâches. Dans notre exemple, le client est l'application Django, qui crée des tâches sur la saisie de l'utilisateur via une requête Web.

Les travailleurs sont les processus réels qui font le travail. Ce sont nos ouvriers de céleri. Vous pouvez avoir un nombre arbitraire de travailleurs s'exécutant sur le nombre de machines, ce qui permet une haute disponibilité et une mise à l'échelle horizontale du traitement des tâches.

Le client et la file d'attente des tâches se parlent via un courtier de messages, qui est chargé d'accepter les tâches du (des) client (s) et de les transmettre au (x) travailleur (s). RabbitMQ est le courtier de messages le plus courant pour Celery, bien que Redis soit également un courtier de messages complet et couramment utilisé.

Flux de travail de base consistant à transmettre des messages à un processus de travail asynchrone

Lors de la création d'une application céleri standard, vous développerez généralement le code du client et de l'opérateur, mais le courtier de messages sera un élément d'infrastructure que vous devrez simplement mettre en place (et au-delà de ce que vous pouvez ignorer).

Un exemple

Bien que tout cela semble assez compliqué, le céleri fait du bon travail en le rendant assez facile pour nous via de belles abstractions de programmation.

Pour convertir notre fonction de travail en quelque chose qui peut être exécuté de manière asynchrone, il suffit d’ajouter un décorateur spécial:

de la tâche d'importation de céleri
# ce décorateur est tout ce qui est nécessaire pour dire au céleri que c’est un
# tâche ouvrière
@tâche
def do_work (self, list_of_work):
    pour work_item dans list_of_work:
        do_work_item (work_item)
    retour 'le travail est terminé'

Annoter une tâche à appeler à partir de céleri

De même, appeler la fonction de manière asynchrone à partir du client Django est tout aussi simple:

def my_view (demande):
    # l'appel .delay () ici est tout ce qui est nécessaire
    # pour convertir la fonction à appeler de manière asynchrone
    do_work.delay ()
    # on ne peut plus dire 'travail accompli' ici
    # parce que tout ce que nous avons fait a été de le lancer
    return HttpResponse ('le travail a commencé!')

Appeler le travail de manière asynchrone

Avec seulement quelques lignes de code supplémentaires, nous avons converti notre travail en architecture asynchrone! Tant que vos processus de travailleur et de courtier sont configurés et en cours d'exécution, cela devrait fonctionner.

Suivre les progrès

Très bien, alors nous avons enfin notre tâche en arrière-plan. Mais maintenant, nous voulons suivre les progrès accomplis. Alors, comment ça marche, exactement?

Nous devons encore faire quelques choses. Nous aurons d’abord besoin d’un moyen de suivre les progrès au sein du poste de travailleur. Nous devrons ensuite communiquer ces progrès jusqu’à notre interface afin de pouvoir mettre à jour la barre de progression sur la page. Encore une fois, cela finit par être un peu plus compliqué que vous ne le pensez!

Utilisation d'un objet d'observation pour suivre les progrès de l'ouvrier

Les lecteurs des modèles de conception fondamentaux de Gang of Four peuvent être familiarisés avec le modèle d’observateur. Le modèle d'observateur typique comprend un sujet qui suit l'état, ainsi qu'un ou plusieurs observateurs qui font quelque chose en réponse à l'état. Dans notre scénario de progression, le sujet est le processus / fonction de travail qui effectue le travail, et l'observateur est ce qui va suivre les progrès.

Il existe de nombreuses façons de lier le sujet et l'observateur, mais la plus simple consiste à simplement passer l'observateur en argument à la fonction effectuant le travail.

Cela ressemble à quelque chose comme ça:

@tâche
def do_work (self, list_of_work, progress_observer):
    total_work_to_do = len (list_of_work)
    pour i, work_item dans énumérer (list_of_work):
        do_work_item (work_item)
        # dire à l'observateur des progrès combien d'éléments sur le total
        # nous avons traité
        progress_observer.set_progress (i, total_work_to_do)
    retour 'le travail est terminé'

Utilisation d'un observateur pour surveiller l'avancement des travaux

Tout ce que nous avons à faire est de passer un Progress_observer valide et voilà, nos progrès seront suivis!

Obtenir des progrès chez le client

Vous pensez peut-être «attendez une minute… vous venez d’appeler une fonction appelée set_progress, vous n’avez vraiment rien fait!»

Vrai! Alors, comment cela fonctionne-t-il réellement?

Rappelez-vous - notre objectif est d’obtenir ces informations sur les progrès jusqu’à la page Web afin que nous puissions montrer à nos utilisateurs ce qui se passe. Mais le suivi des progrès se produit tout au long du processus de travail! Nous sommes maintenant confrontés au même problème que nous avions avec le transfert de la tâche asynchrone plus tôt.

Heureusement, Celery fournit également un mécanisme permettant de transmettre des messages au client. Cela se fait via un mécanisme appelé résultat backends, et, à l'instar des courtiers, vous avez le choix entre plusieurs backends différents. RabbitMQ et Redis peuvent tous deux être utilisés en tant que courtiers et moteurs de résultats et constituent des choix raisonnables, bien qu’il n’existe techniquement aucun couplage entre le courtier et le moteur de résultats.

Quoi qu’il en soit, comme les courtiers, les détails ne sont pas abordés à moins que vous ne fassiez quelque chose de très avancé. Mais le fait est que vous collez le résultat de la tâche quelque part (avec l’ID unique de la tâche), puis les autres processus peuvent obtenir des informations sur les tâches par ID en le demandant au backend.

Dans le céleri, cela se résume assez bien via l'état associé à la tâche. L'état nous permet de définir un statut global, ainsi que d'attacher des métadonnées arbitraires à la tâche. C'est un endroit parfait pour stocker nos progrès actuels et totaux.

Fixer l'état

task.update_state (
    state = PROGRESS_STATE,
    meta = {'current': actuel, 'total': total}
)

Lire l'état

depuis celery.result import AsyncResult
resultat = AsyncResult (id_tâche)
print (result.state) # sera défini sur PROGRESS_STATE print (result.info) # les métadonnées seront ici

Obtenir des mises à jour de progrès au début

Maintenant que nous pouvons obtenir des mises à jour de progression des travailleurs / tâches vers n'importe quel autre client, la dernière étape consiste simplement à transmettre ces informations au serveur frontal et à les afficher à l'utilisateur.

Si vous voulez avoir envie, vous pouvez utiliser quelque chose comme websockets pour le faire en temps réel. Mais la version la plus simple consiste à interroger de temps en temps une URL pour vérifier sa progression. Nous pouvons simplement servir les informations de progression sous forme de code JSON via une vue et un processus Django et les restituer côté client.

Vue Django:

def get_progress (request, id_tâche):
    resultat = AsyncResult (id_tâche)
    response_data = {
        'state': result.state,
        'details': self.result.info,
    }
    retourne HttpResponse (
        json.dumps (response_data),
        content_type = 'application / json'
    )

Vue Django pour retourner la progression en JSON.

Code JavaScript:

function updateProgress (progressUrl) {
    fetch (progressUrl) .then (function (response) {
        response.json (). then (function (data) {
            // met à jour les composants d'interface utilisateur appropriés
            setProgress (data.state, data.details);
            // et recommencez toutes les demi-secondes
            setTimeout (updateProgress, 500, progressUrl);
        });
    });
}

Code Javascript pour interroger les progrès et mettre à jour l'interface utilisateur.

Mettre tous ensemble

Cela a donné pas mal de détails sur ce qui est, à première vue, une partie très simple et quotidienne de nos vies avec des ordinateurs! J'espère que vous avez appris quelque chose.

Si vous avez besoin d’un moyen simple de créer des barres de progression pour vos applications Django / céleri, vous pouvez consulter célery-progress, une bibliothèque que j’ai écrite pour vous aider à simplifier la tâche. Il en existe également une démonstration en action sur Build with Django.

Merci d'avoir lu! Si vous souhaitez être averti chaque fois que je publie un contenu comme celui-ci sur la création d’objets avec Python et Django, veuillez vous inscrire pour recevoir les mises à jour ci-dessous!

Publié à l'origine à buildwithdjango.com.