- Introducción
- Creación de Jobs
- Middleware para Jobs
- Despacho de Jobs
- Agrupación de Jobs
- Colas con closures
- Ejecución del worker de la cola
- Configuración de Supervisor
- Manejo de jobs fallidos
- Limpieza de jobs en colas
- Monitoreo de sus colas
- Pruebas
- Eventos de jobs
#Introducción
Al construir su aplicación web, puede tener tareas, como analizar y almacenar un archivo CSV subido, que tardan demasiado en realizarse durante una solicitud web típica. Afortunadamente, Laravel le permite crear fácilmente jobs en cola que pueden procesarse en segundo plano. Al mover tareas intensivas en tiempo a una cola, su aplicación puede responder a las solicitudes web con gran rapidez y ofrecer una mejor experiencia de usuario a sus clientes.
Las colas de Laravel proporcionan una API unificada para colas a través de una variedad de backends de cola diferentes, como Amazon SQS, Redis, o incluso una base de datos relacional.
Las opciones de configuración de la cola de Laravel se almacenan en el archivo de configuración config/queue.php de su aplicación. En este archivo, encontrará configuraciones de conexión para cada uno de los drivers de cola incluidos en el framework, incluyendo los drivers de base de datos, Amazon SQS, Redis y Beanstalkd, así como un driver síncrono que ejecuta los jobs inmediatamente (para uso durante el desarrollo local). También se incluye un driver null que descarta los jobs en cola.
Laravel ahora ofrece Horizon, un hermoso panel y sistema de configuración para sus colas impulsadas por Redis. Consulte la documentación completa de Horizon para más información.
#Conexiones vs. Colas
Antes de comenzar con las colas de Laravel, es importante entender la distinción entre "conexiones" y "colas". En el archivo de configuración config/queue.php, hay un arreglo de configuración connections. Esta opción define las conexiones a servicios de cola backend como Amazon SQS, Beanstalk o Redis. Sin embargo, una conexión de cola dada puede tener múltiples "colas", que pueden considerarse como diferentes pilas o montones de jobs en cola.
Tenga en cuenta que cada ejemplo de configuración de conexión en el archivo queue contiene un atributo queue. Esta es la cola predeterminada a la que se enviarán los jobs cuando se despachen a una conexión dada. En otras palabras, si despacha un job sin definir explícitamente a qué cola debe enviarse, el job se colocará en la cola definida en el atributo queue de la configuración de la conexión:
use App\Jobs\ProcessPodcast;
// Este job se envía a la cola predeterminada de la conexión predeterminada...
ProcessPodcast::dispatch();
// Este job se envía a la cola "emails" de la conexión predeterminada...
ProcessPodcast::dispatch()->onQueue('emails');
Algunas aplicaciones pueden no necesitar enviar jobs a múltiples colas, prefiriendo tener una cola simple. Sin embargo, enviar jobs a múltiples colas puede ser especialmente útil para aplicaciones que desean priorizar o segmentar cómo se procesan los jobs, ya que el worker de la cola de Laravel permite especificar qué colas debe procesar por prioridad. Por ejemplo, si envía jobs a una cola high, puede ejecutar un worker que les dé mayor prioridad de procesamiento:
php artisan queue:work --queue=high,default
#Notas del driver y requisitos previos
#Base de datos
Para usar el driver de cola database, necesitará una tabla en la base de datos para almacenar los jobs. Para generar una migración que cree esta tabla, ejecute el comando Artisan queue:table. Una vez creada la migración, puede migrar su base de datos usando el comando migrate:
php artisan queue:table
php artisan migrate
Finalmente, no olvide indicar a su aplicación que use el driver database actualizando la variable QUEUE_CONNECTION en el archivo .env de su aplicación:
QUEUE_CONNECTION=database
#Redis
Para usar el driver de cola redis, debe configurar una conexión a base de datos Redis en el archivo de configuración config/database.php.
Las opciones serializer y compression de Redis no son compatibles con el driver de cola redis.
Cluster de Redis
Si su conexión de cola Redis usa un Cluster de Redis, los nombres de sus colas deben contener una etiqueta de hash de clave. Esto es necesario para asegurar que todas las claves Redis para una cola dada se coloquen en la misma ranura hash:
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => '{default}',
'retry_after' => 90,
],
Bloqueo
Al usar la cola Redis, puede usar la opción de configuración block_for para especificar cuánto tiempo debe esperar el driver para que un job esté disponible antes de iterar el ciclo del worker y volver a consultar la base de datos Redis.
Ajustar este valor según la carga de su cola puede ser más eficiente que consultar continuamente la base de datos Redis en busca de nuevos jobs. Por ejemplo, puede establecer el valor en 5 para indicar que el driver debe bloquearse durante cinco segundos mientras espera que un job esté disponible:
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => 5,
],
Establecer block_for en 0 hará que los workers de cola se bloqueen indefinidamente hasta que un job esté disponible. Esto también impedirá que señales como SIGTERM sean manejadas hasta que se procese el siguiente job.
#Otros requisitos previos del driver
Las siguientes dependencias son necesarias para los drivers de cola listados. Estas dependencias pueden instalarse mediante el gestor de paquetes Composer:
- Amazon SQS:
aws/aws-sdk-php ~3.0 - Beanstalkd:
pda/pheanstalk ~4.0 - Redis:
predis/predis ~1.0o la extensión PHP phpredis
#Creación de Jobs
#Generación de clases de Job
Por defecto, todos los jobs en cola de su aplicación se almacenan en el directorio app/Jobs. Si el directorio app/Jobs no existe, se creará cuando ejecute el comando Artisan make:job:
php artisan make:job ProcessPodcast
La clase generada implementará la interfaz Illuminate\Contracts\Queue\ShouldQueue, indicando a Laravel que el job debe enviarse a la cola para ejecutarse de forma asíncrona.
Los stubs de jobs pueden personalizarse usando la publicación de stubs.
#Estructura de la clase
Las clases de job son muy simples, normalmente contienen solo un método handle que se invoca cuando el job es procesado por la cola. Para comenzar, veamos un ejemplo de clase de job. En este ejemplo, fingiremos que gestionamos un servicio de publicación de podcasts y necesitamos procesar los archivos de podcast subidos antes de publicarlos:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Crear una nueva instancia del job.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Ejecutar el job.
*/
public function handle(AudioProcessor $processor): void
{
// Procesar el podcast subido...
}
}
En este ejemplo, note que pudimos pasar un modelo Eloquent directamente al constructor del job en cola. Debido al trait SerializesModels que usa el job, los modelos Eloquent y sus relaciones cargadas se serializan y deserializan de forma elegante cuando el job se procesa.
Si su job en cola acepta un modelo Eloquent en su constructor, solo el identificador del modelo se serializará en la cola. Cuando el job se maneja realmente, el sistema de colas recuperará automáticamente la instancia completa del modelo y sus relaciones cargadas desde la base de datos. Este enfoque para la serialización de modelos permite enviar cargas útiles mucho más pequeñas a su driver de cola.
#Inyección de dependencias en el método handle
El método handle se invoca cuando el job es procesado por la cola. Note que podemos indicar dependencias en el método handle del job. El contenedor de servicios de Laravel inyecta automáticamente estas dependencias.
Si desea tener control total sobre cómo el contenedor inyecta dependencias en el método handle, puede usar el método bindMethod del contenedor. El método bindMethod acepta un callback que recibe el job y el contenedor. Dentro del callback, puede invocar el método handle como desee. Normalmente, debería llamar a este método desde el método boot de su service provider App\Providers\AppServiceProvider:
use App\Jobs\ProcessPodcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Foundation\Application;
$this->app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) {
return $job->handle($app->make(AudioProcessor::class));
});
Los datos binarios, como el contenido bruto de imágenes, deben pasarse a través de la función base64_encode antes de enviarlos a un job en cola. De lo contrario, el job puede no serializarse correctamente a JSON al colocarse en la cola.
#Relaciones en cola
Debido a que todas las relaciones cargadas de modelos Eloquent también se serializan cuando un job se pone en cola, la cadena serializada del job puede volverse bastante grande. Además, cuando un job se deserializa y las relaciones del modelo se vuelven a recuperar de la base de datos, se recuperan en su totalidad. Cualquier restricción previa aplicada a la relación antes de que el modelo se serializara durante el proceso de encolado no se aplicará cuando el job se deserialice. Por lo tanto, si desea trabajar con un subconjunto de una relación dada, debe volver a restringir esa relación dentro de su job en cola.
O, para evitar que las relaciones se serialicen, puede llamar al método withoutRelations en el modelo al establecer un valor de propiedad. Este método devolverá una instancia del modelo sin sus relaciones cargadas:
/**
* Crear una nueva instancia del job.
*/
public function __construct(Podcast $podcast)
{
$this->podcast = $podcast->withoutRelations();
}
Si usa la promoción de propiedades en el constructor de PHP y desea indicar que un modelo Eloquent no debe tener sus relaciones serializadas, puede usar el atributo WithoutRelations:
use Illuminate\Queue\Attributes\WithoutRelations;
/**
* Crear una nueva instancia del job.
*/
public function __construct(
#[WithoutRelations]
public Podcast $podcast
) {
}
Si un job recibe una colección o arreglo de modelos Eloquent en lugar de un solo modelo, los modelos dentro de esa colección no tendrán sus relaciones restauradas cuando el job se deserialice y ejecute. Esto es para evitar un uso excesivo de recursos en jobs que manejan grandes cantidades de modelos.
#Jobs únicos
Los jobs únicos requieren un driver de caché que soporte locks. Actualmente, los drivers de caché memcached, redis, dynamodb, database, file y array soportan locks atómicos. Además, las restricciones de jobs únicos no se aplican a jobs dentro de lotes.
A veces, puede querer asegurarse de que solo una instancia de un job específico esté en la cola en cualquier momento. Puede hacerlo implementando la interfaz ShouldBeUnique en su clase de job. Esta interfaz no requiere que defina métodos adicionales en su clase:
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
...
}
En el ejemplo anterior, el job UpdateSearchIndex es único. Por lo tanto, el job no se despachará si otra instancia del job ya está en la cola y no ha terminado de procesarse.
En ciertos casos, puede querer definir una "clave" específica que haga único al job o puede querer especificar un timeout tras el cual el job ya no se mantenga único. Para lograr esto, puede definir propiedades o métodos uniqueId y uniqueFor en su clase de job:
<?php
use App\Models\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
/**
* La instancia del producto.
*
* @var \App\Product
*/
public $product;
/**
* El número de segundos tras los cuales se liberará el lock único del job.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Obtener el ID único para el job.
*/
public function uniqueId(): string
{
return $this->product->id;
}
}
En el ejemplo anterior, el job UpdateSearchIndex es único por el ID del producto. Por lo tanto, cualquier nuevo despacho del job con el mismo ID de producto será ignorado hasta que el job existente haya completado su procesamiento. Además, si el job existente no se procesa dentro de una hora, el lock único se liberará y otro job con la misma clave única podrá despacharse a la cola.
Si su aplicación despacha jobs desde múltiples servidores web o contenedores, debe asegurarse de que todos sus servidores se comuniquen con el mismo servidor de caché central para que Laravel pueda determinar con precisión si un job es único.
#Mantener jobs únicos hasta que comience el procesamiento
Por defecto, los jobs únicos se "desbloquean" después de que un job completa su procesamiento o falla todos sus intentos de reintento. Sin embargo, puede haber situaciones donde desee que su job se desbloquee inmediatamente antes de ser procesado. Para lograr esto, su job debe implementar el contrato ShouldBeUniqueUntilProcessing en lugar del contrato ShouldBeUnique:
<?php
use App\Models\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}
#Locks para jobs únicos
En segundo plano, cuando se despacha un job ShouldBeUnique, Laravel intenta adquirir un lock con la clave uniqueId. Si el lock no se adquiere, el job no se despacha. Este lock se libera cuando el job completa su procesamiento o falla todos sus intentos de reintento. Por defecto, Laravel usará el driver de caché predeterminado para obtener este lock. Sin embargo, si desea usar otro driver para adquirir el lock, puede definir un método uniqueVia que devuelva el driver de caché que debe usarse:
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
...
/**
* Obtener el driver de caché para el lock único del job.
*/
public function uniqueVia(): Repository
{
return Cache::driver('redis');
}
}
Si solo necesita limitar el procesamiento concurrente de un job, use el middleware de job WithoutOverlapping en su lugar.
#Jobs cifrados
Laravel le permite asegurar la privacidad e integridad de los datos de un job mediante cifrado. Para comenzar, simplemente agregue la interfaz ShouldBeEncrypted a la clase del job. Una vez que esta interfaz se agrega a la clase, Laravel cifrará automáticamente su job antes de enviarlo a la cola:
<?php
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
// ...
}
#Middleware para Jobs
El middleware para jobs le permite envolver lógica personalizada alrededor de la ejecución de jobs en cola, reduciendo el código repetitivo en los propios jobs. Por ejemplo, considere el siguiente método handle que aprovecha las funciones de limitación de tasa de Redis de Laravel para permitir que solo un job se procese cada cinco segundos:
use Illuminate\Support\Facades\Redis;
/**
* Ejecutar el job.
*/
public function handle(): void
{
Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
info('Lock obtenido...');
// Manejar job...
}, function () {
// No se pudo obtener el lock...
return $this->release(5);
});
}
Aunque este código es válido, la implementación del método handle se vuelve ruidosa porque está llena de lógica de limitación de tasa de Redis. Además, esta lógica debe duplicarse para cualquier otro job que queramos limitar.
En lugar de limitar la tasa en el método handle, podríamos definir un middleware para jobs que maneje la limitación de tasa. Laravel no tiene una ubicación predeterminada para middleware de jobs, por lo que puede colocar el middleware donde desee en su aplicación. En este ejemplo, colocaremos el middleware en un directorio app/Jobs/Middleware:
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RateLimited
{
/**
* Procesar el job en cola.
*
* @param \Closure(object): void $next
*/
public function handle(object $job, Closure $next): void
{
Redis::throttle('key')
->block(0)->allow(1)->every(5)
->then(function () use ($job, $next) {
// Lock obtenido...
$next($job);
}, function () use ($job) {
// No se pudo obtener el lock...
$job->release(5);
});
}
}
Como puede ver, al igual que el middleware de rutas, el middleware para jobs recibe el job que se está procesando y un callback que debe invocarse para continuar con el procesamiento del job.
Después de crear middleware para jobs, pueden asignarse a un job devolviéndolos desde el método middleware del job. Este método no existe en los jobs generados por el comando Artisan make:job, por lo que deberá agregarlo manualmente a su clase de job:
use App\Jobs\Middleware\RateLimited;
/**
* Obtener el middleware por el que debe pasar el job.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited];
}
El middleware para jobs también puede asignarse a listeners de eventos encolables, mailables y notificaciones.
#Limitación de tasa
Aunque acabamos de demostrar cómo escribir su propio middleware de limitación de tasa para jobs, Laravel incluye un middleware de limitación de tasa que puede utilizar para limitar la tasa de jobs. Al igual que los limitadores de tasa de rutas, los limitadores de tasa para jobs se definen usando el método for del facade RateLimiter.
Por ejemplo, puede querer permitir que los usuarios hagan una copia de seguridad de sus datos una vez por hora mientras no impone tal límite a los clientes premium. Para lograr esto, puede definir un RateLimiter en el método boot de su AppServiceProvider:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
/**
* Inicializar cualquier servicio de la aplicación.
*/
public function boot(): void
{
RateLimiter::for('backups', function (object $job) {
return $job->user->vipCustomer()
? Limit::none()
: Limit::perHour(1)->by($job->user->id);
});
}
En el ejemplo anterior, definimos un límite de tasa por hora; sin embargo, puede definir fácilmente un límite basado en minutos usando el método perMinute. Además, puede pasar cualquier valor que desee al método by del límite de tasa; sin embargo, este valor se usa más comúnmente para segmentar límites por cliente:
return Limit::perMinute(50)->by($job->user->id);
Una vez que haya definido su límite de tasa, puede adjuntar el limitador de tasa a su job usando el middleware Illuminate\Queue\Middleware\RateLimited. Cada vez que el job exceda el límite de tasa, este middleware liberará el job de nuevo a la cola con un retraso apropiado basado en la duración del límite de tasa.
use Illuminate\Queue\Middleware\RateLimited;
/**
* Obtener el middleware por el que debe pasar el job.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('backups')];
}
Liberar un job limitado por tasa de nuevo a la cola incrementará el número total de attempts del job. Puede querer ajustar las propiedades tries y maxExceptions en su clase de job en consecuencia. O puede usar el método retryUntil para definir el tiempo hasta el cual el job ya no debe intentarse.
Si no desea que un job sea reintentado cuando está limitado por tasa, puede usar el método dontRelease:
/**
* Obtener el middleware por el que debe pasar el job.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new RateLimited('backups'))->dontRelease()];
}
Si está utilizando Redis, puede usar el middleware Illuminate\Queue\Middleware\RateLimitedWithRedis, que está optimizado para Redis y es más eficiente que el middleware básico de limitación de tasa.
#Prevención de solapamiento de trabajos
Laravel incluye un middleware Illuminate\Queue\Middleware\WithoutOverlapping que le permite evitar solapamientos de trabajos basados en una clave arbitraria. Esto puede ser útil cuando un trabajo en cola está modificando un recurso que solo debe ser modificado por un trabajo a la vez.
Por ejemplo, imaginemos que tiene un trabajo en cola que actualiza el puntaje crediticio de un usuario y desea evitar solapamientos de trabajos de actualización del puntaje para el mismo ID de usuario. Para lograr esto, puede devolver el middleware WithoutOverlapping desde el método middleware de su trabajo:
use Illuminate\Queue\Middleware\WithoutOverlapping;
/**
* Obtenga el middleware por el que debe pasar el trabajo.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new WithoutOverlapping($this->user->id)];
}
Cualquier trabajo solapado del mismo tipo será liberado de nuevo a la cola. También puede especificar el número de segundos que deben transcurrir antes de que el trabajo liberado sea intentado nuevamente:
/**
* Obtenga el middleware por el que debe pasar el trabajo.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)];
}
Si desea eliminar inmediatamente cualquier trabajo solapado para que no se reintente, puede usar el método dontRelease:
/**
* Obtenga el middleware por el que debe pasar el trabajo.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->dontRelease()];
}
El middleware WithoutOverlapping está impulsado por la función de bloqueo atómico de Laravel. A veces, su trabajo puede fallar inesperadamente o agotarse el tiempo de espera de tal manera que el bloqueo no se libera. Por lo tanto, puede definir explícitamente un tiempo de expiración del bloqueo usando el método expireAfter. Por ejemplo, el siguiente ejemplo indicará a Laravel que libere el bloqueo WithoutOverlapping tres minutos después de que el trabajo haya comenzado a procesarse:
/**
* Obtenga el middleware por el que debe pasar el trabajo.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->expireAfter(180)];
}
El middleware WithoutOverlapping requiere un driver de caché que soporte locks. Actualmente, los drivers de caché memcached, redis, dynamodb, database, file y array soportan bloqueos atómicos.
#Compartir claves de bloqueo entre clases de trabajo
Por defecto, el middleware WithoutOverlapping solo previene solapamientos de trabajos de la misma clase. Por lo tanto, aunque dos clases de trabajo diferentes usen la misma clave de bloqueo, no se evitará que se solapen. Sin embargo, puede indicar a Laravel que aplique la clave entre clases de trabajo usando el método shared:
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProviderIsDown
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}
class ProviderIsUp
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}
#Limitación de excepciones
Laravel incluye un middleware Illuminate\Queue\Middleware\ThrottlesExceptions que le permite limitar la frecuencia de excepciones. Una vez que el trabajo lanza un número dado de excepciones, todos los intentos posteriores de ejecutar el trabajo se retrasan hasta que transcurra un intervalo de tiempo especificado. Este middleware es particularmente útil para trabajos que interactúan con servicios externos inestables.
Por ejemplo, imaginemos un trabajo en cola que interactúa con una API externa que comienza a lanzar excepciones. Para limitar las excepciones, puede devolver el middleware ThrottlesExceptions desde el método middleware de su trabajo. Normalmente, este middleware debe combinarse con un trabajo que implemente intentos basados en tiempo:
use DateTime;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Obtenga el middleware por el que debe pasar el trabajo.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new ThrottlesExceptions(10, 5)];
}
/**
* Determine el tiempo en el que el trabajo debe agotar el tiempo de espera.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}
El primer argumento del constructor aceptado por el middleware es el número de excepciones que el trabajo puede lanzar antes de ser limitado, mientras que el segundo argumento es el número de minutos que deben transcurrir antes de que el trabajo sea intentado nuevamente una vez que ha sido limitado. En el ejemplo de código anterior, si el trabajo lanza 10 excepciones en 5 minutos, esperaremos 5 minutos antes de intentar el trabajo nuevamente.
Cuando un trabajo lanza una excepción pero aún no se ha alcanzado el umbral de excepciones, el trabajo normalmente se reintentará inmediatamente. Sin embargo, puede especificar el número de minutos que debe retrasarse un trabajo así llamando al método backoff al adjuntar el middleware al trabajo:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Obtenga el middleware por el que debe pasar el trabajo.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5))->backoff(5)];
}
Internamente, este middleware usa el sistema de caché de Laravel para implementar la limitación de tasa, y el nombre de la clase del trabajo se utiliza como la "clave" de caché. Puede sobrescribir esta clave llamando al método by al adjuntar el middleware a su trabajo. Esto puede ser útil si tiene múltiples trabajos que interactúan con el mismo servicio externo y desea que compartan un "bucket" común de limitación:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Obtenga el middleware por el que debe pasar el trabajo.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10))->by('key')];
}
Si está utilizando Redis, puede usar el middleware Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis, que está optimizado para Redis y es más eficiente que el middleware básico de limitación de excepciones.
#Envío de trabajos
Una vez que haya escrito su clase de trabajo, puede enviarla usando el método dispatch en el propio trabajo. Los argumentos pasados al método dispatch serán entregados al constructor del trabajo:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Almacenar un nuevo podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast);
return redirect('/podcasts');
}
}
Si desea enviar un trabajo condicionalmente, puede usar los métodos dispatchIf y dispatchUnless:
ProcessPodcast::dispatchIf($accountActive, $podcast);
ProcessPodcast::dispatchUnless($accountSuspended, $podcast);
En las nuevas aplicaciones Laravel, el driver sync es el driver de cola predeterminado. Este driver ejecuta los trabajos de forma síncrona en primer plano durante la solicitud actual, lo cual es conveniente durante el desarrollo local. Si desea comenzar a poner trabajos en cola para procesamiento en segundo plano, puede especificar un driver de cola diferente en el archivo de configuración config/queue.php de su aplicación.
#Envío diferido
Si desea especificar que un trabajo no esté disponible inmediatamente para ser procesado por un worker de cola, puede usar el método delay al enviar el trabajo. Por ejemplo, especifiquemos que un trabajo no esté disponible para procesamiento hasta 10 minutos después de haber sido enviado:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Almacenar un nuevo podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast)
->delay(now()->addMinutes(10));
return redirect('/podcasts');
}
}
El servicio de cola Amazon SQS tiene un tiempo máximo de retraso de 15 minutos.
#Envío después de que la respuesta se envía al navegador
Alternativamente, el método dispatchAfterResponse retrasa el envío de un trabajo hasta después de que la respuesta HTTP se haya enviado al navegador del usuario si su servidor web usa FastCGI. Esto permitirá que el usuario comience a usar la aplicación aunque un trabajo en cola aún se esté ejecutando. Esto normalmente solo debe usarse para trabajos que toman alrededor de un segundo, como enviar un correo electrónico. Dado que se procesan dentro de la solicitud HTTP actual, los trabajos enviados de esta manera no requieren que un worker de cola esté ejecutándose para ser procesados:
use App\Jobs\SendNotification;
SendNotification::dispatchAfterResponse();
También puede dispatch una closure y encadenar el método afterResponse al helper dispatch para ejecutar una closure después de que la respuesta HTTP haya sido enviada al navegador:
use App\Mail\WelcomeMessage;
use Illuminate\Support\Facades\Mail;
dispatch(function () {
Mail::to('taylor@example.com')->send(new WelcomeMessage);
})->afterResponse();
#Envío síncrono
Si desea enviar un trabajo inmediatamente (de forma síncrona), puede usar el método dispatchSync. Al usar este método, el trabajo no se pondrá en cola y se ejecutará inmediatamente dentro del proceso actual:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Almacenar un nuevo podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Crear podcast...
ProcessPodcast::dispatchSync($podcast);
return redirect('/podcasts');
}
}
#Trabajos y transacciones de base de datos
Aunque está perfectamente bien enviar trabajos dentro de transacciones de base de datos, debe tener especial cuidado para asegurarse de que su trabajo pueda ejecutarse correctamente. Cuando se envía un trabajo dentro de una transacción, es posible que el trabajo sea procesado por un worker antes de que la transacción principal haya sido confirmada. Cuando esto sucede, cualquier actualización que haya hecho a modelos o registros de base de datos durante la transacción puede que aún no se refleje en la base de datos. Además, cualquier modelo o registro creado dentro de la transacción puede que no exista aún en la base de datos.
Afortunadamente, Laravel proporciona varios métodos para evitar este problema. Primero, puede establecer la opción de conexión after_commit en el arreglo de configuración de su conexión de cola:
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],
Cuando la opción after_commit es true, puede enviar trabajos dentro de transacciones de base de datos; sin embargo, Laravel esperará hasta que las transacciones principales abiertas hayan sido confirmadas antes de enviar realmente el trabajo. Por supuesto, si no hay transacciones abiertas, el trabajo se enviará inmediatamente.
Si una transacción es revertida debido a una excepción que ocurre durante la transacción, los trabajos que fueron enviados durante esa transacción serán descartados.
Establecer la opción de configuración after_commit en true también hará que cualquier listener de eventos en cola, mailables, notificaciones y eventos de broadcast se envíen después de que todas las transacciones abiertas de base de datos hayan sido confirmadas.
#Especificar el comportamiento de envío tras commit en línea
Si no establece la opción de configuración after_commit en true, aún puede indicar que un trabajo específico debe enviarse después de que todas las transacciones abiertas hayan sido confirmadas. Para lograr esto, puede encadenar el método afterCommit a su operación de envío:
use App\Jobs\ProcessPodcast;
ProcessPodcast::dispatch($podcast)->afterCommit();
De igual forma, si la opción de configuración after_commit está establecida en true, puede indicar que un trabajo específico debe enviarse inmediatamente sin esperar a que se confirmen las transacciones abiertas:
ProcessPodcast::dispatch($podcast)->beforeCommit();
#Encadenamiento de trabajos
El encadenamiento de trabajos le permite especificar una lista de trabajos en cola que deben ejecutarse en secuencia después de que el trabajo principal se haya ejecutado con éxito. Si un trabajo en la secuencia falla, el resto de los trabajos no se ejecutarán. Para ejecutar una cadena de trabajos en cola, puede usar el método chain proporcionado por el facade Bus. El bus de comandos de Laravel es un componente de bajo nivel sobre el que se construye el envío de trabajos en cola:
use App\Jobs\OptimizePodcast;
use App\Jobs\ProcessPodcast;
use App\Jobs\ReleasePodcast;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->dispatch();
Además de encadenar instancias de clases de trabajo, también puede encadenar closures:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
function () {
Podcast::update(/* ... */);
},
])->dispatch();
Eliminar trabajos usando el método $this->delete() dentro del trabajo no evitará que los trabajos encadenados se procesen. La cadena solo dejará de ejecutarse si un trabajo en la cadena falla.
#Conexión y cola de la cadena
Si desea especificar la conexión y la cola que deben usarse para los trabajos encadenados, puede usar los métodos onConnection y onQueue. Estos métodos especifican la conexión de cola y el nombre de la cola que deben usarse a menos que el trabajo en cola tenga explícitamente asignada una conexión o cola diferente:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->onConnection('redis')->onQueue('podcasts')->dispatch();
#Fallos en la cadena
Al encadenar trabajos, puede usar el método catch para especificar una closure que debe invocarse si un trabajo dentro de la cadena falla. El callback dado recibirá la instancia Throwable que causó el fallo del trabajo:
use Illuminate\Support\Facades\Bus;
use Throwable;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->catch(function (Throwable $e) {
// Un trabajo dentro de la cadena ha fallado...
})->dispatch();
Dado que los callbacks de cadena se serializan y ejecutan más tarde por la cola de Laravel, no debe usar la variable $this dentro de los callbacks de cadena.
#Personalizando la cola de una conexión
#Envío a una cola específica
Al enviar trabajos a diferentes colas, puede "categorizar" sus trabajos en cola e incluso priorizar cuántos workers asigna a varias colas. Tenga en cuenta que esto no envía trabajos a diferentes "conexiones" de cola definidas en su archivo de configuración de colas, sino solo a colas específicas dentro de una sola conexión. Para especificar la cola, use el método onQueue al enviar el trabajo:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Almacenar un nuevo podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Crear podcast...
ProcessPodcast::dispatch($podcast)->onQueue('processing');
return redirect('/podcasts');
}
}
Alternativamente, puede especificar la cola del trabajo llamando al método onQueue dentro del constructor del trabajo:
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Crear una nueva instancia de trabajo.
*/
public function __construct()
{
$this->onQueue('processing');
}
}
#Envío a una conexión específica
Si su aplicación interactúa con múltiples conexiones de cola, puede especificar a qué conexión enviar un trabajo usando el método onConnection:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Almacenar un nuevo podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Crear podcast...
ProcessPodcast::dispatch($podcast)->onConnection('sqs');
return redirect('/podcasts');
}
}
Puede encadenar los métodos onConnection y onQueue juntos para especificar la conexión y la cola para un trabajo:
ProcessPodcast::dispatch($podcast)
->onConnection('sqs')
->onQueue('processing');
Alternativamente, puede especificar la conexión del trabajo llamando al método onConnection dentro del constructor del trabajo:
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Crear una nueva instancia de trabajo.
*/
public function __construct()
{
$this->onConnection('sqs');
}
}
#Especificar intentos máximos / valores de timeout
#Intentos máximos
Si uno de sus trabajos en cola está encontrando un error, probablemente no quiera que siga intentando indefinidamente. Por lo tanto, Laravel proporciona varias formas de especificar cuántas veces o por cuánto tiempo un trabajo puede ser intentado.
Una forma de especificar el número máximo de intentos de un trabajo es mediante el interruptor --tries en la línea de comandos Artisan. Esto se aplicará a todos los trabajos procesados por el worker a menos que el trabajo que se está procesando especifique el número de intentos permitidos:
php artisan queue:work --tries=3
Si un trabajo excede su número máximo de intentos, será considerado un trabajo "fallido". Para más información sobre cómo manejar trabajos fallidos, consulte la documentación de trabajos fallidos. Si se proporciona --tries=0 al comando queue:work, el trabajo se reintentará indefinidamente.
Puede tomar un enfoque más granular definiendo el número máximo de intentos en la propia clase del trabajo. Si el número máximo de intentos está especificado en el trabajo, tendrá prioridad sobre el valor --tries proporcionado en la línea de comandos:
<?php
namespace App\Jobs;
class ProcessPodcast implements ShouldQueue
{
/**
* El número de veces que el trabajo puede ser intentado.
*
* @var int
*/
public $tries = 5;
}
Si necesita control dinámico sobre los intentos máximos de un trabajo en particular, puede definir un método tries en el trabajo:
/**
* Determine el número de veces que el trabajo puede ser intentado.
*/
public function tries(): int
{
return 5;
}
#Intentos basados en tiempo
Como alternativa a definir cuántas veces un trabajo puede ser intentado antes de fallar, puede definir un tiempo hasta el cual el trabajo ya no debe ser intentado. Esto permite que un trabajo sea intentado cualquier número de veces dentro de un marco temporal dado. Para definir el tiempo hasta el cual un trabajo debe ser intentado, agregue un método retryUntil a su clase de trabajo. Este método debe devolver una instancia DateTime:
use DateTime;
/**
* Determine el tiempo en el que el trabajo debe agotar el tiempo de espera.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(10);
}
También puede definir una propiedad tries o un método retryUntil en sus listeners de eventos en cola.
#Excepciones máximas
A veces puede querer especificar que un trabajo puede ser intentado muchas veces, pero debe fallar si los reintentos son causados por un número dado de excepciones no manejadas (en lugar de ser liberado directamente por el método release). Para lograr esto, puede definir una propiedad maxExceptions en su clase de trabajo:
<?php
namespace App\Jobs;
use Illuminate\Support\Facades\Redis;
class ProcessPodcast implements ShouldQueue
{
/**
* El número de veces que el trabajo puede ser intentado.
*
* @var int
*/
public $tries = 25;
/**
* El número máximo de excepciones no manejadas permitidas antes de fallar.
*
* @var int
*/
public $maxExceptions = 3;
/**
* Ejecutar el trabajo.
*/
public function handle(): void
{
Redis::throttle('key')->allow(10)->every(60)->then(function () {
// Bloqueo obtenido, procesar el podcast...
}, function () {
// No se pudo obtener el bloqueo...
return $this->release(10);
});
}
}
En este ejemplo, el trabajo se libera por diez segundos si la aplicación no puede obtener un bloqueo Redis y continuará siendo reintentado hasta 25 veces. Sin embargo, el trabajo fallará si se lanzan tres excepciones no manejadas.
#Timeout
A menudo, usted sabe aproximadamente cuánto tiempo espera que tomen sus trabajos en cola. Por esta razón, Laravel le permite especificar un valor de "timeout". Por defecto, el valor de timeout es de 60 segundos. Si un trabajo se está procesando por más tiempo que el número de segundos especificado por el valor de timeout, el worker que procesa el trabajo terminará con un error. Normalmente, el worker será reiniciado automáticamente por un gestor de procesos configurado en su servidor.
El número máximo de segundos que los trabajos pueden ejecutarse puede especificarse usando el interruptor --timeout en la línea de comandos Artisan:
php artisan queue:work --timeout=30
Si el trabajo excede sus intentos máximos por agotamiento continuo del tiempo de espera, será marcado como fallido.
También puede definir el número máximo de segundos que un trabajo puede ejecutarse en la propia clase del trabajo. Si el timeout está especificado en el trabajo, tendrá prioridad sobre cualquier timeout especificado en la línea de comandos:
<?php
namespace App\Jobs;
class ProcessPodcast implements ShouldQueue
{
/**
* El número de segundos que el trabajo puede ejecutarse antes de agotar el tiempo.
*
* @var int
*/
public $timeout = 120;
}
A veces, los procesos de bloqueo de E/S como sockets o conexiones HTTP salientes pueden no respetar el tiempo de espera especificado. Por lo tanto, al usar estas funciones, siempre debe intentar especificar un tiempo de espera utilizando sus APIs también. Por ejemplo, al usar Guzzle, siempre debe especificar un valor de tiempo de espera para la conexión y la solicitud.
La extensión PHP pcntl debe estar instalada para poder especificar los tiempos de espera de los trabajos. Además, el valor de "timeout" de un trabajo siempre debe ser menor que su valor de "retry after". De lo contrario, el trabajo podría reintentarse antes de que realmente haya terminado de ejecutarse o haya expirado el tiempo de espera.
#Fallar al expirar el tiempo de espera
Si desea indicar que un trabajo debe marcarse como fallido cuando expire el tiempo de espera, puede definir la propiedad $failOnTimeout en la clase del trabajo:
/**
* Indica si el trabajo debe marcarse como fallido al expirar el tiempo de espera.
*
* @var bool
*/
public $failOnTimeout = true;
#Manejo de errores
Si se lanza una excepción mientras se procesa el trabajo, este se liberará automáticamente de nuevo en la cola para que pueda intentarse otra vez. El trabajo seguirá siendo liberado hasta que se haya intentado el número máximo de veces permitido por su aplicación. El número máximo de intentos se define con el interruptor --tries usado en el comando Artisan queue:work. Alternativamente, el número máximo de intentos puede definirse en la propia clase del trabajo. Más información sobre cómo ejecutar el worker de la cola se encuentra más abajo.
#Liberar manualmente un trabajo
A veces puede que desee liberar manualmente un trabajo de nuevo en la cola para que pueda intentarse más tarde. Puede lograr esto llamando al método release:
/**
* Ejecutar el trabajo.
*/
public function handle(): void
{
// ...
$this->release();
}
Por defecto, el método release liberará el trabajo de nuevo en la cola para su procesamiento inmediato. Sin embargo, puede indicar a la cola que no haga disponible el trabajo para procesamiento hasta que haya transcurrido un número dado de segundos pasando un entero o una instancia de fecha al método release:
$this->release(10);
$this->release(now()->addSeconds(10));
#Marcar manualmente un trabajo como fallido
Ocasionalmente puede necesitar marcar manualmente un trabajo como "fallido". Para hacerlo, puede llamar al método fail:
/**
* Ejecutar el trabajo.
*/
public function handle(): void
{
// ...
$this->fail();
}
Si desea marcar su trabajo como fallido debido a una excepción que ha capturado, puede pasar la excepción al método fail. O, para mayor comodidad, puede pasar un mensaje de error en forma de cadena que será convertido en una excepción por usted:
$this->fail($exception);
$this->fail('Something went wrong.');
Para más información sobre trabajos fallidos, consulte la documentación sobre cómo manejar fallos en trabajos.
#Agrupación de trabajos (Job Batching)
La función de agrupación de trabajos de Laravel le permite ejecutar fácilmente un lote de trabajos y luego realizar alguna acción cuando el lote haya terminado de ejecutarse. Antes de comenzar, debe crear una migración de base de datos para construir una tabla que contendrá información meta sobre sus lotes de trabajos, como su porcentaje de finalización. Esta migración puede generarse usando el comando Artisan queue:batches-table:
php artisan queue:batches-table
php artisan migrate
#Definir trabajos agrupables
Para definir un trabajo agrupable, debe crear un trabajo en cola como de costumbre; sin embargo, debe agregar el trait Illuminate\Bus\Batchable a la clase del trabajo. Este trait proporciona acceso a un método batch que puede usarse para obtener el lote actual en el que se está ejecutando el trabajo:
<?php
namespace App\Jobs;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ImportCsv implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Ejecutar el trabajo.
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
// Determinar si el lote ha sido cancelado...
return;
}
// Importar una porción del archivo CSV...
}
}
#Despacho de lotes
Para despachar un lote de trabajos, debe usar el método batch del facade Bus. Por supuesto, la agrupación es principalmente útil cuando se combina con callbacks de finalización. Por lo tanto, puede usar los métodos then, catch y finally para definir callbacks de finalización para el lote. Cada uno de estos callbacks recibirá una instancia de Illuminate\Bus\Batch cuando se invoquen. En este ejemplo, imaginaremos que estamos encolando un lote de trabajos que procesan un número dado de filas de un archivo CSV:
use App\Jobs\ImportCsv;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;
$batch = Bus::batch([
new ImportCsv(1, 100),
new ImportCsv(101, 200),
new ImportCsv(201, 300),
new ImportCsv(301, 400),
new ImportCsv(401, 500),
])->before(function (Batch $batch) {
// El lote ha sido creado pero no se han agregado trabajos...
})->progress(function (Batch $batch) {
// Un solo trabajo se ha completado con éxito...
})->then(function (Batch $batch) {
// Todos los trabajos se completaron con éxito...
})->catch(function (Batch $batch, Throwable $e) {
// Se detectó la primera falla en un trabajo del lote...
})->finally(function (Batch $batch) {
// El lote ha terminado de ejecutarse...
})->dispatch();
return $batch->id;
El ID del lote, al que se puede acceder mediante la propiedad $batch->id, puede usarse para consultar el bus de comandos de Laravel para obtener información sobre el lote después de que haya sido despachado.
Dado que los callbacks de lote se serializan y ejecutan más tarde por la cola de Laravel, no debe usar la variable $this dentro de los callbacks.
#Nombrar lotes
Algunas herramientas como Laravel Horizon y Laravel Telescope pueden proporcionar información de depuración más amigable para los lotes si estos tienen nombre. Para asignar un nombre arbitrario a un lote, puede llamar al método name mientras define el lote:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// Todos los trabajos se completaron con éxito...
})->name('Import CSV')->dispatch();
#Conexión y cola del lote
Si desea especificar la conexión y la cola que deben usarse para los trabajos agrupados, puede usar los métodos onConnection y onQueue. Todos los trabajos agrupados deben ejecutarse dentro de la misma conexión y cola:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// Todos los trabajos se completaron con éxito...
})->onConnection('redis')->onQueue('imports')->dispatch();
#Cadenas y lotes
Puede definir un conjunto de trabajos encadenados dentro de un lote colocando los trabajos encadenados dentro de un array. Por ejemplo, podemos ejecutar dos cadenas de trabajos en paralelo y ejecutar un callback cuando ambas cadenas hayan terminado de procesarse:
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
Bus::batch([
[
new ReleasePodcast(1),
new SendPodcastReleaseNotification(1),
],
[
new ReleasePodcast(2),
new SendPodcastReleaseNotification(2),
],
])->then(function (Batch $batch) {
// ...
})->dispatch();
Por otro lado, puede ejecutar lotes de trabajos dentro de una cadena definiendo lotes dentro de la cadena. Por ejemplo, podría primero ejecutar un lote de trabajos para lanzar múltiples podcasts y luego un lote de trabajos para enviar las notificaciones de lanzamiento:
use App\Jobs\FlushPodcastCache;
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new FlushPodcastCache,
Bus::batch([
new ReleasePodcast(1),
new ReleasePodcast(2),
]),
Bus::batch([
new SendPodcastReleaseNotification(1),
new SendPodcastReleaseNotification(2),
]),
])->dispatch();
#Añadir trabajos a lotes
A veces puede ser útil añadir trabajos adicionales a un lote desde dentro de un trabajo agrupado. Este patrón puede ser útil cuando necesita agrupar miles de trabajos que podrían tardar demasiado en despacharse durante una solicitud web. Por lo tanto, en su lugar, puede desear despachar un lote inicial de trabajos "cargadores" que hidraten el lote con aún más trabajos:
$batch = Bus::batch([
new LoadImportBatch,
new LoadImportBatch,
new LoadImportBatch,
])->then(function (Batch $batch) {
// Todos los trabajos se completaron con éxito...
})->name('Import Contacts')->dispatch();
En este ejemplo, usaremos el trabajo LoadImportBatch para hidratar el lote con trabajos adicionales. Para lograr esto, podemos usar el método add en la instancia del lote que puede accederse mediante el método batch del trabajo:
use App\Jobs\ImportContacts;
use Illuminate\Support\Collection;
/**
* Ejecutar el trabajo.
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
return;
}
$this->batch()->add(Collection::times(1000, function () {
return new ImportContacts;
}));
}
Solo puede añadir trabajos a un lote desde dentro de un trabajo que pertenezca al mismo lote.
#Inspeccionar lotes
La instancia Illuminate\Bus\Batch que se proporciona a los callbacks de finalización de lote tiene una variedad de propiedades y métodos para ayudarle a interactuar e inspeccionar un lote dado de trabajos:
// El UUID del lote...
$batch->id;
// El nombre del lote (si aplica)...
$batch->name;
// El número de trabajos asignados al lote...
$batch->totalJobs;
// El número de trabajos que no han sido procesados por la cola...
$batch->pendingJobs;
// El número de trabajos que han fallado...
$batch->failedJobs;
// El número de trabajos que se han procesado hasta ahora...
$batch->processedJobs();
// El porcentaje de finalización del lote (0-100)...
$batch->progress();
// Indica si el lote ha terminado de ejecutarse...
$batch->finished();
// Cancelar la ejecución del lote...
$batch->cancel();
// Indica si el lote ha sido cancelado...
$batch->cancelled();
#Devolver lotes desde rutas
Todas las instancias Illuminate\Bus\Batch son serializables a JSON, lo que significa que puede devolverlas directamente desde una de las rutas de su aplicación para obtener una carga JSON con información sobre el lote, incluyendo su progreso de finalización. Esto facilita mostrar información sobre el progreso del lote en la interfaz de usuario de su aplicación.
Para recuperar un lote por su ID, puede usar el método findBatch del facade Bus:
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Route;
Route::get('/batch/{batchId}', function (string $batchId) {
return Bus::findBatch($batchId);
});
#Cancelar lotes
A veces puede necesitar cancelar la ejecución de un lote dado. Esto puede lograrse llamando al método cancel en la instancia Illuminate\Bus\Batch:
/**
* Ejecutar el trabajo.
*/
public function handle(): void
{
if ($this->user->exceedsImportLimit()) {
return $this->batch()->cancel();
}
if ($this->batch()->cancelled()) {
return;
}
}
Como habrá notado en los ejemplos anteriores, los trabajos agrupados normalmente deberían determinar si su lote correspondiente ha sido cancelado antes de continuar con la ejecución. Sin embargo, para mayor comodidad, puede asignar el middleware SkipIfBatchCancelled al trabajo. Como su nombre indica, este middleware instruirá a Laravel para que no procese el trabajo si su lote correspondiente ha sido cancelado:
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
/**
* Obtener el middleware que debe pasar el trabajo.
*/
public function middleware(): array
{
return [new SkipIfBatchCancelled];
}
#Fallos en lotes
Cuando un trabajo agrupado falla, se invocará el callback catch (si está asignado). Este callback solo se invoca para el primer trabajo que falla dentro del lote.
#Permitir fallos
Cuando un trabajo dentro de un lote falla, Laravel marcará automáticamente el lote como "cancelado". Si lo desea, puede desactivar este comportamiento para que un fallo en un trabajo no marque automáticamente el lote como cancelado. Esto puede lograrse llamando al método allowFailures al despachar el lote:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// Todos los trabajos se completaron con éxito...
})->allowFailures()->dispatch();
#Reintentar trabajos fallidos en lotes
Para mayor comodidad, Laravel proporciona un comando Artisan queue:retry-batch que le permite reintentar fácilmente todos los trabajos fallidos de un lote dado. El comando queue:retry-batch acepta el UUID del lote cuyos trabajos fallidos deben reintentarse:
php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5
#Poda de lotes
Sin poda, la tabla job_batches puede acumular registros muy rápidamente. Para mitigar esto, debe programar el comando Artisan queue:prune-batches para que se ejecute diariamente:
$schedule->command('queue:prune-batches')->daily();
Por defecto, todos los lotes finalizados que tengan más de 24 horas serán podados. Puede usar la opción hours al llamar al comando para determinar cuánto tiempo conservar los datos del lote. Por ejemplo, el siguiente comando eliminará todos los lotes que finalizaron hace más de 48 horas:
$schedule->command('queue:prune-batches --hours=48')->daily();
A veces, su tabla jobs_batches puede acumular registros de lotes que nunca se completaron con éxito, como lotes donde un trabajo falló y ese trabajo nunca fue reintentado con éxito. Puede instruir al comando queue:prune-batches para que pode estos registros de lotes no finalizados usando la opción unfinished:
$schedule->command('queue:prune-batches --hours=48 --unfinished=72')->daily();
De igual manera, su tabla jobs_batches también puede acumular registros de lotes cancelados. Puede instruir al comando queue:prune-batches para que pode estos registros de lotes cancelados usando la opción cancelled:
$schedule->command('queue:prune-batches --hours=48 --cancelled=72')->daily();
#Almacenamiento de lotes en DynamoDB
Laravel también ofrece soporte para almacenar información meta de lotes en DynamoDB en lugar de una base de datos relacional. Sin embargo, deberá crear manualmente una tabla DynamoDB para almacenar todos los registros de lotes.
Normalmente, esta tabla debería llamarse job_batches, pero debe nombrar la tabla según el valor de la configuración queue.batching.table dentro del archivo de configuración queue de su aplicación.
#Configuración de la tabla de lotes en DynamoDB
La tabla job_batches debe tener una clave primaria de partición de tipo string llamada application y una clave primaria de ordenamiento de tipo string llamada id. La parte application de la clave contendrá el nombre de su aplicación según lo definido por el valor de configuración name dentro del archivo de configuración app de su aplicación. Dado que el nombre de la aplicación es parte de la clave de la tabla DynamoDB, puede usar la misma tabla para almacenar lotes de trabajo de múltiples aplicaciones Laravel.
Además, puede definir un atributo ttl para su tabla si desea aprovechar la poda automática de lotes.
#Configuración de DynamoDB
A continuación, instale el SDK de AWS para que su aplicación Laravel pueda comunicarse con Amazon DynamoDB:
composer require aws/aws-sdk-php
Luego, establezca el valor de la opción de configuración queue.batching.driver a dynamodb. Además, debe definir las opciones de configuración key, secret y region dentro del arreglo de configuración batching. Estas opciones se usarán para autenticarse con AWS. Al usar el driver dynamodb, la opción de configuración queue.batching.database no es necesaria:
'batching' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'job_batches',
],
#Poda de lotes en DynamoDB
Cuando utiliza DynamoDB para almacenar información de lotes de trabajos, los comandos típicos de poda usados para lotes almacenados en bases de datos relacionales no funcionarán. En su lugar, puede utilizar la funcionalidad TTL nativa de DynamoDB para eliminar automáticamente registros de lotes antiguos.
Si definió su tabla DynamoDB con un atributo ttl, puede definir parámetros de configuración para indicar a Laravel cómo podar los registros de lotes. El valor de configuración queue.batching.ttl_attribute define el nombre del atributo que contiene el TTL, mientras que el valor queue.batching.ttl define el número de segundos después de los cuales un registro de lote puede eliminarse de la tabla DynamoDB, relativo a la última vez que se actualizó el registro:
'batching' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'job_batches',
'ttl_attribute' => 'ttl',
'ttl' => 60 * 60 * 24 * 7, // 7 días...
],
#Encolando closures
En lugar de despachar una clase de trabajo a la cola, también puede despachar un closure. Esto es ideal para tareas rápidas y simples que necesitan ejecutarse fuera del ciclo de la solicitud actual. Al despachar closures a la cola, el contenido del código del closure se firma criptográficamente para que no pueda modificarse en tránsito:
$podcast = App\Podcast::find(1);
dispatch(function () use ($podcast) {
$podcast->publish();
});
Usando el método catch, puede proporcionar un closure que se ejecutará si el closure encolado falla en completarse exitosamente después de agotar todos los intentos de reintento configurados de su cola:
use Throwable;
dispatch(function () use ($podcast) {
$podcast->publish();
})->catch(function (Throwable $e) {
// Este trabajo ha fallado...
});
Dado que los callbacks catch se serializan y ejecutan más tarde por la cola de Laravel, no debe usar la variable $this dentro de los callbacks catch.
#Ejecutando el worker de la cola
#El comando queue:work
Laravel incluye un comando Artisan que iniciará un worker de cola y procesará nuevos trabajos a medida que se agreguen a la cola. Puede ejecutar el worker usando el comando Artisan queue:work. Tenga en cuenta que una vez que el comando queue:work ha comenzado, continuará ejecutándose hasta que se detenga manualmente o cierre su terminal:
php artisan queue:work
Para mantener el proceso queue:work ejecutándose permanentemente en segundo plano, debe usar un monitor de procesos como Supervisor para asegurarse de que el worker de la cola no deje de ejecutarse.
Puede incluir la bandera -v al invocar el comando queue:work si desea que los IDs de los trabajos procesados se incluyan en la salida del comando:
php artisan queue:work -v
Recuerde, los workers de cola son procesos de larga duración y almacenan el estado de la aplicación cargada en memoria. Como resultado, no notarán cambios en su base de código después de haber sido iniciados. Por lo tanto, durante su proceso de despliegue, asegúrese de reiniciar sus workers de cola. Además, recuerde que cualquier estado estático creado o modificado por su aplicación no se restablecerá automáticamente entre trabajos.
Alternativamente, puede ejecutar el comando queue:listen. Al usar el comando queue:listen, no tiene que reiniciar manualmente el worker cuando desea recargar su código actualizado o restablecer el estado de la aplicación; sin embargo, este comando es significativamente menos eficiente que el comando queue:work:
php artisan queue:listen
#Ejecutar múltiples workers de cola
Para asignar múltiples workers a una cola y procesar trabajos concurrentemente, simplemente debe iniciar múltiples procesos queue:work. Esto puede hacerse localmente mediante múltiples pestañas en su terminal o en producción usando la configuración de su gestor de procesos. Al usar Supervisor, puede usar el valor de configuración numprocs.
#Especificar la conexión y la cola
También puede especificar qué conexión de cola debe usar el worker. El nombre de la conexión pasado al comando work debe corresponder a una de las conexiones definidas en su archivo de configuración config/queue.php:
php artisan queue:work redis
Por defecto, el comando queue:work solo procesa trabajos para la cola predeterminada en una conexión dada. Sin embargo, puede personalizar aún más su worker de cola procesando solo colas particulares para una conexión dada. Por ejemplo, si todos sus correos electrónicos se procesan en una cola emails en su conexión de cola redis, puede emitir el siguiente comando para iniciar un worker que solo procese esa cola:
php artisan queue:work redis --queue=emails
#Procesar un número especificado de trabajos
La opción --once puede usarse para indicar al worker que solo procese un único trabajo de la cola:
php artisan queue:work --once
La opción --max-jobs puede usarse para indicar al worker que procese el número dado de trabajos y luego salga. Esta opción puede ser útil cuando se combina con Supervisor para que sus workers se reinicien automáticamente después de procesar un número dado de trabajos, liberando cualquier memoria que hayan acumulado:
php artisan queue:work --max-jobs=1000
#Procesar todos los trabajos en cola y luego salir
La opción --stop-when-empty puede usarse para indicar al worker que procese todos los trabajos y luego salga de forma ordenada. Esta opción puede ser útil al procesar colas Laravel dentro de un contenedor Docker si desea apagar el contenedor después de que la cola esté vacía:
php artisan queue:work --stop-when-empty
#Procesar trabajos durante un número dado de segundos
La opción --max-time puede usarse para indicar al worker que procese trabajos durante el número dado de segundos y luego salga. Esta opción puede ser útil cuando se combina con Supervisor para que sus workers se reinicien automáticamente después de procesar trabajos durante un tiempo dado, liberando cualquier memoria que hayan acumulado:
# Procesar trabajos durante una hora y luego salir...
php artisan queue:work --max-time=3600
#Duración del sueño del worker
Cuando hay trabajos disponibles en la cola, el worker seguirá procesando trabajos sin demora entre ellos. Sin embargo, la opción sleep determina cuántos segundos "dormirá" el worker si no hay trabajos disponibles. Por supuesto, mientras duerme, el worker no procesará nuevos trabajos:
php artisan queue:work --sleep=3
#Modo de mantenimiento y colas
Mientras su aplicación esté en modo de mantenimiento, no se manejarán trabajos encolados. Los trabajos continuarán siendo manejados normalmente una vez que la aplicación salga del modo de mantenimiento.
Para forzar a sus workers de cola a procesar trabajos incluso si el modo de mantenimiento está habilitado, puede usar la opción --force:
php artisan queue:work --force
#Consideraciones de recursos
Los workers de cola en modo demonio no "reinician" el framework antes de procesar cada trabajo. Por lo tanto, debe liberar cualquier recurso pesado después de que cada trabajo termine. Por ejemplo, si está manipulando imágenes con la biblioteca GD, debe liberar la memoria con imagedestroy cuando termine de procesar la imagen.
#Prioridades de cola
A veces usted puede desear priorizar cómo se procesan sus colas. Por ejemplo, en su archivo de configuración config/queue.php puede establecer la queue predeterminada para su conexión redis en low. Sin embargo, ocasionalmente puede que desee enviar una tarea a una cola de alta prioridad high de la siguiente manera:
dispatch((new Job)->onQueue('high'));
Para iniciar un worker que verifique que todos los trabajos de la cola high se procesen antes de continuar con cualquier trabajo en la cola low, pase una lista separada por comas de nombres de colas al comando work:
php artisan queue:work --queue=high,low
#Workers de cola y despliegue
Dado que los workers de cola son procesos de larga duración, no notarán los cambios en su código sin ser reiniciados. Por lo tanto, la forma más sencilla de desplegar una aplicación que use workers de cola es reiniciar los workers durante su proceso de despliegue. Puede reiniciar todos los workers de forma ordenada ejecutando el comando queue:restart:
php artisan queue:restart
Este comando indicará a todos los workers de cola que salgan de forma ordenada después de terminar de procesar su trabajo actual, para que no se pierdan trabajos existentes. Dado que los workers de cola saldrán cuando se ejecute el comando queue:restart, debe estar ejecutando un gestor de procesos como Supervisor para reiniciar automáticamente los workers de cola.
La cola utiliza el cache para almacenar señales de reinicio, por lo que debe verificar que un driver de caché esté configurado correctamente para su aplicación antes de usar esta función.
#Expiraciones y tiempos de espera de trabajos
#Expiración de trabajos
En su archivo de configuración config/queue.php, cada conexión de cola define una opción retry_after. Esta opción especifica cuántos segundos debe esperar la conexión de cola antes de reintentar un trabajo que está siendo procesado. Por ejemplo, si el valor de retry_after está configurado en 90, el trabajo será liberado de nuevo a la cola si ha estado procesándose durante 90 segundos sin ser liberado o eliminado. Normalmente, debe establecer el valor de retry_after al número máximo de segundos que razonablemente deberían tomar sus trabajos para completar el procesamiento.
La única conexión de cola que no contiene un valor retry_after es Amazon SQS. SQS reintentará el trabajo basado en el Default Visibility Timeout que se gestiona dentro de la consola de AWS.
#Tiempos de espera del worker
El comando Artisan queue:work expone una opción --timeout. Por defecto, el valor de --timeout es de 60 segundos. Si un trabajo se está procesando por más tiempo que el número de segundos especificado por el valor de timeout, el worker que procesa el trabajo saldrá con un error. Normalmente, el worker será reiniciado automáticamente por un gestor de procesos configurado en su servidor:
php artisan queue:work --timeout=60
La opción de configuración retry_after y la opción CLI --timeout son diferentes, pero trabajan juntas para asegurar que los trabajos no se pierdan y que los trabajos solo se procesen exitosamente una vez.
El valor de --timeout siempre debe ser al menos varios segundos menor que el valor de configuración retry_after. Esto asegurará que un worker que procesa un trabajo congelado siempre sea terminado antes de que el trabajo sea reintentado. Si su opción --timeout es mayor que el valor de configuración retry_after, sus trabajos podrían procesarse dos veces.
#Configuración de Supervisor
En producción, necesita una forma de mantener sus procesos queue:work en ejecución. Un proceso queue:work puede dejar de ejecutarse por diversas razones, como un timeout excedido del worker o la ejecución del comando queue:restart.
Por esta razón, debe configurar un monitor de procesos que pueda detectar cuando sus procesos queue:work terminan y reiniciarlos automáticamente. Además, los monitores de procesos le permiten especificar cuántos procesos queue:work desea ejecutar simultáneamente. Supervisor es un monitor de procesos comúnmente usado en entornos Linux y discutiremos cómo configurarlo en la documentación siguiente.
#Instalación de Supervisor
Supervisor es un monitor de procesos para el sistema operativo Linux, y reiniciará automáticamente sus procesos queue:work si fallan. Para instalar Supervisor en Ubuntu, puede usar el siguiente comando:
sudo apt-get install supervisor
Si configurar y administrar Supervisor por su cuenta le resulta abrumador, considere usar Laravel Forge, que instalará y configurará Supervisor automáticamente para sus proyectos Laravel en producción.
#Configuración de Supervisor
Los archivos de configuración de Supervisor normalmente se almacenan en el directorio /etc/supervisor/conf.d. Dentro de este directorio, puede crear cualquier cantidad de archivos de configuración que indiquen a Supervisor cómo deben ser monitoreados sus procesos. Por ejemplo, creemos un archivo laravel-worker.conf que inicie y monitoree procesos queue:work:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
stopwaitsecs=3600
En este ejemplo, la directiva numprocs indicará a Supervisor que ejecute ocho procesos queue:work y los monitoree a todos, reiniciándolos automáticamente si fallan. Debe cambiar la directiva command de la configuración para reflejar la conexión de cola y las opciones de worker deseadas.
Debe asegurarse de que el valor de stopwaitsecs sea mayor que el número de segundos consumidos por su trabajo de mayor duración. De lo contrario, Supervisor podría matar el trabajo antes de que termine de procesarse.
#Inicio de Supervisor
Una vez creado el archivo de configuración, puede actualizar la configuración de Supervisor e iniciar los procesos usando los siguientes comandos:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"
Para más información sobre Supervisor, consulte la documentación de Supervisor.
#Manejo de trabajos fallidos
A veces sus trabajos en cola fallarán. No se preocupe, ¡las cosas no siempre salen según lo planeado! Laravel incluye una forma conveniente de especificar el número máximo de intentos que debe tener un trabajo. Después de que un trabajo asíncrono exceda este número de intentos, será insertado en la tabla failed_jobs de la base de datos. Los trabajos despachados sincrónicamente que fallan no se almacenan en esta tabla y sus excepciones son manejadas inmediatamente por la aplicación.
Una migración para crear la tabla failed_jobs normalmente ya está presente en nuevas aplicaciones Laravel. Sin embargo, si su aplicación no contiene una migración para esta tabla, puede usar el comando queue:failed-table para crear la migración:
php artisan queue:failed-table
php artisan migrate
Al ejecutar un proceso queue worker, puede especificar el número máximo de intentos que debe tener un trabajo usando el interruptor --tries en el comando queue:work. Si no especifica un valor para la opción --tries, los trabajos solo serán intentados una vez o tantas veces como lo especifique la propiedad $tries de la clase del trabajo:
php artisan queue:work redis --tries=3
Usando la opción --backoff, puede especificar cuántos segundos debe esperar Laravel antes de reintentar un trabajo que ha encontrado una excepción. Por defecto, un trabajo es liberado inmediatamente de nuevo a la cola para que pueda ser intentado otra vez:
php artisan queue:work redis --tries=3 --backoff=3
Si desea configurar cuántos segundos debe esperar Laravel antes de reintentar un trabajo que ha encontrado una excepción de forma individual para cada trabajo, puede hacerlo definiendo una propiedad backoff en su clase de trabajo:
/**
* El número de segundos a esperar antes de reintentar el trabajo.
*
* @var int
*/
public $backoff = 3;
Si requiere una lógica más compleja para determinar el tiempo de espera antes del reintento, puede definir un método backoff en su clase de trabajo:
/**
* Calcular el número de segundos a esperar antes de reintentar el trabajo.
*/
public function backoff(): int
{
return 3;
}
Puede configurar fácilmente tiempos de espera "exponenciales" devolviendo un array de valores de backoff desde el método backoff. En este ejemplo, el retraso para el reintento será de 1 segundo para el primer reintento, 5 segundos para el segundo, 10 segundos para el tercero y 10 segundos para cada reintento posterior si quedan más intentos:
/**
* Calcular el número de segundos a esperar antes de reintentar el trabajo.
*
* @return array<int, int>
*/
public function backoff(): array
{
return [1, 5, 10];
}
#Limpieza después de trabajos fallidos
Cuando un trabajo en particular falla, puede que desee enviar una alerta a sus usuarios o revertir cualquier acción que haya sido parcialmente completada por el trabajo. Para lograr esto, puede definir un método failed en su clase de trabajo. La instancia Throwable que causó el fallo del trabajo será pasada al método failed:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
class ProcessPodcast implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
/**
* Crear una nueva instancia del trabajo.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Ejecutar el trabajo.
*/
public function handle(AudioProcessor $processor): void
{
// Procesar el podcast subido...
}
/**
* Manejar un fallo en el trabajo.
*/
public function failed(?Throwable $exception): void
{
// Enviar notificación al usuario sobre el fallo, etc...
}
}
Se instancia una nueva instancia del trabajo antes de invocar el método failed; por lo tanto, cualquier modificación a las propiedades de la clase que haya ocurrido dentro del método handle se perderá.
#Reintento de trabajos fallidos
Para ver todos los trabajos fallidos que han sido insertados en la tabla failed_jobs de su base de datos, puede usar el comando Artisan queue:failed:
php artisan queue:failed
El comando queue:failed listará el ID del trabajo, conexión, cola, hora del fallo y otra información sobre el trabajo. El ID del trabajo puede usarse para reintentar el trabajo fallido. Por ejemplo, para reintentar un trabajo fallido que tiene un ID ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece, ejecute el siguiente comando:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece
Si es necesario, puede pasar múltiples IDs al comando:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d
También puede reintentar todos los trabajos fallidos de una cola en particular:
php artisan queue:retry --queue=name
Para reintentar todos sus trabajos fallidos, ejecute el comando queue:retry y pase all como ID:
php artisan queue:retry all
Si desea eliminar un trabajo fallido, puede usar el comando queue:forget:
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d
Cuando use Horizon, debe usar el comando horizon:forget para eliminar un trabajo fallido en lugar del comando queue:forget.
Para eliminar todos sus trabajos fallidos de la tabla failed_jobs, puede usar el comando queue:flush:
php artisan queue:flush
#Ignorar modelos faltantes
Al inyectar un modelo Eloquent en un trabajo, el modelo se serializa automáticamente antes de ser colocado en la cola y se vuelve a recuperar de la base de datos cuando el trabajo es procesado. Sin embargo, si el modelo ha sido eliminado mientras el trabajo esperaba ser procesado por un worker, su trabajo puede fallar con una ModelNotFoundException.
Para mayor comodidad, puede optar por eliminar automáticamente los trabajos con modelos faltantes configurando la propiedad deleteWhenMissingModels de su trabajo a true. Cuando esta propiedad está en true, Laravel descartará silenciosamente el trabajo sin lanzar una excepción:
/**
* Eliminar el trabajo si sus modelos ya no existen.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
#Poda de trabajos fallidos
Puede podar los registros en la tabla failed_jobs de su aplicación invocando el comando Artisan queue:prune-failed:
php artisan queue:prune-failed
Por defecto, se podarán todos los registros de trabajos fallidos que tengan más de 24 horas. Si proporciona la opción --hours al comando, solo se conservarán los registros de trabajos fallidos insertados dentro de las últimas N horas. Por ejemplo, el siguiente comando eliminará todos los registros de trabajos fallidos insertados hace más de 48 horas:
php artisan queue:prune-failed --hours=48
#Almacenamiento de trabajos fallidos en DynamoDB
Laravel también ofrece soporte para almacenar sus registros de trabajos fallidos en DynamoDB en lugar de una tabla relacional. Sin embargo, debe crear manualmente una tabla DynamoDB para almacenar todos los registros de trabajos fallidos. Normalmente, esta tabla debería llamarse failed_jobs, pero debe nombrarla según el valor de configuración queue.failed.table en el archivo de configuración queue de su aplicación.
La tabla failed_jobs debe tener una clave primaria de partición de tipo string llamada application y una clave primaria de ordenamiento de tipo string llamada uuid. La parte application de la clave contendrá el nombre de su aplicación definido por el valor de configuración name en el archivo de configuración app de su aplicación. Dado que el nombre de la aplicación es parte de la clave de la tabla DynamoDB, puede usar la misma tabla para almacenar trabajos fallidos de múltiples aplicaciones Laravel.
Además, asegúrese de instalar el SDK de AWS para que su aplicación Laravel pueda comunicarse con Amazon DynamoDB:
composer require aws/aws-sdk-php
Luego, configure el valor de la opción queue.failed.driver a dynamodb. Además, debe definir las opciones de configuración key, secret y region dentro del arreglo de configuración de trabajos fallidos. Estas opciones se usarán para autenticarse con AWS. Al usar el driver dynamodb, la opción de configuración queue.failed.database no es necesaria:
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'failed_jobs',
],
#Deshabilitar el almacenamiento de trabajos fallidos
Puede indicar a Laravel que descarte los trabajos fallidos sin almacenarlos configurando el valor de la opción queue.failed.driver a null. Normalmente, esto puede lograrse mediante la variable de entorno QUEUE_FAILED_DRIVER:
QUEUE_FAILED_DRIVER=null
#Eventos de trabajos fallidos
Si desea registrar un listener de eventos que se invoque cuando un trabajo falle, puede usar el método failing del facade Queue. Por ejemplo, podemos adjuntar un closure a este evento desde el método boot del AppServiceProvider incluido con Laravel:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobFailed;
class AppServiceProvider extends ServiceProvider
{
/**
* Registrar cualquier servicio de la aplicación.
*/
public function register(): void
{
// ...
}
/**
* Inicializar cualquier servicio de la aplicación.
*/
public function boot(): void
{
Queue::failing(function (JobFailed $event) {
// $event->connectionName
// $event->job
// $event->exception
});
}
}
#Limpiar trabajos de las colas
Cuando use Horizon, debe usar el comando horizon:clear para limpiar trabajos de la cola en lugar del comando queue:clear.
Si desea eliminar todos los trabajos de la cola predeterminada de la conexión predeterminada, puede hacerlo usando el comando Artisan queue:clear:
php artisan queue:clear
También puede proporcionar el argumento connection y la opción queue para eliminar trabajos de una conexión y cola específicas:
php artisan queue:clear redis --queue=emails
Limpiar trabajos de las colas solo está disponible para los drivers de cola SQS, Redis y base de datos. Además, el proceso de eliminación de mensajes SQS puede tardar hasta 60 segundos, por lo que los trabajos enviados a la cola SQS hasta 60 segundos después de limpiar la cola también podrían ser eliminados.
#Monitoreo de sus colas
Si su cola recibe un aumento repentino de trabajos, podría saturarse, lo que provocaría un tiempo de espera largo para que los trabajos se completen. Si lo desea, Laravel puede alertarle cuando el conteo de trabajos en su cola exceda un umbral especificado.
Para comenzar, debe programar el comando queue:monitor para que se ejecute cada minuto. El comando acepta los nombres de las colas que desea monitorear así como el umbral deseado para el conteo de trabajos:
php artisan queue:monitor redis:default,redis:deployments --max=100
Programar este comando por sí solo no es suficiente para activar una notificación que le alerte sobre el estado saturado de la cola. Cuando el comando detecta una cola que tiene un conteo de trabajos que excede su umbral, se despachará un evento Illuminate\Queue\Events\QueueBusy. Puede escuchar este evento dentro del EventServiceProvider de su aplicación para enviar una notificación a usted o a su equipo de desarrollo:
use App\Notifications\QueueHasLongWaitTime;
use Illuminate\Queue\Events\QueueBusy;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
/**
* Registrar cualquier otro evento para su aplicación.
*/
public function boot(): void
{
Event::listen(function (QueueBusy $event) {
Notification::route('mail', 'dev@example.com')
->notify(new QueueHasLongWaitTime(
$event->connection,
$event->queue,
$event->size
));
});
}
#Pruebas
Al probar código que despacha trabajos, puede que desee indicar a Laravel que no ejecute realmente el trabajo, ya que el código del trabajo puede probarse directamente y por separado del código que lo despacha. Por supuesto, para probar el trabajo en sí, puede instanciar un trabajo y llamar directamente al método handle en su prueba.
Puede usar el método fake del facade Queue para evitar que los trabajos en cola sean realmente enviados a la cola. Después de llamar al método fake del facade Queue, puede entonces afirmar que la aplicación intentó enviar trabajos a la cola:
<?php
namespace Tests\Feature;
use App\Jobs\AnotherJob;
use App\Jobs\FinalJob;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_orders_can_be_shipped(): void
{
Queue::fake();
// Realizar el envío del pedido...
// Afirmar que no se enviaron trabajos...
Queue::assertNothingPushed();
// Afirmar que un trabajo fue enviado a una cola dada...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// Afirmar que un trabajo fue enviado dos veces...
Queue::assertPushed(ShipOrder::class, 2);
// Afirmar que un trabajo no fue enviado...
Queue::assertNotPushed(AnotherJob::class);
// Afirmar que un Closure fue enviado a la cola...
Queue::assertClosurePushed();
// Afirmar el número total de trabajos enviados...
Queue::assertCount(3);
}
}
Puede pasar un closure a los métodos assertPushed o assertNotPushed para afirmar que un trabajo fue enviado que pasa una determinada "prueba de verdad". Si al menos un trabajo enviado pasa la prueba dada, la afirmación será exitosa:
Queue::assertPushed(function (ShipOrder $job) use ($order) {
return $job->order->id === $order->id;
});
#Falsificar un subconjunto de trabajos
Si solo necesita falsificar trabajos específicos mientras permite que otros trabajos se ejecuten normalmente, puede pasar los nombres de clase de los trabajos que deben ser falsificados al método fake:
public function test_orders_can_be_shipped(): void
{
Queue::fake([
ShipOrder::class,
]);
// Realizar el envío del pedido...
// Afirmar que un trabajo fue enviado dos veces...
Queue::assertPushed(ShipOrder::class, 2);
}
Puede falsificar todos los trabajos excepto un conjunto especificado usando el método except:
Queue::fake()->except([
ShipOrder::class,
]);
#Pruebas de cadenas de trabajos
Para probar cadenas de trabajos, necesitará utilizar las capacidades de falsificación del facade Bus. El método assertChained del facade Bus puede usarse para afirmar que una cadena de trabajos fue despachada. El método assertChained acepta un array de trabajos encadenados como su primer argumento:
use App\Jobs\RecordShipment;
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertChained([
ShipOrder::class,
RecordShipment::class,
UpdateInventory::class
]);
Como puede ver en el ejemplo anterior, el array de trabajos encadenados puede ser un array de nombres de clase de los trabajos. Sin embargo, también puede proporcionar un array de instancias reales de trabajos. Al hacerlo, Laravel se asegurará de que las instancias de trabajo sean de la misma clase y tengan los mismos valores de propiedad que los trabajos encadenados despachados por su aplicación:
Bus::assertChained([
new ShipOrder,
new RecordShipment,
new UpdateInventory,
]);
Puede usar el método assertDispatchedWithoutChain para afirmar que un trabajo fue enviado sin una cadena de trabajos:
Bus::assertDispatchedWithoutChain(ShipOrder::class);
#Pruebas de lotes encadenados
Si su cadena de trabajos contiene un lote de trabajos, puede afirmar que el lote encadenado coincide con sus expectativas insertando una definición Bus::chainedBatch dentro de su afirmación de cadena:
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
Bus::assertChained([
new ShipOrder,
Bus::chainedBatch(function (PendingBatch $batch) {
return $batch->jobs->count() === 3;
}),
new UpdateInventory,
]);
#Pruebas de lotes de trabajos
El método assertBatched del facade Bus puede usarse para afirmar que un lote de trabajos fue despachado. El closure dado al método assertBatched recibe una instancia de Illuminate\Bus\PendingBatch, que puede usarse para inspeccionar los trabajos dentro del lote:
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->name == 'import-csv' &&
$batch->jobs->count() === 10;
});
Puede usar el método assertBatchCount para afirmar que se despachó un número dado de lotes:
Bus::assertBatchCount(3);
Puede usar assertNothingBatched para afirmar que no se despacharon lotes:
Bus::assertNothingBatched();
#Pruebas de interacción trabajo / lote
Además, ocasionalmente puede necesitar probar la interacción de un trabajo individual con su lote subyacente. Por ejemplo, puede necesitar probar si un trabajo canceló el procesamiento adicional para su lote. Para lograr esto, debe asignar un lote falso al trabajo mediante el método withFakeBatch. El método withFakeBatch devuelve una tupla que contiene la instancia del trabajo y el lote falso:
[$job, $batch] = (new ShipOrder)->withFakeBatch();
$job->handle();
$this->assertTrue($batch->cancelled());
$this->assertEmpty($batch->added);
#Eventos de trabajos
Usando los métodos before y after en el facade Queue, puede especificar callbacks que se ejecuten antes o después de que un trabajo en cola sea procesado. Estos callbacks son una excelente oportunidad para realizar registros adicionales o incrementar estadísticas para un panel de control. Normalmente, debe llamar a estos métodos desde el método boot de un service provider. Por ejemplo, podemos usar el AppServiceProvider incluido con Laravel:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
class AppServiceProvider extends ServiceProvider
{
/**
* Registrar cualquier servicio de la aplicación.
*/
public function register(): void
{
// ...
}
/**
* Inicializar cualquier servicio de la aplicación.
*/
public function boot(): void
{
Queue::before(function (JobProcessing $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
Queue::after(function (JobProcessed $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
}
}
Usando el método looping en el facade Queue, puede especificar callbacks que se ejecuten antes de que el worker intente obtener un trabajo de una cola. Por ejemplo, podría registrar un closure para revertir cualquier transacción que haya quedado abierta por un trabajo fallido previamente:
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
Queue::looping(function () {
while (DB::transactionLevel() > 0) {
DB::rollBack();
}
});