How do you pause a Laravel queue

You can use the Queue::looping() event listener to pause an entire queue or connection (not just an individual job class). Unlike other methods, this will not put each job in a cycle of pop/requeue while the queue is paused, meaning the number of attempts will not increase.

Here's what the docs say:

Using the looping method on the Queue facade, you may specify callbacks that execute before the worker attempts to fetch a job from a queue.

https://laravel.com/docs/5.8/queues#job-events

What this doesn't document very well is that if the callback returns false then the worker will not fetch another job. For example, this will prevent the default queue from running:

Queue::looping(function (\Illuminate\Queue\Events\Looping $event) {
    // $event->connectionName (e.g. "database")
    // $event->queue (e.g. "default")

    if ($event->queue == 'default') {
        return false;
    }
});

Note: The queue property of the event will contain the value from the command line when the worker process was started, so if your worker was checking more than one queue (e.g. artisan queue:work --queue=high,default) then the value of queue in the event will be 'high,default'. As a precaution, you may instead want to explode the string by commas and check if default is in the list.

So for example, if you want to create a rudimentary circuit breaker to pause the mail queue when your mail service returns a maintenance error, then you can register a listener like this in your EventServiceProvider.php:

/**
 * Register any events for your application.
 *
 * @return void
 */
public function boot()
{
    parent::boot();

    Queue::looping(function (\Illuminate\Queue\Events\Looping $event) {
        if (($event->queue == 'mail') && (cache()->get('mail-queue-paused'))) {
            return false;
        }
    });
}

This assumes you have a mechanism somewhere else in your application to detect the appropriate situation and, in this example, that mechanism would need to assign a value to the mail-queue-paused key in the shared cache (because that's what my code is checking for). There are much more robust solutions, but setting a specific well-known key in the cache (and expiring it automatically) is simple and achieves the desired effect.


<?php

namespace App\Jobs;

use ...

class SendRequest implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    const REMOTE_SERVER_UNAVAILABLE = 'remote_server_unavailable';

    private $msg;
    private $retryAfter;

    public function __construct($msg)
    {
        $this->msg = $msg;
        $this->retryAfter = 10;
    }

    /**
    * Execute the job.
    *
    * @return void
    */
    public function handle(){
        try {
            // if we have tried sending the request and get a RemoteServerException, we will
            // redispatch the job directly and return.
            if(Cache::get(self::REMOTE_SERVER_UNAVAILABLE)) {
                self::dispatch($this->msg)->delay(Carbon::now()->addMinutes($this->retryAfter));
                return;                  
            }
            // send request to remote server
            // ...
        } catch (RemoteServerException $e) {
            // set a cache value expires in 10 mins if not exists.
            Cache::add(self::REMOTE_SERVER_UNAVAILABLE,'1', $this->retryAfter);
            // if the remote service undergoes a maintenance, redispatch a new delayed job.
            self::dispatch($this->msg)->delay(Carbon::now()->addMinutes($this->retryAfter));            
        }
    }
}