Utilizando las Colas o Queues y los Trabajos o Jobs para posponer Tareas en Laravel

Laravel al permitir poder hacer toda clase de sistemas desde sencillos a complejos, muchas veces estas tareas pasando por un alto computo, y es una de las desventajas principales que tenemos en las aplicaciones web con respecto a las aplicaciones de escritorio, y es que las aplicaciones web, usualmente (las tradicionales al menos) tienen menos recursos de cómputo disponibles, o son más limitados y esto es algo que vemos en hacer operaciones sencillas como enviar un correo o procesar una imagen, que al emplear el hilo principal del servidor (el mismo que empleamos a momento de hacer peticiones al servidor) vemos que el servidor tarda unos pocos segundos en devolver una respuesta y es este tiempo adicional el empleado para hacer estas tareas completas. Adicionalmente, si la operación a realizar supera la capacidad de cómputo o muchas personas al mismo tiempo realizan operaciones de este tipo, el servidor puede dar un error de agotamiento del tiempo (time exhaustion).

Por lo comentado en el anterior párrafo, es que debemos de emplear un mecanismo para poder hacer todos estos procesos de manera eficiente, y la forma que tenemos de hacer esto, es delegando estas operaciones, en vez de hacerlas el hilo principal, podemos. Enviarle estas tareas a un proceso (o varios dependiendo de como lo configuremos) el cual se encarga de ir procesando tareas de manera consecutiva una detrás de la otra y con esto, ya introducimos la definición de colas y trabajos.

Los trabajos son estas operaciones costosas a nivel de computo, enviar un correo, procesar una imagen, procesar un archivo excel, generar un pdf, etc, los cuales son asignados o enviados a una cola o colas (dependiendo de como lo configuremos) y de van procesando de manera eficiente es decir, podemos habilitar una cantidad determinada de procesos secundarios que son manejados por las colas para procesar estas tarea por lo tanto, con este sistema, no importa cuantos trabajos existan en un mismo tiempo, que se irán procesando de a poco sin saturar el sistema. Adicionalmente, es posible especificar prioridades en los trabajos para que se ejecuten antes que otros.

En definitiva, al emplear el sistema de colas y trabajadores, se mejora la capacidad de respuesta, es posible incrementar de manera horizontal la cantidad de trabajadores disponibles para procesar estos trabajos, es posible volver a ejecutar los trabajos fallidos, dando con esto una mejor tolerancia a fallos al sistema, todo esto, de manera asíncrona sin afectar al hilo principal

Así que aclarado la importancia de este sistema, vamos a conocer cómo implementarlo.

Controlador de cola

Primero, debemos de elegir un controlador de colar para emplear entre todos los existentes:

  • 'sync': El controlador 'sync' ejecuta los trabajos en cola justo después y en el mismo ciclo solicitud-respuesta. Es adecuado para entornos de desarrollo y pruebas, pero no para producción.
  • 'database: el controlador de 'base de datos' almacena los trabajos en cola en una tabla de base de datos en un proceso de trabajo de cola independiente.
  • 'redis': el controlador 'redis' utiliza Redis como almacén de cola.
  • 'beanstalkd': El controlador 'beanstalkd' utiliza el servicio de cola Beanstalkd, para procesar las colas.
  • 'sqs' (Amazon Simple Queue Service): el controlador 'sqs' se utiliza para la integración con Amazon SQS.
  • 'rabbitmq': el controlador 'rabbitmq'  permite utilizar RabbitMQ como controlador de cola.
  • 'null': el controlador nulo se utiliza para desactivar el sistema de colas por completo.

Por defecto, está configurado el de la base de datos:

 

config\queue.php

'default' => env('QUEUE_CONNECTION', 'database')

Y para realizar las siguientes pruebas, puedes emplear el de database aunque, usualmente Redis es una excelente opción por ser una base de datos rápida y eficiente y que instalamos anteriormente y es la que usaremos:

config\queue.php

'default' => env('QUEUE_CONNECTION', 'redis')

Finalmente, iniciamos el proceso para ejecutar los trabajos mediante:

$ php artisan queue:work

Y veremos por la consola:

INFO  Processing jobs from the [default] queue.  

Cada vez que se procesa un trabajo, verás por la terminal:

.................................................................... 3s DONE
  2024-07-12 09:44:31 App\Jobs\TestJob ............................................................................... RUNNING
  2024-07-12 09:44:34 App\Jobs\TestJob ............................................................................... 3s DONE
  2024-07-12 09:45:43 App\Jobs\TestJob ............................................................................... RUNNING

No importa si desde tus controladores o similares despachas trabajos y la cola NO está activa, Laravel las registras y cuando actives el proceso de la cola, las despacha; y esto es todo, ya con esto Laravel levanta un proceso para gestionar los trabajos, falta crear el trabajo que veremos en el siguiente apartado.

En esta entrada vamos a conocer el uso de las Colas y los Trabajos en Laravel (Queues y Jobs) para poder realizar trabajos en segundo plano.

