22

How can I browse all the pending jobs within my Redis queue so that I could cancel the Mailable that has a certain emailAddress-sendTime pair?

I'm using Laravel 5.5 and have a Mailable that I'm using successfully as follows:

$sendTime = Carbon::now()->addHours(3);
Mail::to($emailAddress)
      ->bcc([config('mail.supportTeam.address'), config('mail.main.address')])
                    ->later($sendTime, new MyCustomMailable($subject, $dataForMailView));

When this code runs, a job gets added to my Redis queue.

I've already read the Laravel docs but remain confused.

How can I cancel a Mailable (prevent it from sending)?

I'd love to code a webpage within my Laravel app that makes this easy for me.

Or maybe there are tools that already make this easy (maybe FastoRedis?)? In that case, instructions about how to achieve this goal that way would also be really helpful. Thanks!

Update:

I've tried browsing the Redis queue using FastoRedis, but I can't figure out how to delete a Mailable, such as the red arrow points to here: enter image description here

UPDATE:

Look at the comprehensive answer I provided below.

Ryan
  • 22,332
  • 31
  • 176
  • 357
  • 1
    take a look at Laravel Horizon, however I do think that it's not possible to cancel jobs there. I'm also thinking that you can *query* your **Mail Job** and delete it? – Wreigh Jan 15 '18 at 01:15
  • Hi Ryan, please press right mouse button on key and select edit. – Topilski Alexandr Mar 24 '18 at 07:06
  • The answer that worked for me (the original asker) is here: https://stackoverflow.com/a/48696817/470749 – Ryan Jul 23 '18 at 12:26

10 Answers10

20

Make it easier.

Don't send an email with the later option. You must dispatch a Job with the later option, and this job will be responsible to send the email.

Inside this job, before send the email, check the emailAddress-sendTime pair. If is correct, send the email, if not, return true and the email won't send and the job will finish.

Sangar82
  • 5,070
  • 1
  • 35
  • 52
6

Comprehensive Answer:

I now use my own custom DispatchableWithControl trait instead of the Dispatchable trait.

I call it like this:

$executeAt = Carbon::now()->addDays(7)->addHours(2)->addMinutes(17);
SomeJobThatWillSendAnEmailOrDoWhatever::dispatch($contactId, $executeAt);

namespace App\Jobs;

use App\Models\Tag;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Log;

class SomeJobThatWillSendAnEmailOrDoWhatever implements ShouldQueue {

    use DispatchableWithControl,
        InteractsWithQueue,
        Queueable,
        SerializesModels;

    protected $contactId;
    protected $executeAt;

    /**
     * 
     * @param string $contactId
     * @param Carbon $executeAt
     * @return void
     */
    public function __construct($contactId, $executeAt) {
        $this->contactId = $contactId;
        $this->executeAt = $executeAt;
    }

    /**
     * Execute the job. 
     *
     * @return void
     */
    public function handle() {
        if ($this->checkWhetherShouldExecute($this->contactId, $this->executeAt)) {
            //do stuff here
        }
    }

    /**
     * The job failed to process. 
     *
     * @param  Exception  $exception
     * @return void
     */
    public function failed(Exception $exception) {
        // Send user notification of failure, etc...
        Log::error(static::class . ' failed: ' . $exception);
    }

}

namespace App\Jobs;

use App\Models\Automation;
use Carbon\Carbon;
use Illuminate\Foundation\Bus\PendingDispatch;
use Log;

trait DispatchableWithControl {

    use \Illuminate\Foundation\Bus\Dispatchable {//https://stackoverflow.com/questions/40299080/is-there-a-way-to-extend-trait-in-php
        \Illuminate\Foundation\Bus\Dispatchable::dispatch as parentDispatch;
    }

