4 casos de uso sobre cuándo usar Celery en una aplicación en Flask

- Andrés Cruz

Celery te ayuda a ejecutar código de forma asincrónica o en un cronograma periódico, que son cosas muy comunes que querrías hacer en la mayoría de los proyectos web cuando salimos un poco de la rutina de crear CRUDs y presentación de información al usuario.

Usualmente requerimos de hacer otros procesos como enviar emails, conectarnos a APIs o alguna tarea que no estemos seguros cuando va a terminar; en estos casos vas a necesitar ejecutar un proceso asíncrono.

A continuación, presentamos un par de casos de uso para los casos en los que es posible que desee utilizar el Celery.

Caso de uso n. ° 1: envío de correos electrónicos

Yo diría que este es uno de los ejemplos más comunes que nos vamos a enfrentar de por qué es una buena idea usar Celery o buscar una solución que le permita ejecutar tareas de forma asincrónica.

Por ejemplo, imagine que alguien visita la página de contacto de su sitio con la esperanza de completarla y enviarle un correo electrónico. Después de hacer clic en el botón enviar correo electrónico, se enviará un correo electrónico a su bandeja de entrada.

Envío de correos electrónicos sin Celery

La mejor manera de explicar por qué el Celery es útil es demostrando primero cómo funcionaría si no estuvieras usando Celery.

Un flujo de trabajo típico para esto sería:

  1. El usuario visita / página de contacto y se le presenta un formulario de contacto
  2. El usuario completa el formulario de contacto
  3. El usuario hace clic en el botón Enviar correo electrónico
  4. El cursor del mouse del usuario se convierte en un ícono de ocupado y ahora su navegador se cuelga
  5. Su aplicación Flask maneja la solicitud POST
  6. Su aplicación Flask valida el formulario
  7. Es probable que su aplicación Flask compile una plantilla del correo electrónico
  8. Su aplicación Flask toma ese correo electrónico y lo envía a su proveedor de correo electrónico configurado
  9. Su aplicación Flask espera hasta que su proveedor de correo electrónico (gmail, sendgrid, etc.) responda
  10. Su aplicación Flask devuelve una respuesta HTML al usuario redirigiéndola a una página
  11. El navegador del usuario muestra la nueva página y el cursor del mouse está ocupado.

Observe cómo los pasos 4 y 11 están en cursiva. Esos son pasos muy importantes porque entre los pasos 4 y 11 el usuario está sentado allí con un ícono de cursor del mouse ocupado y su sitio parece estar cargando lento para ese usuario. Están esperando una respuesta.

El problema con el flujo de trabajo anterior:

El verdadero problema aquí es que no tienes control sobre la duración de los pasos 8 y 9. Pueden tardar 500 ms, 2 segundos, 20 segundos o incluso un tiempo de espera después de 120 segundos.

Eso es porque se está comunicando con un sitio externo. En este caso, los servidores SMTP de gmail o algún otro servicio de correo electrónico transaccional como sendgrid o mailgun. No tienes absolutamente ningún control sobre el tiempo que tardarán en procesar tu solicitud.

Lo realmente peligroso de este escenario es ahora imaginar si 10 visitantes estuvieran tratando de completar su formulario de contacto y tuviera gunicorn o uwsgi en ejecución, que son servidores de aplicaciones de Python populares.

Si no los tiene configurados con varios trabajadores y / o subprocesos, entonces su servidor de aplicaciones se atascará mucho y no podrá manejar las 10 de esas solicitudes hasta que cada una finalice secuencialmente.

Normalmente, esto no es un problema si sus solicitudes finalizan rápidamente, por ejemplo, en menos de 100 ms y, especialmente, no es un gran problema si tiene un par de procesos en ejecución. Puede ejecutar docenas de solicitudes simultáneas en poco tiempo, pero no si demoran 2 o 3 segundos cada una, eso lo cambia todo.

También empeora porque otras solicitudes también comenzarán a bloquearse. Estas solicitudes pueden ser otro visitante que intenta acceder a su página de inicio o cualquier otra página de su aplicación.

Básicamente, su servidor de aplicaciones se sobrecargará con la espera y cuanto más tarden sus solicitudes en responder, peor será para cada solicitud y, antes de que se dé cuenta, ahora se tarda 8 segundos en cargar una página de inicio simple en lugar de 80 milisegundos.

Por qué el Celery es una herramienta increíble

Técnicamente, la salsa secreta para resolver el problema anterior es poder realizar los pasos 8 y 9 en segundo plano. Es por eso que Celery a menudo se etiqueta como un "trabajador en segundo plano".

