Tareas as铆ncronas en Django con Redis y Celery

    Introducci贸n

    En este tutorial, proporcionar茅 una comprensi贸n general de por qu茅 las colas de mensajes de apio son valiosas junto con c贸mo utilizar el apio junto con Redis en una aplicaci贸n Django. Para demostrar los detalles de la implementaci贸n, construir茅 una aplicaci贸n de procesamiento de im谩genes minimalista que genera miniaturas de las im谩genes enviadas por los usuarios.

    Se cubrir谩n los siguientes temas:

    • Antecedentes de las colas de mensajes con apio y redis
    • Configuraci贸n del desarrollador local con Django, Celery y Redis
    • Creaci贸n de miniaturas de im谩genes dentro de una tarea de apio
    • Implementar en un servidor Ubuntu

    El c贸digo para este ejemplo se puede encontrar en GitHub junto con las instrucciones de instalaci贸n y configuraci贸n si solo desea saltar directamente a una aplicaci贸n funcionalmente completa; de lo contrario, durante el resto del art铆culo, lo guiar茅 a trav茅s de c贸mo construir todo desde cero.

    Antecedentes de las colas de mensajes con apio y redis

    Celery es un paquete de software de cola de tareas basado en Python que permite la ejecuci贸n de cargas de trabajo computacionales as铆ncronas impulsadas por informaci贸n contenida en mensajes que se producen en el c贸digo de la aplicaci贸n (Django en este ejemplo) destinado a una cola de tareas de Celery. El apio tambi茅n se puede utilizar para ejecutar tareas repetibles, peri贸dicas (es decir, programadas), pero ese no ser谩 el enfoque de este art铆culo.

    El apio se utiliza mejor junto con una soluci贸n de almacenamiento que a menudo se denomina agente de mensajes. Un corredor de mensajes com煤n que se utiliza con el apio es Redis, que es un almac茅n de datos de valor clave en memoria de alto rendimiento. Espec铆ficamente, Redis se usa para almacenar mensajes producidos por el c贸digo de la aplicaci贸n que describen el trabajo a realizar en la cola de tareas de Celery. Redis tambi茅n sirve como almacenamiento de los resultados de las colas de apio que luego son recuperados por los consumidores de la cola.

    Configuraci贸n del desarrollador local con Django, Celery y Redis

    Comenzar茅 con la parte m谩s dif铆cil primero, que es la instalaci贸n de Redis.

    Instalaci贸n de Redis en Windows

    • Descargue el archivo zip de Redis y descompr铆malo en alg煤n directorio
    • Busque el archivo llamado redis-server.exe y haga doble clic para iniciar el servidor en una ventana de comandos
    • Del mismo modo, busque otro archivo llamado redis-cli.exe y haga doble clic en 茅l para abrir el programa en una ventana de comando separada
    • Dentro de la ventana de comandos que ejecuta el cliente cli, pruebe para asegurarse de que el cliente puede hablar con el servidor emitiendo el comando pingy, si todo va bien, se PONGdebe devolver una respuesta de

    Instalaci贸n de Redis en Mac OSX / Linux

    • Descargue el archivo tarball de Redis y extr谩igalo en alg煤n directorio
    • Ejecute el archivo make con make installpara construir el programa
    • Abra una ventana de terminal y ejecute el redis-servercomando
    • En otra ventana de terminal, ejecuta redis-cli
    • Dentro de la ventana de la terminal que ejecuta el cliente cli, pruebe para asegurarse de que el cliente pueda hablar con el servidor emitiendo el comando pingy, si todo va bien, se PONGdebe devolver una respuesta de

    Instale Python Virtual Env y las dependencias

    Ahora puedo pasar a crear un entorno virtual Python3 e instalar los paquetes de dependencia necesarios para este proyecto.

    Para comenzar, crear茅 un directorio para albergar cosas llamado image_parroter y luego dentro de 茅l crear茅 mi entorno virtual. Todos los comandos de aqu铆 en adelante ser谩n solo del tipo Unix, pero la mayor铆a, si no todos, ser谩n iguales para un entorno Windows.

    $ mkdir image_parroter
    $ cd image_parroter
    $ python3 -m venv venv
    $ source venv/bin/activate
    

    Con el entorno virtual ahora activado, puedo instalar los paquetes de Python.

    (venv) $ pip install Django Celery redis Pillow django-widget-tweaks
    (venv) $ pip freeze > requirements.txt
    
    • Pillow es un paquete de Python no relacionado con el apio para el procesamiento de im谩genes que usar茅 m谩s adelante en este tutorial para demostrar un caso de uso del mundo real para las tareas del apio.
    • Django Widget Tweaks es un complemento de Django para proporcionar flexibilidad en c贸mo se procesan las entradas de formulario.

    Configuraci贸n del proyecto Django

    Continuando, creo un proyecto Django llamado image_parroter y luego una aplicaci贸n Django llamada thumbnailer.

    (venv) $ django-admin startproject image_parroter
    (venv) $ cd image_parroter
    (venv) $ python manage.py startapp thumbnailer
    

    En este punto, la estructura del directorio tiene el siguiente aspecto:

    $ tree -I venv
    .
    鈹斺攢鈹 image_parroter
        鈹溾攢鈹 image_parroter
        鈹偮犅 鈹溾攢鈹 __init__.py
        鈹偮犅 鈹溾攢鈹 settings.py
        鈹偮犅 鈹溾攢鈹 urls.py
        鈹偮犅 鈹斺攢鈹 wsgi.py
        鈹溾攢鈹 manage.py
        鈹斺攢鈹 thumbnailer
            鈹溾攢鈹 __init__.py
            鈹溾攢鈹 admin.py
            鈹溾攢鈹 apps.py
            鈹溾攢鈹 migrations
            鈹偮犅 鈹斺攢鈹 __init__.py
            鈹溾攢鈹 models.py
            鈹溾攢鈹 tests.py
            鈹斺攢鈹 views.py
    

    Para integrar Celery dentro de este proyecto de Django, agrego un nuevo m贸dulo image_parroter / image_parrroter / celery.py siguiendo las convenciones descritas en los documentos de Celery . Dentro de este nuevo m贸dulo de Python, importo el ospaquete y la Celeryclase del paquete de apio.

    El osm贸dulo se usa para asociar una variable de entorno Celery llamada DJANGO_SETTINGS_MODULEcon el m贸dulo de configuraci贸n del proyecto Django. Despu茅s de eso, creo una instancia de la Celeryclase para crear la celery_appvariable de instancia. Luego actualizo la configuraci贸n de la aplicaci贸n Celery con configuraciones que pronto colocar茅 en el archivo de configuraci贸n del proyecto Django identificable con un prefijo ‘CELERY_’. Finalmente, le digo a la celery_appinstancia reci茅n creada que descubra autom谩ticamente las tareas dentro del proyecto.

    El m贸dulo celery.py completo se muestra a continuaci贸n:

    # image_parroter/image_parroter/celery.py
    
    import os
    from celery import Celery
    
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'image_parroter.settings')
    
    celery_app = Celery('image_parroter')
    celery_app.config_from_object('django.conf:settings', namespace="CELERY")
    celery_app.autodiscover_tasks()
    

    Ahora, en el m贸dulo settings.py del proyecto, en la parte inferior, defino una secci贸n para la configuraci贸n de apio y agrego la configuraci贸n que ve a continuaci贸n. Estas configuraciones le dicen a Celery que use Redis como el intermediario de mensajes, as铆 como d贸nde conectarse a 茅l. Tambi茅n le dicen a Celery que espere que los mensajes se pasen de un lado a otro entre las colas de tareas de Celery y el agente de mensajes de Redis para que est茅n en el tipo mime de application / json.

    # image_parroter/image_parroter/settings.py
    
    ... skipping to the bottom
    
    # celery
    CELERY_BROKER_URL = 'redis://localhost:6379'
    CELERY_RESULT_BACKEND = 'redis://localhost:6379'
    CELERY_ACCEPT_CONTENT = ['application/json']
    CELERY_RESULT_SERIALIZER = 'json'
    CELERY_TASK_SERIALIZER = 'json'
    

    A continuaci贸n, necesito asegurarme de que la aplicaci贸n de apio creada y configurada previamente se inyecte en la aplicaci贸n Django cuando se ejecute. Esto se hace importando la aplicaci贸n Celery dentro del script principal __init__.py del proyecto Django y registr谩ndolo expl铆citamente como un s铆mbolo de espacio de nombres dentro del paquete “image_parroter” de Django.

    # image_parroter/image_parroter/__init__.py
    
    from .celery import celery_app
    
    __all__ = ('celery_app',)
    

    Contin煤o siguiendo las convenciones sugeridas agregando un nuevo m贸dulo llamado tasks.py dentro de la aplicaci贸n “thumbnailer”. Dentro del m贸dulo tasks.py, importo el shared_tasksdecorador de funciones y lo uso para definir una funci贸n de tarea de apio llamada adding_task, como se muestra a continuaci贸n.

    # image_parroter/thumbnailer/tasks.py
    
    from celery import shared_task
    
    @shared_task
    def adding_task(x, y):
        return x + y
    

    Por 煤ltimo, necesito agregar la aplicaci贸n de miniaturas a la lista del INSTALLED_APPSm贸dulo settings.py del proyecto image_parroter. Mientras estoy all铆, tambi茅n deber铆a agregar la aplicaci贸n “widget_tweaks” que se usar谩 para controlar la representaci贸n de la entrada del formulario que usar茅 m谩s adelante para permitir que los usuarios carguen archivos.

    # image_parroter/image_parroter/settings.py
    
    ... skipping to the INSTALLED_APPS
    
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'thumbnailer.apps.ThumbnailerConfig',
        'widget_tweaks',
    ]
    

    Ahora puedo probar cosas usando algunos comandos simples en tres terminales.

    En una terminal necesito tener el servidor redis ejecut谩ndose, as铆:

    $ redis-server
    48621:C 21 May 21:55:23.706 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    48621:C 21 May 21:55:23.707 # Redis version=4.0.8, bits=64, commit=00000000, modified=0, pid=48621, just started
    48621:C 21 May 21:55:23.707 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
    48621:M 21 May 21:55:23.708 * Increased maximum number of open files to 10032 (it was originally set to 2560).
                    _._                                                  
               _.-``__ ''-._                                             
          _.-``    `.  `_.  ''-._           Redis 4.0.8 (00000000/0) 64 bit
      .-`` .-```.  ```/    _.,_ ''-._                                   
     (    '      ,       .-`  | `,    )     Running in standalone mode
     |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
     |    `-._   `._    /     _.-'    |     PID: 48621
      `-._    `-._  `-./  _.-'    _.-'                                   
     |`-._`-._    `-.__.-'    _.-'_.-'|                                  
     |    `-._`-._        _.-'_.-'    |           http://redis.io        
      `-._    `-._`-.__.-'_.-'    _.-'                                   
     |`-._`-._    `-.__.-'    _.-'_.-'|                                  
     |    `-._`-._        _.-'_.-'    |                                  
      `-._    `-._`-.__.-'_.-'    _.-'                                   
          `-._    `-.__.-'    _.-'                                       
              `-._        _.-'                                           
                  `-.__.-'                                               
    
    48621:M 21 May 21:55:23.712 # Server initialized
    48621:M 21 May 21:55:23.712 * Ready to accept connections
    

    En una segunda terminal, con una instancia activa del entorno virtual Python instalado previamente, en el directorio del paquete ra铆z del proyecto (el mismo que contiene el m贸dulo manage.py) lanzo el programa celery.

    (venv) $ celery worker -A image_parroter --loglevel=info
     
     -------------- [email聽protected] v4.3.0 (rhubarb)
    ---- **** ----- 
    --- * ***  * -- Darwin-18.5.0-x86_64-i386-64bit 2019-05-22 03:01:38
    -- * - **** --- 
    - ** ---------- [config]
    - ** ---------- .> app:         image_parroter:0x110b18eb8
    - ** ---------- .> transport:   redis://localhost:6379//
    - ** ---------- .> results:     redis://localhost:6379/
    - *** --- * --- .> concurrency: 8 (prefork)
    -- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
    --- ***** ----- 
     -------------- [queues]
                    .> celery           exchange=celery(direct) key=celery
                    
    
    [tasks]
      . thumbnailer.tasks.adding_task
    

    En la tercera y 煤ltima terminal, nuevamente con el entorno virtual Python activo, puedo iniciar el shell Django Python y probar mi adding_task, as铆:

    (venv) $ python manage.py shell
    Python 3.6.6 |Anaconda, Inc.| (default, Jun 28 2018, 11:07:29) 
    >>> from thumbnailer.tasks import adding_task
    >>> task = adding_task.delay(2, 5)
    >>> print(f"id={task.id}, state={task.state}, status={task.status}") 
    id=86167f65-1256-497e-b5d9-0819f24e95bc, state=SUCCESS, status=SUCCESS
    >>> task.get()
    7
    

    Tenga en cuenta el uso del .delay(...)m茅todo en el adding_taskobjeto. Esta es la forma com煤n de pasar los par谩metros necesarios al objeto de tarea con el que se est谩 trabajando, as铆 como de iniciar su env铆o al intermediario de mensajes y la cola de tareas. El resultado de llamar al .delay(...)m茅todo es un valor de retorno similar a una promesa del tipo celery.result.AsyncResult. Este valor de retorno contiene informaci贸n como la identificaci贸n de la tarea, su estado de ejecuci贸n y el estado de la tarea junto con la capacidad de acceder a cualquier resultado producido por la tarea a trav茅s del .get()m茅todo como se muestra en el ejemplo.

    Creaci贸n de miniaturas de im谩genes dentro de una tarea de apio

    Ahora que la configuraci贸n de la placa de la caldera para integrar una instancia de Celery respaldada por Redis en la aplicaci贸n Django est谩 fuera del camino, puedo pasar a demostrar algunas funciones m谩s 煤tiles con la aplicaci贸n de miniaturas mencionada anteriormente.

    De vuelta en el m贸dulo tasks.py, importo la Imageclase del PILpaquete, luego agrego una nueva tarea llamada make_thumbnails, que acepta una ruta de archivo de imagen y una lista de dimensiones de ancho y alto de 2 tuplas para crear miniaturas.

    # image_parroter/thumbnailer/tasks.py
    import os
    from zipfile import ZipFile
    
    from celery import shared_task
    from PIL import Image
    
    from django.conf import settings
    
    @shared_task
    def make_thumbnails(file_path, thumbnails=[]):
        os.chdir(settings.IMAGES_DIR)
        path, file = os.path.split(file_path)
        file_name, ext = os.path.splitext(file)
    
        zip_file = f"{file_name}.zip"
        results = {'archive_path': f"{settings.MEDIA_URL}images/{zip_file}"}
        try:
            img = Image.open(file_path)
            zipper = ZipFile(zip_file, 'w')
            zipper.write(file)
            os.remove(file_path)
            for w, h in thumbnails:
                img_copy = img.copy()
                img_copy.thumbnail((w, h))
                thumbnail_file = f'{file_name}_{w}x{h}.{ext}'
                img_copy.save(thumbnail_file)
                zipper.write(thumbnail_file)
                os.remove(thumbnail_file)
    
            img.close()
            zipper.close()
        except IOError as e:
            print(e)
    
        return results
    

    La tarea de miniaturas anterior simplemente carga el archivo de imagen de entrada en una instancia de Pillow Image, luego recorre la lista de dimensiones que se pas贸 a la tarea creando una miniatura para cada una, agregando cada miniatura a un archivo zip mientras tambi茅n limpia los archivos intermedios. Se devuelve un diccionario simple que especifica la URL desde la que se puede descargar el archivo zip de miniaturas.

    Con la tarea de apio definida, paso a construir las vistas de Django para ofrecer una plantilla con un formulario de carga de archivos.

    Para comenzar, le doy al proyecto Django una MEDIA_ROOTubicaci贸n donde pueden residir los archivos de imagen y los archivos zip (us茅 esto en la tarea de ejemplo anterior) y especifico desde MEDIA_URLd贸nde se puede servir el contenido. En el m贸dulo image_parroter / settings.py agrego el MEDIA_ROOT, MEDIA_URL, IMAGES_DIRlugares ajustes a continuaci贸n proporcionan la l贸gica para crear estas ubicaciones si no existen.

    # image_parroter/settings.py
    
    ... skipping down to the static files section
    
    # Static files (CSS, JavaScript, Images)
    # https://docs.djangoproject.com/en/2.2/howto/static-files/
    
    STATIC_URL = '/static/'
    MEDIA_URL = '/media/'
    
    MEDIA_ROOT = os.path.abspath(os.path.join(BASE_DIR, 'media'))
    IMAGES_DIR = os.path.join(MEDIA_ROOT, 'images')
    
    if not os.path.exists(MEDIA_ROOT) or not os.path.exists(IMAGES_DIR):
        os.makedirs(IMAGES_DIR)
    

    Dentro del m贸dulo thumbnailer / views.py, importo la django.views.Viewclase y la uso para crear una HomeViewclase que contiene m茅todos gety post, como se muestra a continuaci贸n.

    El getm茅todo simplemente devuelve una plantilla home.html, que se crear谩 en breve, y le entrega FileUploadFormun ImageFieldcampo compuesto por un campo como se ve arriba de la HomeViewclase.

    El postm茅todo construye el FileUploadFormobjeto usando los datos enviados en la solicitud, verifica su validez, luego, si es v谩lido, guarda el archivo cargado en el IMAGES_DIRe inicia una make_thumbnailstarea mientras toma la tarea idy el estado para pasar a la plantilla, o devuelve el formulario con su errores en la plantilla home.html.

    # thumbnailer/views.py
    
    import os
    
    from celery import current_app
    
    from django import forms
    from django.conf import settings
    from django.http import JsonResponse
    from django.shortcuts import render
    from django.views import View
    
    from .tasks import make_thumbnails
    
    class FileUploadForm(forms.Form):
        image_file = forms.ImageField(required=True)
    
    class HomeView(View):
        def get(self, request):
            form = FileUploadForm()
            return render(request, 'thumbnailer/home.html', { 'form': form })
        
        def post(self, request):
            form = FileUploadForm(request.POST, request.FILES)
            context = {}
    
            if form.is_valid():
                file_path = os.path.join(settings.IMAGES_DIR, request.FILES['image_file'].name)
    
                with open(file_path, 'wb+') as fp:
                    for chunk in request.FILES['image_file']:
                        fp.write(chunk)
    
                task = make_thumbnails.delay(file_path, thumbnails=[(128, 128)])
    
                context['task_id'] = task.id
                context['task_status'] = task.status
    
                return render(request, 'thumbnailer/home.html', context)
    
            context['form'] = form
    
            return render(request, 'thumbnailer/home.html', context)
    
    
    class TaskView(View):
        def get(self, request, task_id):
            task = current_app.AsyncResult(task_id)
            response_data = {'task_status': task.status, 'task_id': task.id}
    
            if task.status == 'SUCCESS':
                response_data['results'] = task.get()
    
            return JsonResponse(response_data)
    

    Debajo de la HomeViewclase, he colocado una TaskViewclase que se utilizar谩 a trav茅s de una solicitud AJAX para verificar el estado de la make_thumbnailstarea. Aqu铆 notar谩 que import茅 el current_appobjeto del paquete de apio y lo us茅 para recuperar el AsyncResultobjeto de la tarea asociado con el task_idde la solicitud. Creo un response_datadiccionario del estado y la identificaci贸n de la tarea, luego, si el estado indica que la tarea se ha ejecutado correctamente, obtengo los resultados llamando al get()m茅todo del AsynchResultobjeto y asign谩ndolo a la resultsclave del response_dataque se devolver谩 como JSON al solicitante HTTP.

    Antes de que pueda crear la interfaz de usuario de la plantilla, necesito asignar las clases de vistas de Django anteriores a algunas URL sensibles. Comienzo agregando un m贸dulo urls.py dentro de la aplicaci贸n de miniaturas y defino las siguientes URL:

    # thumbnailer/urls.py
    
    from django.urls import path
    
    from . import views
    
    urlpatterns = [
      path('', views.HomeView.as_view(), name="home"),
      path('task/<str:task_id>/', views.TaskView.as_view(), name="task"),
    ]
    

    Luego, en la configuraci贸n de la URL principal del proyecto, necesito incluir las URL de nivel de la aplicaci贸n, as铆 como tambi茅n informar sobre la URL de los medios, as铆:

    # image_parroter/urls.py
    
    from django.contrib import admin
    from django.urls import path, include
    from django.conf import settings
    from django.conf.urls.static import static
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('', include('thumbnailer.urls')),
    ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    

    A continuaci贸n, empiezo a crear una vista de plantilla simple para que un usuario env铆e un archivo de imagen, as铆 como para verificar el estado de las make_thumbnailstareas enviadas e iniciar una descarga de las miniaturas resultantes. Para empezar, necesito crear un directorio para albergar esta plantilla 煤nica dentro del directorio de miniaturas, de la siguiente manera:

    (venv) $ mkdir -p thumbnailer/templates/thumbnailer
    

    Luego, dentro de este directorio templates / thumbnailer agrego una plantilla llamada home.html. Dentro de home.html empiezo cargando las etiquetas de plantilla “widget_tweaks”, luego paso a definir el HTML importando un marco CSS llamado bulma CSS , as铆 como una biblioteca JavaScript llamada Axios.js . En el cuerpo de la p谩gina HTML proporciono un t铆tulo, un marcador de posici贸n para mostrar un mensaje de resultados en progreso y el formulario de carga del archivo.

    <!-- templates/thumbnailer/home.html -->
    {% load widget_tweaks %}
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Thumbnailer</title>
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
      <script src="https://cdn.jsdelivr.net/npm/vue"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
      <script defer src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></script>
    </head>
    <body>
      <nav class="navbar" role="navigation" aria-label="main navigation">
        <div class="navbar-brand">
          <a class="navbar-item" href="/">
            Thumbnailer
          </a>
        </div>
      </nav>
      <section class="hero is-primary is-fullheight-with-navbar">
        <div class="hero-body">
          <div class="container">
            <h1 class="title is-size-1 has-text-centered">Thumbnail Generator</h1>
            <p class="subtitle has-text-centered" id="progress-title"></p>
            <div class="columns is-centered">
              <div class="column is-8">
                <form action="{% url 'home' %}" method="POST" enctype="multipart/form-data">
                  {% csrf_token %}
                  <div class="file is-large has-name">
                    <label class="file-label">
                      {{ form.image_file|add_class:"file-input" }}
                      <span class="file-cta">
                        <span class="file-icon"><i class="fas fa-upload"></i></span>
                        <span class="file-label">Browse image</span>
                      </span>
                      <span id="file-name" class="file-name" 
                        style="background-color: white; color: black; min-width: 450px;">
                      </span>
                    </label>
                    <input class="button is-link is-large" type="submit" value="Submit">
                  </div>
                  
                </form>
              </div>
            </div>
          </div>
        </div>
      </section>
      <script>
      var file = document.getElementById('{{form.image_file.id_for_label}}');
      file.onchange = function() {
        if(file.files.length > 0) {
          document.getElementById('file-name').innerHTML = file.files[0].name;
        }
      };
      </script>
    
      {% if task_id %}
      <script>
      var taskUrl = "{% url 'task' task_id=task_id %}";
      var dots = 1;
      var progressTitle = document.getElementById('progress-title');
      updateProgressTitle();
      var timer = setInterval(function() {
        updateProgressTitle();
        axios.get(taskUrl)
          .then(function(response){
            var taskStatus = response.data.task_status
            if (taskStatus === 'SUCCESS') {
              clearTimer('Check downloads for results');
              var url = window.location.protocol + '//' + window.location.host + response.data.results.archive_path;
              var a = document.createElement("a");
              a.target="_BLANK";
              document.body.appendChild(a);
              a.style = "display: none";
              a.href = url;
              a.download = 'results.zip';
              a.click();
              document.body.removeChild(a);
            } else if (taskStatus === 'FAILURE') {
              clearTimer('An error occurred');
            }
          })
          .catch(function(err){
            console.log('err', err);
            clearTimer('An error occurred');
          });
      }, 800);
    
      function updateProgressTitle() {
        dots++;
        if (dots > 3) {
          dots = 1;
        }
        progressTitle.innerHTML = 'processing images ';
        for (var i = 0; i < dots; i++) {
          progressTitle.innerHTML += '.';
        }
      }
      function clearTimer(message) {
        clearInterval(timer);
        progressTitle.innerHTML = message;
      }
      </script> 
      {% endif %}
    </body>
    </html>
    

    En la parte inferior del bodyelemento, agregu茅 JavaScript para proporcionar un comportamiento adicional. Primero creo una referencia al campo de entrada del archivo y registro un oyente de cambios, que simplemente agrega el nombre del archivo seleccionado a la interfaz de usuario, una vez seleccionado.

    Luego viene la parte m谩s relevante. Utilizo el ifoperador l贸gico de plantilla de Django para verificar la presencia de un task_idser transmitido desde la HomeViewvista de clase. Esto indica una respuesta despu茅s de que make_thumbnailsse ha enviado una tarea. Luego uso la urletiqueta de plantilla de Django para construir una URL de verificaci贸n de estado de tarea apropiada y comenzar una solicitud AJAX temporizada por intervalo a esa URL usando la biblioteca Axios que mencion茅 anteriormente.

    Si el estado de una tarea se informa como “脡XITO”, inyecto un enlace de descarga en el DOM y hago que se active, lo que activa la descarga y borro el temporizador de intervalo. Si el estado es “FALLO”, simplemente borro el intervalo, y si el estado no es “脡XITO” ni “FALLO”, no hago nada hasta que se invoca el siguiente intervalo.

    En este punto, puedo abrir otra terminal, una vez m谩s con el entorno virtual Python activo, e iniciar el servidor de desarrollo Django, como se muestra a continuaci贸n:

    (venv) $ python manage.py runserver
    
    • Los terminales de tareas de redis-server y apio descritos anteriormente tambi茅n deben estar ejecut谩ndose, y si no ha reiniciado el trabajador de Apio desde que agreg贸 la make_thumbnailstarea, querr谩 Ctrl+Cdetener el trabajador y luego celery worker -A image_parroter --loglevel=infovolver a ejecutarlo para reiniciarlo. Los trabajadores de apio deben reiniciarse cada vez que se realiza un cambio de c贸digo relacionado con la tarea de apio.

    Ahora puedo cargar la vista home.html en mi navegador en http: // localhost: 8000 , enviar un archivo de imagen y la aplicaci贸n deber铆a responder con un archivo results.zip que contiene la imagen original y una miniatura de 128×128 p铆xeles.

    Implementar en un servidor Ubuntu

    Para completar este art铆culo, mostrar茅 c贸mo instalar y configurar esta aplicaci贸n Django que utiliza Redis y Celery para tareas en segundo plano asincr贸nicas en un servidor Ubuntu v18 LTS.

    Una vez SSH en el servidor, lo actualizo y luego instalo los paquetes necesarios.

    # apt-get update
    # apt-get install python3-pip python3-dev python3-venv nginx redis-server -y
    

    Tambi茅n hago un usuario llamado “webapp”, que me da un directorio de inicio para instalar el proyecto Django.

    # adduser webapp
    

    Despu茅s de ingresar los datos del usuario, agrego el usuario de la aplicaci贸n web a los grupos sudo y www-data, cambio al usuario de la aplicaci贸n web y luego cda su directorio de inicio.

    # usermod -aG sudo webapp
    # usermod -aG www-data webapp
    $ su webapp
    $ cd
    

    Dentro del directorio de la aplicaci贸n web, puedo clonar el repositorio image_parroter GitHub, cden el repositorio, crear un entorno virtual de Python, activarlo y luego instalar las dependencias del archivo requirements.txt.

    $ git clone https://github.com/amcquistan/image_parroter.git
    $ python3 -m venv venv
    $ . venv/bin/activate
    (venv) $ pip install -r requirements.txt
    

    Adem谩s de los requisitos que acabo de instalar, quiero agregar uno nuevo para el contenedor de la aplicaci贸n web uwsgi que servir谩 a la aplicaci贸n Django.

    (venv) $ pip install uWSGI
    

    Antes de continuar, ser铆a un buen momento para actualizar el archivo settings.py para cambiar el valor DEBUG a False y agregar la direcci贸n IP a la lista de ALLOWED_HOSTS.

    Despu茅s de eso, mu茅vase al directorio del proyecto Django image_parroter (el que contiene el m贸dulo wsgi.py) y agregue un nuevo archivo para guardar los ajustes de configuraci贸n de uwsgi, llamado uwsgi.ini, y coloque lo siguiente en 茅l:

    # uwsgi.ini
    [uwsgi]
    chdir=/home/webapp/image_parroter/image_parroter
    module=image_parroter.wsgi:application
    master=True
    processes=4
    harakiri=20
    
    socket=/home/webapp/image_parroter/image_parroter/image_parroter/webapp.sock  
    chmod-socket=660  
    vacuum=True
    logto=/var/log/uwsgi/uwsgi.log
    die-on-term=True 
    

    Antes de que me olvide, debo continuar y agregar el directorio de registro y otorgarle los permisos y la propiedad adecuados.

    (venv) $ sudo mkdir /var/log/uwsgi
    (venv) $ sudo chown webapp:www-data /var/log/uwsgi 
    

    A continuaci贸n, hago un archivo de servicio systemd para administrar el servidor de aplicaciones uwsgi, que se encuentra en /etc/systemd/system/uwsgi.servicey contiene lo siguiente:

    # uwsgi.service
    [Unit]
    Description=uWSGI Python container server  
    After=network.target
    
    [Service]
    User=webapp
    Group=www-data
    WorkingDirectory=/home/webapp/image_parroter/image_parroter
    Environment="/home/webapp/image_parroter/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin"
    ExecStart=/home/webapp/image_parroter/venv/bin/uwsgi --ini image_parroter/uwsgi.ini
    
    [Install]
    WantedBy=multi-user.target
    

    Ahora puedo iniciar el servicio uwsgi, verificar que su estado sea correcto y habilitarlo para que se inicie autom谩ticamente al arrancar.

    (venv) $ sudo systemctl start uwsgi.service
    (venv) $ sudo systemctl status uwsgi.service
    (venv) $ sudo systemctl enable uwsgi.service
    

    En este punto, la aplicaci贸n Django y el servicio uwsgi est谩n configurados y puedo continuar con la configuraci贸n del servidor redis.

    Personalmente prefiero usar los servicios systemd, por lo que editar茅 el /etc/redis/redis.confarchivo de configuraci贸n estableciendo el supervisedpar谩metro igual a systemd. Despu茅s de eso, reinicio el servidor redis, verifico su estado y lo habilito para que se inicie en el arranque.

    (venv) $ sudo systemctl restart redis-server
    (venv) $ sudo systemctl status redis-server
    (venv) $ sudo systemctl enable redis-server
    

    El siguiente paso es configurar el apio. Comienzo este proceso creando una ubicaci贸n de registro para Apio y le doy a esta ubicaci贸n los permisos y la propiedad adecuados, as铆:

    (venv) $ sudo mkdir /var/log/celery
    (venv) $ sudo chown webapp:www-data /var/log/celery
    

    A continuaci贸n, agrego un archivo de configuraci贸n de Apio, llamado celery.conf, en el mismo directorio que el archivo uwsgi.ini descrito anteriormente, colocando lo siguiente en 茅l:

    # celery.conf
    
    CELERYD_NODES="worker1 worker2"
    CELERY_BIN="/home/webapp/image_parroter/venv/bin/celery"
    CELERY_APP="image_parroter"
    CELERYD_MULTI="multi"
    CELERYD_PID_FILE="/home/webapp/image_parroter/image_parroter/image_parroter/%n.pid"
    CELERYD_LOG_FILE="/var/log/celery/%n%I.log"
    CELERYD_LOG_LEVEL="INFO"
    

    Para terminar de configurar apio, agrego su propio archivo de servicio systemd en /etc/systemd/system/celery.servicey coloco lo siguiente en 茅l:

    # celery.service
    [Unit]
    Description=Celery Service
    After=network.target
    
    [Service]
    Type=forking
    User=webapp
    Group=webapp
    EnvironmentFile=/home/webapp/image_parroter/image_parroter/image_parroter/celery.conf
    WorkingDirectory=/home/webapp/image_parroter/image_parroter
    ExecStart=/bin/sh -c '${CELERY_BIN} multi start ${CELERYD_NODES} 
      -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} 
      --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
    ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} 
      --pidfile=${CELERYD_PID_FILE}'
    ExecReload=/bin/sh -c '${CELERY_BIN} multi restart ${CELERYD_NODES} 
      -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} 
      --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
    
    [Install]
    WantedBy=multi-user.target
    

    Lo 煤ltimo que debe hacer es configurar nginx para que funcione como un proxy inverso para la aplicaci贸n uwsgi / django, as铆 como para servir el contenido en el directorio de medios. Hago esto agregando una configuraci贸n nginx en /etc/nginx/sites-available/image_parroter, que contiene lo siguiente:

    server {
      listen 80;
      server_name _;
    
      location /favicon.ico { access_log off; log_not_found off; }
      location /media/ {
        root /home/webapp/image_parroter/image_parroter;
      }
    
      location / {
        include uwsgi_params;
        uwsgi_pass unix:/home/webapp/image_parroter/image_parroter/image_parroter/webapp.sock;
      }
    }
    

    A continuaci贸n, elimino la configuraci贸n nginx predeterminada que me permite usar server_name _;para capturar todo el tr谩fico http en el puerto 80, luego creo un enlace simb贸lico entre la configuraci贸n que acabo de agregar en el directorio “sitios disponibles” al directorio “sitios habilitados” adyacente lo.

    $ sudo rm /etc/nginx/sites-enabled/default
    $ sudo ln -s /etc/nginx/sites-available/image_parroter /etc/nginx/sites-enabled/image_parroter
    

    Una vez hecho esto, puedo reiniciar nginx, verificar su estado y habilitarlo para que se inicie en el arranque.

    $ sudo systemctl restart nginx
    $ sudo systemctl status nginx
    $ sudo systemctl enable nginx
    

    En este punto, puedo apuntar mi navegador a la direcci贸n IP de este servidor Ubuntu y probar la aplicaci贸n de miniaturas.

    Conclusi贸n

    Este art铆culo describi贸 por qu茅 usar, as铆 como c贸mo usar, Celery con el prop贸sito com煤n de iniciar una tarea asincr贸nica, que se activa y se ejecuta en serie hasta su finalizaci贸n. Esto conducir谩 a una mejora significativa en la experiencia del usuario, reduciendo el impacto de las rutas de c贸digo de larga ejecuci贸n que impiden que el servidor de aplicaciones web maneje m谩s solicitudes.

    He hecho todo lo posible para proporcionar una explicaci贸n detallada del proceso de principio a fin desde la configuraci贸n de un entorno de desarrollo, la implementaci贸n de tareas de apio, la producci贸n de tareas en el c贸digo de la aplicaci贸n Django y el consumo de resultados a trav茅s de Django y algo de JavaScript simple.

    Gracias por leer y, como siempre, no dude en comentar o criticar a continuaci贸n.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *