How to cancel queued job in Laravel or Redis - laravel

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:
UPDATE:
Look at the comprehensive answer I provided below.

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.

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.

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);

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".

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

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]);
}
}
}

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.

Removing all queued jobs:
Redis::command('flushdb');

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

Delete the job from the queue.
$this->delete();

Related

Operation without entity

I've been looking for a solution for a while but none of the one I find really allows me to do what I want. I would just like to create routes that don't necessarily require an entity or id to be used. Can you help me the documentation is not clear to do this.
Thank you beforehand.
As you can read in the General Design Considerations, just make an ordinary PHP class (POPO). Give it an ApiResource annontation like this:
* #ApiResource(
* collectionOperations={
* "post"
* },
* itemOperations={}
* )
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
You should add your path if your class is not in the Entity folder.
Api Platform will expect json to be posted and try to unserialize it into an instance of your class. Make a custom DataPersister to process the instance, for example if your class is App\ApiCommand\Doit:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\ApiCommand\Doit;
use App\ApiResult\DoitResult;
final class DoitDataPersister implements ContextAwareDataPersisterInterface
{
public function supports($data, array $context = []): bool
{
return $data instanceof Doit;
}
public function persist($data, array $context = [])
{
// code to process $data
$result = new DoitResult();
$result->description = 'Hello world';
return $result;
}
public function remove($data, array $context = [])
{
// will not be called if you have no delete operation
}
}
If you need Doctrine, add:
public function __construct(ManagerRegistry $managerRegistry)
{
$this->managerRegistry = $managerRegistry;
}
See Injecting Extensions for how to use it.
Notice that the result returned by ::persist is not an instance of Doit. If you return a Doit api platform will try to serialize that as the result of your operation. But we have marked Doit as an ApiResource so (?) api platform looks for an item operation that can retrieve it, resulting in an error "No item route associated with the type App\ApiCommand\Doit". To avoid this you can return any object that Symfonies serializer can serialize that is not an ApiResource. In the example an instance of DoitResult. Alternatively you can return an instance of Symfony\Component\HttpFoundation\Response but then you have to take care of the serialization yourself.
The post operation should already work, but the swagger docs are made from metadata. To tell api platform that it should expect a DoitResult to be returned, change the #ApiResource annotation:
* collectionOperations={
* "post"={
* "output"=DoitResult::class
* }
* },
This will the add a new type for DoitResult to the swagger docs, but the descriptions are still wrong. You can correct them using a SwaggerDecorator. Here is one for a 201 post response:
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, string $format = null, array $context = [])
{
$summary = 'short explanation about DoitResult';
$docs = $this->decorated->normalize($object, $format, $context);
$docs['paths']['/doit']['post']['responses']['201']['description'] = 'Additional explanation about DoitResult';
$responseContent = $docs['paths']['/doit']['post']['responses']['201']['content'];
$this->setByRef($docs, $responseContent['application/ld+json']['schema']['properties']['hydra:member']['items']['$ref'],
'description', $summary);
$this->setByRef($docs, $responseContent['application/json']['schema']['items']['$ref'],
'description', $summary);
return $docs;
}
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
private function setByRef(&$docs, $ref, $key, $value)
{
$pieces = explode('/', substr($ref, 2));
$sub =& $docs;
foreach ($pieces as $piece) {
$sub =& $sub[$piece];
}
$sub[$key] = $value;
}
}
To configure the service add the following to api/config/services.yaml:
'App\Swagger\SwaggerDecorator':
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '#App\Swagger\SwaggerDecorator.inner' ]
autoconfigure: false
If your post operation is not actually creating something you may not like the 201 response. You can change that by specifying the response code in the #ApiResource annotation, for example:
* collectionOperations={
* "post"={
* "output"=DoitResult::class,
* "status"=200
* }
* },
You may want to adapt the SwaggerDecorator accordingly.
Creating a "get" collection operation is similar, but you need to make a DataProvider instead of a DataPersister. The chapter9-api branch of my tutorial contains an example of a SwaggerDecorator for a collection response.
Thanks you for answer. I had some information but not everything. I will try the weekend.

Laravel skip sending mail based on conditions

