Tareas asíncronas en Django con Redis y Apio

Introducción

En este tutorial proporcionaré una comprensión general de por qué las colas de mensajes de apio son valiosas y cómo utilizar el apio junto con Redis en una aplicación de Django. Para demostrar los aspectos específicos de la implementación, construiré una aplicación de procesamiento de imágenes minimalista que genere miniaturas de las imágenes enviadas por los usuarios.
Los siguientes temas serán cubiertos:
  • Antecedentes de las colas de mensajes con apio y redis.
  • Configuración de desarrollo local con Django, apio y redis
  • Creación de miniaturas de imágenes dentro de una tarea de apio
  • Implementación 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, lo guiaré para desarrollar 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 la 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 usar para ejecutar tareas repetibles, de período (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 conoce como intermediario de mensajes. Un agente de mensajes común que se usa con apio es Redis, que es un ejecutante, en la memoria, almacén de datos de valores clave. Específicamente, Redis se usa para almacenar mensajes producidos por el código de la aplicación que describe el trabajo a realizar en la cola de tareas de Apio. Redis también sirve como almacenamiento de resultados que salen de las colas de apio que luego son recuperados por los consumidores de la cola.

Configuración de desarrollo local con Django, apio y redis

Comenzaré con la parte más difícil primero que está instalando Redis.
Instalación de Redis en Windows
  1. Descarga el archivo zip de Redis y descomprime en algún directorio.
  2. Encuentre el archivo llamado redis-server.exe y haga doble clic para iniciar el servidor en una ventana de comando
  3. Del mismo modo, busque otro archivo llamado redis-cli.exe y haga doble clic para abrir el programa en una ventana de comando separada
  4. Dentro de la ventana de comandos que ejecuta el cliente cli, haga una prueba para asegurarse de que el cliente pueda comunicarse con el servidor emitiendo el comando pingy, si todo va bien, se PONGdebe devolver una respuesta
Instalación de Redis en Mac OSX / Linux
  1. Descargue el archivo tarball de Redis y extráigalo en algún directorio
  2. Ejecuta el archivo make make installpara construir el programa.
  3. Abre una ventana de terminal y ejecuta el redis-servercomando
  4. En otra ventana de terminal ejecutar redis-cli
  5. Dentro de la ventana del terminal que ejecuta el cliente cli, haga una prueba para asegurarse de que el cliente pueda comunicarse con el servidor emitiendo el comando pingy, si todo va bien, se PONGdebe devolver una respuesta
Instalar Python Virtual Env y Dependencias
Ahora puedo pasar a la creación de un entorno virtual Python3 e instalar los paquetes de dependencia necesarios para este proyecto.
Para comenzar, crearé un directorio para alojar cosas llamadas image_parroter y luego crearé mi entorno virtual. Todos los comandos de aquí en adelante serán del mismo 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
Ahora que el entorno virtual está 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 tareas de apio.
  • Django Widget Tweaks es un complemento de Django para proporcionar flexibilidad en la forma en que se representan las entradas de formularios.
Configurando el 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 es la siguiente:
$ 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 en este proyecto de Django, agrego un nuevo módulo de imagen de loro / loro de imagen / 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 celery.
El osmódulo se utiliza para asociar una variable de entorno Celery llamada DJANGO_SETTINGS_MODULEcon el módulo de configuración del proyecto Django. A continuación, instalo una instancia de la Celeryclase para crear la celery_appvariable de instancia. Luego actualizo la configuración de la aplicación Celery con la configuración que pronto colocaré en el archivo de configuración del proyecto Django identificable con el prefijo 'CELERY_'. Por último, les cuento a los recién creados.celery_app instancia que descubra automáticamente las tareas dentro del proyecto.
El módulo celery.py completado 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 del apio y añado la configuración que se ve a continuación. Esta configuración le dice a Celery que use a Redis como el intermediario de mensajes, así como a dónde conectarse. También le dicen a Celery que espere que los mensajes se transfieran entre las colas de tareas de Celery y Redis message broker 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, debo 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 nombre dentro del paquete Django "image_parroter".
# 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 la shared_tasksfunción decorator y la uso para definir una función 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 INSTALLED_APPSen el mó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 a los usuarios cargar 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 unos pocos comandos simples en tres terminales.
En una terminal necesito tener el servidor redis en ejecución, 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 un segundo terminal, con una instancia activa del entorno virtual de Python instalado anteriormente, en el directorio del paquete raíz del proyecto (el mismo que contiene el módulo manage.py) lanzo el programa apio.
(venv) $ celery worker -A image_parroter --loglevel=info

 -------------- celery@Adams-MacBook-Pro-191.local 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, de nuevo con el entorno virtual de 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 iniciar el envío al intermediario de mensajes y la cola de tareas. El resultado de llamar al .delay(...)método es un valor devuelto de tipo promesa celery.result.AsyncResultEste valor de retorno contiene información como el ID de la tarea, su estado de ejecución y el estado de la tarea junto con la capacidad de acceder a los resultados producidos 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 con respaldo de Redis en la aplicación Django está fuera de la forma en que puedo pasar a demostrar una funcionalidad más útil con la aplicación de miniaturas anteriormente mencionada.
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 pasadas a la tarea creando una miniatura para cada una, agregando cada miniatura a un archivo zip al mismo tiempo que limpia los archivos intermedios. Se devuelve un diccionario simple especificando la URL desde donde se puede descargar el archivo zip de miniaturas.
Con la tarea de apio definida, paso a la construcción de las vistas de Django para presentar una plantilla con un formulario de carga de archivos.
Para comenzar, le doy al proyecto de Django una MEDIA_ROOTubicación donde pueden residir los archivos de imágenes y los archivos zip (utilicé esto en la tarea de ejemplo anterior) y también especifico desde MEDIA_URLdónde se puede servir el contenido. En el módulo image_parroter / settings.py agrego el MEDIA_ROOTMEDIA_URLIMAGES_DIRla configuración de ubicaciones 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 getpostmétodos, 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 como se ve por encima de la HomeViewclase.
El postmétodo construye el FileUploadFormobjeto utilizando 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 al mismo tiempo que toma la tarea idy el estado para pasar a la plantilla, o devuelve el formulario con su Errores a 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 he importado 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 con el 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 que lo asigna a la resultsclave de la response_dataque se devolverá como JSON al solicitante HTTP.
Antes de poder crear la interfaz de usuario de la plantilla, necesito asignar las clases de vistas de Django anteriores a algunas URL razonables. 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 del nivel de la aplicación, así como informarle de la URL del medio, de esta manera:
# 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, comienzo 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 comenzar, necesito crear un directorio para alojar esta plantilla única dentro del directorio de miniaturas, de la siguiente manera:
(venv) $ mkdir -p thumbnailer/templates/thumbnailer
Luego, dentro de este directorio de plantillas / miniaturas, agrego una plantilla llamada home.html. Dentro de home.html, comienzo cargando las etiquetas de la plantilla "widget_tweaks", luego avanzo para definir el HTML importando un marco CSS llamado bulma CSS , así como una biblioteca de 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 curso 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, he agregado JavaScript para proporcionar un comportamiento adicional. Primero creo una referencia al campo de entrada del archivo y registro un detector 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 haya enviado una tarea. Luego uso la urletiqueta de la plantilla de Django para construir una URL de verificación de estado de tarea adecuada y comenzar una solicitud AJAX de 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 dispare, lo que activa la descarga y borra el temporizador de intervalos. Si el estado es "FALLO", simplemente borro el intervalo, y si el estado no es "ÉXITO" o "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 de Python activo, e iniciar el servidor de desarrollo Django, como se muestra a continuación:
(venv) $ python manage.py runserver
  • Los terminales de tareas redis-server y apio descritos anteriormente también deben estar en ejecución, y si no ha reiniciado el trabajador de Apio desde que agregó la make_thumbnailstarea, querrá Ctrl+Cdetener al trabajador y luego ejecute celery worker -A image_parroter --loglevel=infonuevamente para reiniciarlo. Los trabajadores de apio deben reiniciarse cada vez que se realice 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 debe responder con un archivo results.zip que contenga la imagen original y una miniatura de 128x128 píxeles.

Implementación en un servidor Ubuntu

Para completar este artículo, demostraré cómo instalar y configurar esta aplicación de Django que utiliza Redis y Celery para tareas en segundo plano asíncronas en un servidor Ubuntu v18 LTS.
Una vez que SSH puso 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 en.
# adduser webapp
Después de ingresar los datos del usuario, a continuación, agrego el usuario de la aplicación web al sudo y los grupos de datos de www, 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 GitHub de image_parroter, cden el repositorio, crear un entorno virtual de Python, activarlo y luego instalar 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, vaya al directorio del proyecto image_parroter de Django (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 seguir adelante, agregar el directorio de registro y darle 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 en el arranque.
(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 de systemd, así que editaré el /etc/redis/redis.confarchivo de configuración configurando el supervisedparámetro igual a systemdDespués, reinicio el servidor redis, reviso 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
Lo siguiente es configurar el apio. Comienzo este proceso creando una ubicación de registro para Celery y le doy a esta ubicación los permisos y la propiedad adecuados, como:
(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 Celery, 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 el apio, agrego su propio archivo de servicio systemd /etc/systemd/system/celery.servicey coloco lo siguiente:
# 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 hay que 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. Lo hago 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 detectar 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, comprobar 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 describe por qué usar, así como cómo usar, el apio con el propósito común de iniciar una tarea asíncrona, 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 inicio 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 algunos JavaScript simples.

Acerca de: Programator

Somos Instinto Programador

0 comentarios:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

Con tecnología de Blogger.