    /**
     * Dispatch the job with the given arguments.
     *
     * @return \Illuminate\Foundation\Bus\PendingDispatch
     */
    public static function dispatch() {
        $args = func_get_args();
        if (count($args) < 2) {
            $args[] = Carbon::now(TT::UTC); //if $executeAt wasn't provided, use 'now' (no delay)
        }
        list($contactId, $executeAt) = $args;
        $newAutomationArray = [
            'contact_id' => $contactId,
            'job_class_name' => static::class,
            'execute_at' => $executeAt->format(TT::MYSQL_DATETIME_FORMAT)
        ];
        Log::debug(json_encode($newAutomationArray));
        Automation::create($newAutomationArray);
        $pendingDispatch = new PendingDispatch(new static(...$args));
        return $pendingDispatch->delay($executeAt);
    }

    /**
     * @param int $contactId
     * @param Carbon $executeAt
     * @return boolean
     */
    public function checkWhetherShouldExecute($contactId, $executeAt) {
        $conditionsToMatch = [
            'contact_id' => $contactId,
            'job_class_name' => static::class,
            'execute_at' => $executeAt->format(TT::MYSQL_DATETIME_FORMAT)
        ];
        Log::debug('checkWhetherShouldExecute ' . json_encode($conditionsToMatch));
        $automation = Automation::where($conditionsToMatch)->first();
        if ($automation) {
            $automation->delete();
            Log::debug('checkWhetherShouldExecute = true, so soft-deleted record.');
            return true;
        } else {
            return false;
        }
    }

}

So, now I can look in my 'automations' table to see pending jobs, and I can delete (or soft-delete) any of those records if I want to prevent the job from executing.

Toby Allen
  • 10,997
  • 11
  • 73
  • 124
Ryan
  • 22,332
  • 31
  • 176
  • 357
  • 3
    You have extended a framework piece of code and added your own business logic to it. It could be done in your job class and there was no need to add it to that trait. You have violated Single Responsibility Principle of SOLID and your code is not reusable anywhere else. – farzan Feb 06 '19 at 14:53
  • 7
    @farzan I don't understand your comment at all. By writing it this way, I *AM* able to use it in multiple places. *Many* of my jobs use this trait (the jobs that I want to be able to easily cancel). What am I misunderstanding? – Ryan Feb 06 '19 at 17:52
  • 1
    Thank you soo much saved my time – Abdul Hakeem Jan 12 '21 at 17:25
3

Delete job by id.

$job = (new \App\Jobs\SendSms('test'))->delay(5);
$id  = app(Dispatcher::class)->dispatch($job);

$res = \Illuminate\Support\Facades\Redis::connection()->zscan('queues:test_queue:delayed', 0, ['match' => '*' . $id . '*']);
$key = array_keys($res[1])[0];

\Illuminate\Support\Facades\Redis::connection()->zrem('queues:test_queue:delayed', $key);
Log
  • 433
  • 4
  • 19
  • $id = app(\Illuminate\Contracts\Bus\Dispatcher::class)->dispatch($job); To be more precise –  Aug 31 '21 at 05:50
2

Maybe instead of canceling it you can actually remove it from the Redis, from what Ive read from official docs about forget command on Redis and from Laravel official doc interacting with redis you can basically call any Redis command from the interface, if you could call the forget command and actually pass node_id which in this case I think it's that number you have in your image DEL 1517797158 I think you could achieve the "cancel".

Nikola Gavric
  • 3,507
  • 1
  • 8
  • 16
1

hope this helps

$connection = null;
$default = 'default';

//For the delayed jobs
var_dump( \Queue::getRedis()->connection($connection)->zrange('queues:'.$default.':delayed' ,0, -1) );

//For the reserved jobs
var_dump( \Queue::getRedis()->connection($connection)->zrange('queues:'.$default.':reserved' ,0, -1) );

$connection is the Redis connection name which is null by default, and The $queue is the name of the queue / tube which is 'default' by default!

source : https://stackoverflow.com/a/42182586/6109499