I'm looping through users in my controller function and sending an email to each user, but there's a special user who will sometimes be in the list who shouldn't receive emails.
I'd like to put an if statement in to skip that loop iteration if it's that user, but when I add return, return null etc, or just nothing in my if/else in the mailable class build function I get
InvalidArgumentException
Invalid view.
I could add the conditional in the controller and I'm sure that would be fine, except I have these emails in a whole load of my controllers so would be writing the conditional many times. If I can get it into a central location (and the mailable class is the only one I'm aware of) then I can write it once.
EDIT: the loop code has been requested so added below, but this is just one of many email loops in my controllers so NOT where I'd like to add my conditional.
$objNotification = new \stdClass();
$objNotification->message_body = "stuff";
$user_ids = DB::select('select user_id from users_to_things where thing_id = ?', [$thing->id]);
foreach($user_ids as $user_id) {
$user = User::find($user_id->user_id);
$objNotification->receiver = $user->name;
Mail::to($user->email)->send(new NotificationEmail($objNotification));
}
Here's my mailable class's build function (where I DO want to add the conditional if at all possible):
public function build()
{
if($this->notification->receiver == 'the special user') {
return;
} else {
return $this->from('somebody#somewhere.com', 'Sender Name')
->subject('some subject')
->view('emails.notification')
->text('emails.notification_plain');
}
}
What you want would conflict with the Single Responsibility Principle. The class is only suppose to send the email.
The build() in your mailable class returns error cause it expect you to return a message ( subject ,body, template or view, etc) for your mail.
1.) maybe instead you can assert a fake mail in your condition;
2.) Create a function in email class
public function needSend($mail)
{
... loads of conditions
return (condition) ? true : false;
}
then
if ($mail->needSend($mail)) {
\Mail::send($mail);
}
3.) Or the easiest but dirtiest way is you can put the condition inside your Controller in your for loop
foreach($user_ids as $user_id) {
$user = User::find($user_id->user_id);
$objNotification->receiver = $user->name;
if(condition == true) continue;
Mail::to($user->email)->send(new NotificationEmail($objNotification));
}
You can also read this thread on github. https://github.com/laravel/ideas/issues/519
Using the notification class, check your condition like a suppression list or maybe runtime condition. Return an empty array and it'll exit.
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return $notifiable->notificationSuppression()->exists() ? [] : ['mail'];
}

Laravel: Is possible send notification with delay, but changing smtp settings dynamically?