La idea de esta API es que nosotros podemos aplazar tareas (dejarlas en segundo plano) que requieren un alto consumo de computo y todo esto es para mejorar la experiencia del usuario y evitar cuelgues en la aplicación; de tal manera que estas tareas de alto cómputo se ejecutarán en otro hilo o proceso en nuestro servidor y se irán resolviendo a medida que lleguen o dependiendo de la prioridad de las mismas.

Básicamente todo este proceso se lleva a cabo entre las tareas/trabajos/jobs (los procesos elevados de cómputo) y las colas (mecanismo para registrar los jobs).

De igual manera vamos a hablar un poco más en detalle de estos dos componentes fundamentales.

Los trabajos o jobs y las colas o queues

Los trabajos son la tarea pesada o que requieren mucho procesamiento que queremos aplazar; y pueden ser de cualquier tipo como envío masivo o simple de emails, procesar imágenes, vídeos etc; por lo tanto son estas tareas que nosotros vamos registrando como pendientes para que Laravel las vaya procesando uno a uno a medida que esta disponible; asi que lo siguiente que te puedes preguntan es, ¿Cómo se maneja esta organización?; es decir, quien es el que se encarga de manejar la prioridad entre tareas o trabajos (colas) y cómo mantenemos una organización de todo esto.

Los jobs

En Laravel, los jobs son una parte importante de la funcionalidad de la cola de trabajos del framework, es decir los jobs son usados para procesar los trabajos pesados almacenados mediante “colas”; estas tareas pesadas pueden ser cualquier cosa como envio de emails, procesar imágenes, vídeos entre otros; esto es ideal ya que, estas tareas no forman parte de la sesión principal del usuario y se evita consumir recursos de la sesión principal del usuario, aparte de que, se evita las famosas ventanas de “no responde” y en general, se tiene una mejor experiencia en el uso de la aplicación.ya que no afecta el rendimiento o la capacidad de respuesta de la aplicación principal.

Los jobs en Laravel pueden ser creados para realizar cualquier tarea que se necesite en el back-end sin que el usuario tenga que esperar a que se complete la tarea; es decir, como enviar un correo o exportar datos en un excel o un formato similar.

Lo estupendo de los jobs es que, se ejecutan de manera secuencial, suponte que creas un job para procesar correos, los correos se van enviando uno a uno sin necesidad de sobrecargar la página, cosa que no sucederia si el envio de correos se manejada desde la sesión principal del usuario y de repente, vengan 5 o más usuarios a enviar correos desde la aplicación, por lo tanto, en un mismo instante de tiempo, tendrias múltiples envios de correos y gastando los recursos que esto requiera.

En resumen, los jobs en Laravel son una parte esencial de la cola de trabajos del framework, y permiten que las tareas pesadas o no esenciales se realicen en segundo plano para mejorar el rendimiento y la capacidad de respuesta de la aplicación principal.

Las colas

Ya en este punto, hemos introducido el concepto de “colas” en el uso de los jobs, de igual manera, vamos a explicarlo rápidamente; las colas de trabajo corresponden a la operación que se quiere realizar, que como comentamos antes, corresponde a la operación “pesada” que se quiere realizar, es decir, en vez de enviar un correo desde un controlador, se almacena en una cola que luego es procesado mediante otro proceso.

Crea tus propias colas en Laravel para mantener el orden

La respuesta a lo comentado anteriormente es bastante simple, serían las colas como mecanismo que permiten registrar o añadir estos trabajos o jobs; podemos registrar o tener tantas colas como queramos y podemos especificar a Laravel mediante artisan que colas queremos que procesen primero; por ejemplo, veamos el siguiente ejemplo:

php artisan queue:work --queue=high,default

Mediante el comando anterior le estamos diciendo a Laravel que primero procese los trabajos de una cola llamada "hight" y luego lo de una cola llamada "default", que por cierto, es la cola que tenemos definida por defecto.

Conexiones para la estructura para mantener las colas

Las conexiones son la manera predeterminada que tenemos para indicar cómo se va a llevar a cabo todo el proceso de las colas (ya que recuerda que la cola absorbe a la tarea, por lo tanto, en este punto, una vez registrada la tarea o job en una cola, en nuestro cola la tarea ya no pinta nada); pero para poder emplear toda esta estructura debemos indicar la conexión y con ella debemos indicar:

  • El controlador o driver
  • Valores predeterminados o de configuración

Por lo tanto, mediante la conexiones nosotros podemos especificar el driver que vamos a emplear que puede ser cualquier como la base de datos u otros servicios; por ejemplo:

Base de datos:

  1. Amazon SQS: aws/aws-sdk-php ~3.0
  2. Beanstalkd: pda/pheanstalk ~4.0
  3. Redis: predis/predis ~1.0 or phpredis PHP extension

Y algunos parámetros extra para la configuración.

Configurar el Driver o conector de las colas para la base de datos

Ahora vamos a abrir el archivo de config/queue.php y el .env, en los cuales vamos a buscar una configuración llamada QUEUE_CONNECTION