melsaka
  • 82
  • 4
  • Assuming the `var_dump` would show me enough details (such as `emailAddress` and `sendTime` of the delayed `Mailable`), how can I then choose one and completely remove it from the queue (*delete* it)? – Ryan Feb 03 '18 at 15:05
  • The `var_dump` doesn't show me more than I could already see via FastoRedis. I just updated my question to show you a screenshot. How can I remove/delete the Mailable that the red arrow points to? I tried running `DEL 1517797158`, but that didn't seem to work. I doubt "1517797158" is the key. Thanks. – Ryan Feb 05 '18 at 00:25
1

One approach may be to have your job check to see if you've set a specific address/time to be canceled (deleted from queue). Setup a database table or cache a value forever with the address/time in an array. Then in your job's handle method check if anything has been marked for removal and compare it to the mailable's address/time it is processing:

public function handle()
{
     if (Cache::has('items_to_remove')) {
         $items = Cache::get('items_to_remove');
         $removed = null;
         foreach ($items as $item) {
             if ($this->mail->to === $item['to'] && $this->mail->sendTime === $item['sendTime']) {
                  $removed = $item;
                  $this->delete();
                  break;
             }
         }
         if (!is_null($removed)) {
             $diff = array_diff($items, $removed);
             Cache::set(['items_to_remove' => $diff]);
         }
      }
  }
  • 1
    I appreciate your answer, but I don't think it applies here. In *what* job's `handle` method? As you can see in my question, the email is being queued via `->later()`, and then none of my code ever sees it again. It's handled via Laravel's queue system, which is currently configured to use Redis. There is no custom job that triggers an immediate sending of mail. – Ryan Feb 09 '18 at 16:36
  • 1
    It's the `SendQueuedMailable` class that handles delayed mailables. [Source here](https://github.com/laravel/framework/blob/5.5/src/Illuminate/Mail/SendQueuedMailable.php#L34). Extend the [mailable](https://github.com/laravel/framework/blob/5.5/src/Illuminate/Mail/Mailable.php#L156) and supply the custom `SendQueuedMailable` instance. –  Feb 09 '18 at 18:54
0

I highly recommend checking out the https://laravel.com/docs/master/redis (I run dev/master) but it shows you where they are headed. Most of it works flawlessly now.

Under laravel 8.65 you can just set various status's depending.

protected function listenForEvents()
{
    $this->laravel['events']->listen(JobProcessing::class, function ($event) {
        $this->writeOutput($event->job, 'starting');
    });

    $this->laravel['events']->listen(JobProcessed::class, function ($event) {
        $this->writeOutput($event->job, 'success');
    });

    $this->laravel['events']->listen(JobFailed::class, function ($event) {
        $this->writeOutput($event->job, 'failed');

        $this->logFailedJob($event);
    });
}

You can even do $this->canceled;

I highly recommend Muhammads Queues in action PDF. Trust me well worth the money if your using. queues for very important things.... especially with redis . At first TBH I was turned off a bit cause hes a Laravel employee and I thought he should just post things that are helpful but he goes into specific use cases that they do with forge and other items he does plus dives deep into the guts of how queue workers work whether its horizon or whatever. Total eyeopener for me.

  • Crap sorry will try to format better tomorrow still newb. just wanted to help if people are still seaching. – Jeremy Schoemaker Sep 28 '21 at 10:10
  • One last great article from the lead queue dev on laravel about throwing exceptions you dont care about that are not reported or blocking https://divinglaravel.com/reporting-exceptions – Jeremy Schoemaker Sep 28 '21 at 10:29
-2

Using redis-cli I ran this command:

KEYS *queue*

on the Redis instance holding queued jobs, then deleted whatever keys showed up in the response

DEL queues:default queues:default:reserved
Guy
  • 1,254
  • 17
  • 16
-2

Delete the job from the queue.

$this->delete();
-3

Removing all queued jobs:

Redis::command('flushdb');
Alexandre Thebaldi
  • 4,546
  • 6
  • 41
  • 55