I'm developing a Multi Tenant (multiple database) with Laravel v5.7 and I'm successful in sending queue emails.
In some specific situations, I'd like to send on-demand notifications with 'delay', similar to the guide On-Demand Notifications, but informing the SMTP settings that should be used before sending.
I've developed a class that changes the values of config().
app/Tenant/SmtpConfig.php
class SmtpConfig
{
public static function setConnection(SmtpConta $conta = null)
{
// get connection default settings
$config = config()->get("mail");
// populate connection default settings
foreach ($config as $key => $value) {
if ( $key == 'host' ) { $config[$key] = $conta->mail_host ?? $config[$key]; }
if ( $key == 'from' ) { $config[$key] = [
'address' => ( $conta->mail_host === 'smtp.mailtrap.io' ) ? $config[$key]['address'] : $conta->mail_username,
'name' => $conta->conta ?? $config[$key]['name']
]; }
if ( $key == 'username' ) { $config[$key] = $conta->mail_username ?? $config[$key]; }
if ( $key == 'password' ) { $config[$key] = !empty($conta->mail_password) ? $conta->mail_password : $config[$key]; }
}
$config['encryption'] = ( $conta->mail_host === 'smtp.mailtrap.io' ) ? null : 'ssl';
// set connection default settings
config()->set("mail", $config);
}
}
... and I call this SmtpConfig class in notification:
/**
* Create a new notification instance.
*
* #param $conta
* #param $subject
* #return void
*/
public function __construct(SmtpConta $conta = null, $subject = null)
{
$this->conta = $conta;
$this->subject = $subject;
$when = \Carbon\Carbon::now()->addSecond(100);
$this->delay($when);
app(\App\Tenant\SmtpConfig::class)::setConnection($this->conta);
}
I can send the 'delayed' notification successfully, but apparently it always uses the default values of the .env file.
Now I'm not sure if where I'm calling the class makes any sense or even how can I tell the notification what SMTP configuration it should use.
I'm currently facing a similar challenge, on a Laravel 5.2 codebase using the Notification backport library.
This is an example of my solution, similar to Kit Loong's suggestion. We just extend the Illuminate\Notifications\Channels\MailChannel class and override the send() method.
You'll need to be able to determine the SMTP config from the recipient(s), or notification objects, so you'll need to edit my example as necessary.
Also this assumes your app is using the default Swift_Mailer so YMMV...
<?php
declare (strict_types = 1);
namespace App\Notifications\Channels;
use Illuminate\Notifications\Channels\MailChannel;
use Illuminate\Notifications\Notification;
class DynamicSmtpMailChannel extends MailChannel
{
/**
* Send the given notification.
*
* #param mixed $notifiable
* #param \Illuminate\Notifications\Notification $notification
* #return void
*/
public function send($notifiable, Notification $notification)
{
//define this method on your model (note $notifiable could be an array or collection of notifiables!)
$customSmtp = $notifiable->getSmtpConfig();
if ($customSmtp) {
$previousSwiftMailer = $this->mailer->getSwiftMailer();
$swiftTransport = new \Swift_SmtpTransport(
$customSmtp->smtp_server,
$customSmtp->smtp_port,
$customSmtp->smtp_encryption
);
$swiftTransport->setUsername($customSmtp->smtp_user);
$swiftTransport->setPassword($customSmtp->smtp_password);
$this->mailer->setSwiftMailer(new \Swift_Mailer($swiftTransport));
}
$result = parent::send($notifiable, $notification);
if (isset($previousSwiftMailer)) {
//restore the previous mailer
$this->mailer->setSwiftMailer($previousSwiftMailer);
}
return $result;
}
}
It may also be beneficial to keep an ephemeral store of custom swift mailers so you can re-use them in the same invokation/request (think about long-running workers) - like a collection class where a hash of the smtp config is used as the item key.
Best of luck with it.
Edit:
I should probably mention you may need to bind this in the service container. Something like this should suffice:
// in a service provider
public function register()
{
$this->app->bind(
\Illuminate\Notifications\Channels\MailChannel::class
\App\Notifications\Channels\DynamicSmtpMailChannel::class
);
}
Or alternatively, register it as a seperate notification channel.
I think you can also refer to this implementation.
https://stackoverflow.com/a/46135925/6011908
You could execute by passing custom smtp configs.
$transport = new Swift_SmtpTransport(
$customSmtp->host,
$customSmtp->port,
$customSmtp->encryption
);

What is the difference between trait and behavior in cakephp 3?