Digo "técnicamente" porque podría resolver este problema con algo como la funcionalidad async / await de Python 3, pero esa es una solución mucho menos robusta.

Celery realizará un seguimiento del trabajo que le envíe en un back-end de base de datos como Redis o RabbitMQ. Esto mantiene el estado fuera del proceso de su servidor de aplicaciones, lo que significa que incluso si su servidor de aplicaciones se bloquea, su cola de trabajos seguirá existiendo.

Celery también le permite realizar un seguimiento de las tareas que fallan. Una "tarea" o trabajo es realmente un trabajo que le dices a Celery que haga, como enviar un correo electrónico. Puede ser cualquier cosa.

Celery también le permite configurar políticas de reintento para las tareas que fallan. Por ejemplo, si ese correo electrónico no se envía, puede indicarle a Celery que lo intente, digamos 5 veces, e incluso que haga estrategias avanzadas de reintento, como retroceso exponencial, lo que significa hacer algo como intentarlo de nuevo después de 4 segundos y luego 8, 16, 32 y 64 segundos más tarde. Puede configurar todo esto con gran detalle.

Celery también le permite calificar las tareas limitadas. Por ejemplo, si desea proteger su formulario de contacto para no permitir más de 1 correo electrónico cada 10 segundos para cada visitante, puede configurar reglas personalizadas como esa muy fácilmente. Puede hacer esto en función de la dirección IP o incluso por usuario que haya iniciado sesión en su sistema.

Lo que estoy tratando de decir es que el Celery es una herramienta muy poderosa que le permite hacer cosas listas para producción casi sin repetición y con muy poca configuración. Es por eso que prefiero usarlo en lugar de async / await u otras soluciones asincrónicas.

Lo bueno es que usamos Docker, por lo que agregar Celery y Redis al proyecto no es gran cosa. Son solo unas pocas líneas de configuración de YAML y hemos terminado.

Envío de correos electrónicos con Celery

  1. Con el conocimiento anterior en la mano, ajustemos un poco el flujo de trabajo:
  2. El usuario visita / página de contacto y se le presenta un formulario de contacto
  3. El usuario completa el formulario de contacto
  4. El usuario hace clic en el botón Enviar correo electrónico
  5. El cursor del mouse del usuario se convierte en un ícono de ocupado y ahora su navegador se cuelga
  6. Su aplicación Flask maneja la solicitud POST
  7. Su aplicación Flask valida el formulario
  8. Su aplicación Flask llama a una tarea de Celery que usted creó
  9. Su aplicación Flask devuelve una respuesta HTML al usuario redirigiéndola a una página
  10. El navegador del usuario muestra la nueva página y el cursor del mouse está ocupado.

Lo que es muy diferente entre el flujo de trabajo anterior y el original es que los pasos 4 a 9 terminarán de ejecutarse casi de inmediato. No me sorprendería que todo termine en 20 milisegundos. Lo que significa que podría manejar 50 de estas solicitudes en 1 segundo y eso es solo con 1 proceso / subproceso en su servidor de aplicaciones.

Eso es una gran mejora y también es muy consistente. Volvemos a controlar el tiempo que tarda el usuario en obtener una respuesta y no estamos atascando nuestro servidor de aplicaciones. Podemos escalar fácilmente a cientos de solicitudes simultáneas por segundo simplemente agregando más procesos del servidor de aplicaciones (o núcleos de CPU básicamente).

Ya no necesitamos enviar el correo electrónico durante el ciclo de solicitud / respuesta y esperar una respuesta de su proveedor de correo electrónico. Podemos simplemente ejecutar la tarea de Celery en segundo plano y responder inmediatamente con una redirección.

El usuario realmente no necesita saber si el correo electrónico se entregó o no. Es probable que vean un simple mensaje flash que diga gracias por contactarte y que les responderás pronto.

Pero si desea monitorear la tarea y recibir una notificación cuando finalice, también puede hacerlo con Celery. Sin embargo, en este caso, realmente no importa si el correo electrónico se entrega 500 ms o 5 segundos después de ese momento, porque todo es igual desde el punto de vista del usuario.

Por cierto, en caso de que se lo esté preguntando, la tarea de Celery en el nuevo paso 7 serían los pasos 7 a 9 del flujo de trabajo original, que fueron:

Es probable que su tarea de Celery compile una plantilla del correo electrónico

Su tarea de Celery toma ese correo electrónico y lo envía a su proveedor de correo electrónico configurado

Su tarea de Celery espera hasta que su proveedor de correo electrónico (gmail, sendgrid, etc.) responda

Así que se hacen las mismas cosas. Es solo que Celery lo maneja en segundo plano.

Caso de uso n. ° 2: Conexión a API de terceros