Que por defecto se llama QUEUE_CONNECTION y tiene la configuración de sync, que si lo analizas un poco verás que tenemos varios tipos de conexiones que podemos emplear, entre ella la llamada database que es la que vamos a emplear, por lo tanto configúralo al menos en el archivo de .env; en mi caso lo voy a dejar así en el .env:

QUEUE_CONNECTION=database
Y en el config/queue.php quede:
    'default' => env('QUEUE_CONNECTION', 'database'),

Crear la tabla de jobs en nuestra base de datos

Ahora bien lo siguiente que de puedes preguntar sería, ¿cómo nosotros podemos configurar la tabla en nuestra base de datos?; siguiente la documentación oficial:

php artisan queue:table php artisan migrate

Ya tenemos un comando de artisan que nos ayuda en todo este proceso, que sería el que nos permite generar la migración para crear la tabla; y con esto estamos listos.

Creando nuestro primer Job en Laravel

Ahora vamos a crear nuestro primer Job que vamos a configurar con la cola de default; para eso, vamos a crear uno mediante nuestro artisan:

php artisan make:job ProcessImageSmall

Y con esto tenemos un archivo generado en App\Jobs

El cual consta de dos bloques principales:

  1. __construct
  2. handle

La función constructora para inicializar los datos básicos que vamos a emplear y la función de handle para hacer el proceso pesado; así de simple, por lo tanto en la función de handle podemos enviar los emails, procesar videos o imágenes, para nuestro ejemplo vamos a suponer que tenemos una imagen que estamos recibiendo por parámetros:

  protected $image;
 
    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(PostImage $image)
    {
        $this->image = $image;
    }

Esta imagen es básicamente una instancia de un modelo que luce de la siguiente manera:

class PostImage extends Model
{
    protected $fillable = ['post_id', 'image'];
 
    public function post(){
        return $this->belongsTo(Post::class);
    }
 
    public function getImageUrl(){
        return URL::asset('images/'.$this->image);
        //return Storage::url($this->image);
    }
}

Y ahora, vamos a hacer un proceso pesado como escalar una imagen; para esto, vamos a emplear el siguiente paquete que podemos instalar mediante composer:

composer require intervention/image

El código que vamos a emplear, básicamente se auto explica solo:

<?php
 
namespace App\Jobs;
 
use App\PostImage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
 
use Intervention\Image\ImageManagerStatic;
 
class ProcessImageSmall implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
    protected $image;
 
    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(PostImage $image)
    {
        $this->image = $image;
    }
 
    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $image = $this->image;
 
        $fullPath = public_path('images' . DIRECTORY_SEPARATOR . $image->image);
        $fullPathSmall = public_path('images' . DIRECTORY_SEPARATOR . 'small' . DIRECTORY_SEPARATOR . $image->image);
 
        $img = ImageManagerStatic::make($fullPath)->resize(300,200);
        $img->save($fullPathSmall);
    }
}

En el cual simplemente creamos una instancia de nuestro paquete, que instalamos anteriormente, especificamos la ubicación de la imagen y mediante la función de resize escalamos la imagen a las dimenciones especificadas; luego, guardamos la nueva imagen y con esto estamos listos.

Despachar un trabajo

Lo siguiente que tenemos que especificar, seria en donde nosotros vamos a llamar a este trabajo, que puede ser en cualquier parte de nuestra aplicación como cuando nosotros cargamos una imagen, etc; supongamos que tenemos un método para cargar una imagen en Laravel por ejemplo:

  public function image(Request $request, Post $post)
    {
        $request->validate([
            'image' => 'required|mimes:jpeg,bmp,png|max:10240', //10Mb
        ]);
 
        $filename = time() . "." . $request->image->extension();
 
        $request->image->move(public_path('images'), $filename);
 
        //$path = $request->image->store('public/images');
 
        $image = PostImage::create(['image' => /*$path*/ $filename, 'post_id' => $post->id]);
 
        ProcessImageSmall::dispatch($image);
 
        return back()->with('status', 'Imagen cargada con exito');
 
    }

Desde aquí; si te fijas bien en el código, verás que definimos una instancia de nuestra cola con el método de dispatch para despachar el trabajo ProcessImageSmall::dispatch($image)

Levantar el demonio para procesar las colas

Ahora si revisamos la tabla anterior, veremos que se registró un trabajo, pero el mismo no ha sido despachado, para despachar el mismo, tenemos que activar el demonio que se encarga de hacer esta labor, levantar el proceso que se encarga de hacer los trabajos pendientes en la tabla jobs:

php artisan queue:work

Y con esto veremos que tenemos todo listo y el trabajo ya fue despachado:

Despachar trabajo

Con esto, aprendimos a emplear los trabajos y las colas en Laravel; recuerda que esto es uno de los muchísimos temas que tratamos en profundidad en nuestro curso de Laravel que puedes tomar en esta plataforma en la sección de cursos.

- Andrés Cruz

In english
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.