I find soft delete in cakephp 3 that implemented via traits. And I try to implement it via behaviors. But unlike the trait version, SoftDeleteBehavior do not work.
I have this line in my model initialize method:
$this->addBehavior('SoftDelete');
And this is my SoftDeleteBehavior
namespace App\Model\Behavior;
use Cake\ORM\Behavior;
use Cake\ORM\RulesChecker;
use Cake\Datasource\EntityInterface;
use App\Model\Behavior\MyQuery;
class SoftDeleteBehavior extends Behavior {
public $user_id = 1;
public function getDeleteDate() {
return isset($this->deleteDate) ? $this->deleteDate : 'deleted';
}
public function getDeleter() {
return isset($this->deleter) ? $this->deleter : 'deleter_id';
}
public function query() {
return new MyQuery($this->connection(), $this);
}
/**
* Perform the delete operation.
*
* Will soft delete the entity provided. Will remove rows from any
* dependent associations, and clear out join tables for BelongsToMany associations.
*
* #param \Cake\DataSource\EntityInterface $entity The entity to soft delete.
* #param \ArrayObject $options The options for the delete.
* #throws \InvalidArgumentException if there are no primary key values of the
* passed entity
* #return bool success
*/
protected function _processDelete($entity, $options) {
if ($entity->isNew()) {
return false;
}
$primaryKey = (array)$this->primaryKey();
if (!$entity->has($primaryKey)) {
$msg = 'Deleting requires all primary key values.';
throw new \InvalidArgumentException($msg);
}
if (isset($options['checkRules']) && !$this->checkRules($entity, RulesChecker::DELETE, $options)) {
return false;
}
$event = $this->dispatchEvent('Model.beforeDelete', [
'entity' => $entity,
'options' => $options
]);
if ($event->isStopped()) {
return $event->result;
}
$this->_associations->cascadeDelete(
$entity,
['_primary' => false] + $options->getArrayCopy()
);
$query = $this->query();
$conditions = (array)$entity->extract($primaryKey);
$statement = $query->update()
->set([$this->getDeleteDate() => date('Y-m-d H:i:s') , $this->getDeleter() => $this->user_id])
->where($conditions)
->execute();
$success = $statement->rowCount() > 0;
if (!$success) {
return $success;
}
$this->dispatchEvent('Model.afterDelete', [
'entity' => $entity,
'options' => $options
]);
return $success;
}
If I use trait, SoftDeleteTrait works in true manner. But SoftDeleteBehavior do not work properly!
One is a PHP language construct, the other is a programmatic concept. You may want to read upon what traits are, so that you understand that this question, as it stands, doesn't make too much sense. Also stuff like "doesn't work" doesn't serve as a proper problem description, please be more specific in the future.
That being said, CakePHP behaviors do serve the purpose of horizontal code reuse, similar to traits, as opposed to vertical reuse by inheritance.
However, even if they have conceptual similarities, you cannot simply exchange them as you seem to do in your code, a trait will be composited into the class on which it is used, so that it becomes part of it as if it were written directly in the class definition, and therefore has the ability to overwrite inherited code like the Table::_processDelete() method, a behavior on the other hand is a totally independent class, which is being instantiated and injected as a dependency into a table class at runtime, and calls to its methods are being delegated via the table class (see Table::__call()), unless a method with the same name already exists on the table class, which in your case means that _processDelete() will never be invoked.
I'd suggest that you study a little more on PHP/OOP basics, as this is rather basic stuff that can be untangled easily by just having a look at the source. Being able to understand how the CakePHP code base and the used concepts do work will make your life much easier.

Laravel 5 Commands - Execute one after other

I have a CustomCommand_1 and a CustomCommand_2.
Any way to create a pipeline of commands and executing CustomCommand_2 right after CustomCommand_1 execution? (without call a command inside the other one).
You can use a callback to decide when something will or won't run, using when() or skip():
$schedule
->call('Mailer#BusinessDayMailer')
->weekdays()
->skip(function(TypeHintedDeciderClass $decider)
{
return $decider->isHoliday();
}
);
Referred: Event Scheduling and Commands & Handlers
You can also read how to add commands in queue here.
See, if that helps.
I could not find any way to do this, so I came up workaround (tested on laravel sync driver).
First, you have to create/adjust base command:
namespace App\Commands;
use Illuminate\Foundation\Bus\DispatchesCommands;
abstract class Command {
use DispatchesCommands;
/**
* #var Command[]
*/
protected $commands = [];
/**
* #param Command|Command[] $command
*/
public function addNextCommand($command) {
if (is_array($command)) {
foreach ($command as $item) {
$this->commands[] = $item;
}
} else {
$this->commands[] = $command;
}
}
public function handlingCommandFinished() {
if (!$this->commands)
return;
$command = array_shift($this->commands);
$command->addNextCommand($this->commands);
$this->dispatch($command);
}
}
Every command has to call $this->handlingCommandFinished(); when they finish execution.
With this, you can chain your commands:
$command = new FirstCommand();
$command->addNextCommand(new SecondCommand());
$command->addNextCommand(new ThirdCommand());
$this->dispatch($command);
Pipeline
Instead of calling handlingCommandFinished in each command, you can use command pipeline!
In App\Providers\BusServiceProvider::boot add:
$dispatcher->pipeThrough([
'App\Commands\Pipeline\ChainCommands'
]);
Add create App\Commands\Pipeline\ChainCommands:
class ChainCommands {
public function handle(Command $command, $next) {
$result = $next($command);
$command->handlingCommandFinished();
return $result;
}
}
What is stopping you from doing the following?
$this->dispatch(new CustomCommand_1);
$this->dispatch(new CustomCommand_2);
// And so on

Resources