Acabamos de hablar sobre el envío de correos electrónicos, pero en realidad esto se aplica a realizar llamadas a API de terceros a un servicio externo que no controlas. Realmente cualquier llamada de red externa.

Esto también se relaciona realmente con la realización de llamadas a la API en su ciclo típico de solicitud/respuesta de una conexión HTTP. Por ejemplo, el usuario visita una página, desea ponerse en contacto con una API de terceros y ahora desea responder al usuario.

Usaria Celery casi siempre para el caso de uso anterior y si necesitaba actualizar la interfaz de usuario de una página después de recuperar los datos de la API, entonces usaría websockets.

Cualquiera de los dos le permite responder de inmediato y luego actualizar su página después de recuperar los datos.

Caso de uso n. ° 3: Realización de tareas de larga duración

Otro caso de uso es hacer algo que lleva bastante tiempo. Esto podría generar un informe que podría tardar 2 minutos o procesos muy largos en general.

Estas son cosas para las que esperaría ver una barra de progreso.

¿Te imaginas lo loco que sería si no estuvieras usando Celery para esto? Imagine cargar una página para generar un informe y luego tener que mantener la pestaña de su navegador abierta durante 2 minutos completos, de lo contrario, el informe no se generaría.

Eso sería una locura, pero Celery hace que esto sea muy fácil de lograr sin esa limitación. Puede utilizar exactamente las mismas estrategias que en el segundo caso de uso para actualizar su interfaz de usuario según sea necesario. Con websockets también sería bastante fácil enviar actualizaciones de progreso.

Caso de uso n. ° 4: ejecución de tareas programadas
Este último caso de uso es diferente a los otros 3 enumerados anteriormente, pero es muy importante.

Imagínese si quisiera realizar una tarea todos los días a la medianoche. En el pasado, es posible que haya utilizado trabajos cron, ¿verdad? No sería tan malo configurar un trabajo cron para ejecutar esa tarea.

Pero hay un par de problemas con el uso de cron. Tenga en cuenta que los mismos problemas también existen con los temporizadores systemd.

Para empezar, es probable que tenga que dividir esa funcionalidad programada en su propio archivo para poder llamarla de forma independiente.

Siendo realistas, eso no es tan malo, pero es algo que querrá hacer, y tal vez se vuelva molesto si tiene que lidiar con la carga de los ajustes de configuración o las variables de entorno para ese archivo (es 1 cosa más con la que lidiar para cada tarea programada).

Además, en el mundo actual, estamos avanzando hacia la colocación de la mayoría de las cosas en contenedores y se considera una práctica recomendada ejecutar solo un proceso por contenedor.

En otras palabras, no querrá ejecutar tanto el demonio cron como su servidor de aplicaciones en el mismo contenedor. Si hace eso, está yendo en contra de la corriente de las mejores prácticas examinadas por la comunidad.

Por lo tanto, puede pensar en ejecutar cron en su host de Docker y cambiar su trabajo cron para ejecutar un comando de Docker en lugar de simplemente llamar a su archivo Flask directamente. Eso es totalmente factible y funcionaría, pero también hay un problema con ese enfoque.

¿Qué pasa si ha escalado horizontalmente a 3 servidores de aplicaciones web? Si cada uno tuviera su propio trabajo cron, entonces estaría ejecutando esa tarea 3 veces al día en lugar de una vez, potencialmente haciendo el triple de trabajo. Definitivamente, ese no es un resultado esperado y podría presentar condiciones de carrera si no tienes cuidado.

Ahora, lo sé, podría decidir configurar los trabajos cron en 1 de los 3 servidores, pero eso va por un camino muy dudoso porque ahora, de repente, tiene estos 3 servidores, pero 1 de ellos es diferente. ¿Qué sucede si está haciendo un reinicio continuo y el 1 que está asignado para hacer el trabajo cron no está disponible cuando se supone que debe ejecutarse una tarea?

Se convertirá rápidamente en una pesadilla de configuración (lo sé porque probé esto en el pasado).

Los problemas anteriores desaparecen con el apio. Tiene un concepto de servidor "beat" que puede ejecutar donde puede configurar tareas que se ejecutan en cualquier horario que desee. Incluso es compatible con la sintaxis de estilo cron, por lo que puede hacer todo tipo de horarios salvajes, como cada segundo martes del mes a la 1 a. M.

También está muy integrado con la configuración de su aplicación. Dado que es una tarea más, todas las variables de entorno y de configuración de su aplicación están disponibles. Es una gran victoria no tener que lidiar con eso por archivo.

Otra ventaja es que el estado de este programa se almacena en su back-end de Apio, como Redis, que solo se guarda en 1 lugar. Eso significa que si tiene 1 o 100 servidores de aplicaciones web, sus tareas solo se ejecutarán una vez.

En el ejemplo de reinicio continuo, no importará si 1 de los 3 servidores de aplicaciones no está disponible. Siempre que al menos uno de ellos esté disponible, la tarea programada podrá ejecutarse.

Es un trato bastante bueno. En mi opinión, también es más fácil de configurar que un trabajo cron.

Usamos bastante las tareas programadas en el curso Build a SAAS App with Flask. Por ejemplo, en un caso, ejecutamos una tarea todos los días a la medianoche que verifica si la fecha de vencimiento de la tarjeta de crédito de un usuario vencerá pronto, y si lo hace, marcamos la tarjeta como is_expiring y ahora la interfaz de usuario web puede mostrar un banner. diciendo que actualice los datos de su tarjeta.

Pequeñas cosas como esa ayudan a reducir la tasa de abandono en una aplicación SAAS. Hay un millón de ejemplos de dónde es posible que desee tener tareas programadas. Quizás podría buscar cuentas de usuario que no hayan tenido actividad en 6 meses y luego enviar un correo electrónico de recordatorio o eliminarlas de su base de datos. ¡Entiendes la idea!

El apio es sin duda una de mis bibliotecas de Python favoritas.

¿Para qué estás usando apio? Házmelo saber a continuación.

Caso de uso n. ° 4: ejecución de tareas programadas

Este último caso de uso es diferente a los otros 3 enumerados anteriormente, pero es muy importante.

Imagínese si quisiera realizar una tarea todos los días a la medianoche. En el pasado, es posible que haya utilizado trabajos cron, ¿verdad? No sería tan malo configurar un trabajo cron para ejecutar esa tarea.

Pero hay un par de problemas con el uso de cron. 

Para empezar, es probable que tenga que dividir esa funcionalidad programada en su propio archivo para poder llamarla de forma independiente.

Siendo realistas, eso no es tan malo, pero es algo que querrá hacer, y tal vez se vuelva molesto si tiene que lidiar con la carga de los ajustes de configuración o las variables de entorno para ese archivo (es 1 cosa más con la que lidiar para cada tarea programada).

Además, en el mundo actual, estamos avanzando hacia la colocación de la mayoría de las cosas en contenedores y se considera una práctica recomendada ejecutar solo un proceso por contenedor.

En otras palabras, no querrá ejecutar tanto el demonio cron como su servidor de aplicaciones en el mismo contenedor. Si hace eso, está yendo en contra de la corriente de las mejores prácticas examinadas por la comunidad.

Por lo tanto, puede pensar en ejecutar cron en su host de Docker y cambiar su trabajo cron para ejecutar un comando de Docker en lugar de simplemente llamar a su archivo Flask directamente. Eso es totalmente factible y funcionaría, pero también hay un problema con ese enfoque.

¿Qué pasa si ha escalado horizontalmente a 3 servidores de aplicaciones web? Si cada uno tuviera su propio trabajo cron, entonces estaría ejecutando esa tarea 3 veces al día en lugar de una vez, potencialmente haciendo el triple de trabajo. Definitivamente, ese no es un resultado esperado y podría presentar condiciones de carrera si no tienes cuidado.

Ahora, lo sé, podría decidir configurar los trabajos cron en 1 de los 3 servidores, pero eso va por un camino muy dudoso porque ahora, de repente, tiene estos 3 servidores, pero 1 de ellos es diferente. ¿Qué sucede si está haciendo un reinicio continuo y el 1 que está asignado para hacer el trabajo cron no está disponible cuando se supone que debe ejecutarse una tarea?

Se convertirá rápidamente en una pesadilla de configuración.

Los problemas anteriores desaparecen con el apio. Tiene un concepto de servidor "beat" que puede ejecutar donde puede configurar tareas que se ejecutan en cualquier horario que desee. Incluso es compatible con la sintaxis de estilo cron, por lo que puede hacer todo tipo de horarios salvajes, como cada segundo martes del mes a la 1 a. M.

También está muy integrado con la configuración de su aplicación. Dado que es una tarea más, todas las variables de entorno y de configuración de su aplicación están disponibles. Es una gran victoria no tener que lidiar con eso por archivo.

Otra ventaja es que el estado de este programa se almacena en su back-end de Celery, como Redis, que solo se guarda en 1 lugar. Eso significa que si tiene 1 o 100 servidores de aplicaciones web, sus tareas solo se ejecutarán una vez.

Lectura recomendada.

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz En Udemy

Acepto recibir anuncios de interes sobre este Blog.