Laravel - Queue jobs are not reused? - laravel

I know, the question is very strange...
Scenario:
I have a Job class that sends an email, but the content of this email is modified, since the template of the email is selected before being of dispatch.
I do not know if it's a truth, but apparently Laravel maintain a cache of content that he fired for the first time. Even by changing the value of the properties, Job sends exactly the same email.
If this is true, I would like to know how to use the same job class to send different emails or what would be the best alternative.
\app\Jobs\SenderEmail001.php
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($template_id, $subject)
{
$this->template_id = $template_id;
$this->subject = $subject;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$template = Template::findOrFail($this->template_id);
\Mail::send([], [], function($message) use ($template)
{
$message
->replyTo('source#domain.com', 'Email Source')
->from('source#domain.com', 'Email Source')
->to('target#domain.com', 'Email Target')
->subject($this->subject)
->setBody($template->markup, 'text/html');
});
}
MyController
\App\Jobs\SenderEmail001::dispatch(6, 'subject subject subject')
->delay(now()->addSecond(100))
->onQueue('default');

Because queue workers are long-lived processes, they will not recognise code changes without restarting. To gracefully restart the workers during deployment... run.
php artisan queue:restart
see more: https://laravel.com/docs/5.7/queues#queue-workers-and-deployment

Related

Why handle method does not fire during job execution?

I have always used events and listeners to add tasks to the queue. Now I'm trying to use Jobs. I do it like this:
my job.
class eventJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $message;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($message)
{
$this->message = $message;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
Log::alert($this->message);
}
}
My .env file: QUEUE_CONNECTION=database
In my controller, I dispatch the event like this:
eventJob::dispatch('my message');
A new record appears in the jobs table and to execute it I run php artisan queue:work
The record is removed from the jobs table, but nothing appears in the file logs
I tried in the handle method and the constructor to do throw new \Exception("Error Processing the job", 1); But nothing is written in the filed_jobs table, from which I made the assumption that the handle method and the constructor do not execute.
I also tried running my job like this:
$job = new eventJob('my test message'); dispatch($job);
But it does not change anything
I don't know why but when I changed config/queue.php file from 'default' => env('QUEUE_CONNECTION', 'sync') to 'default' => env('QUEUE_CONNECTION', 'database') everything started working as it should

Laravel Queue not running the actual job

I'm facing an issue in my production environment. One specific job is not working as expected when specifically called via the scheduler php artisan schedule:run. As it seems, the queue worker does not run through the code in the handle function of the job. Instead, the job is marked as completed in Horizon. The strange thing is that if I run Laravel Tinker on my production server, and push the job manually to the queue it works as expected.
See below for my setup and code snippets.
Does anyone have an idea what the issue is? No code around this specific job has been touched in months, and the issue just showed up randomly last Friday. The server, docker, and Horizon have been restarted several times without any change in the behavior.
Server Setup
Laravel Version: v8.78.1
Laravel Horizon Version: v5.7.18
Docker PHP Image: php:8.1.0-fpm-alpine3.15
App\Console\Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->job(new AdExportFile(
fileName: 'adwords.csv',
daysToExport: 10,
header: [
'Google Click ID',
'Conversion Name',
'Conversion Time',
'Conversion Value',
'Conversion Currency'
],
timezone: config('app.timezone'),
origin: Visit::SOURCE_GOOGLE
))->hourly();
}
App\Jobs\AdExportFile.php
class AdExportFile implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
public const QUEUE = 'default';
/**
* Create a new job instance.
*
* #return void
*/
public function __construct(
protected string $fileName,
protected int $daysToExport,
protected array $header,
protected string $timezone,
protected string $origin,
protected string $delimiter = ',',
protected int $minBaseCommission = 5000
) {
$this->onQueue(static::QUEUE);
\Log::info('AdExportFile: Running __construct');
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
\Log::info('AdExportFile: Running the handle function');
}
}
Log output
AdExportFile: Running __construct
Edit: This has now spread to a second job, that is called via the "$schedule->call" method

Laravel: Set/mock application time globally for development

On my application, users have lists of emails they can send to. Their accounts have settings for the time of day they want emails automatically sent and the timezone they're in.
I would like to test certain scenarios on when my queues are triggered since each user's send time may differ drastically.
I'd like to globally set a fake time with carbon.
In public/index.php, I tried to set:
$time = Carbon::create('2020-09-16 00:00:00');
Carbon::setTestNow($time);
but pieces of my application are not affected.
Is there a global way to set a fake time?
Original question below:
On my application, users have lists of emails they can send to. Their accounts have settings for the time of day they want emails automatically sent and the timezone they're in.
I have a command that will trigger an event that sends email.
Inside the listener, the handle method looks like:
public function handle(ReviewRequested $event)
{
$time = Carbon::create(2020, 9, 15, 0);
Carbon::setTestNow($time);
$reviewRequest = $event->reviewRequest;
Log::info('email sending at ' . $reviewRequest->sent_at . ' and current time is ' . Carbon::now());
Mail::to($reviewRequest->customer->email)
->later($reviewRequest->sent_at, new ReviewRequestMailer($reviewRequest));
}
Note that I'm faking the time with Carbon and setting it to midnight. In this example, The emails should be sent at 9am. The logged info is as follows:
local.INFO: email sending at 2020-09-15 09:00:00 and current time is 2020-09-15 00:00:00
So the current time is 12AM and I'm queuing these up to get sent at 9AM.
As soon as I run php artisan queue:work, the pending jobs (emails) are immediately run and sent. Why is this happening? They should remain queued until 9AM.
Perhaps queuing is using system time and doesn't care about what I set in Carbon? How can I resolve this?
Edit: I forgot to mention that I'm using Redis
Check what queue driver you're using in your .env file. QUEUE_CONNECTION=sync does not allow for any delaying (sync stands for synchronous).
The quickest way to fix this would be doing the following:
change the driver to database QUEUE_CONNECTION=database
clear the cached configuration php artisan config:clear
publish the migration for the jobs table php artisan queue:table
migrate this new table php artisan migrate
After following these steps, you can now have delayed execution in your queues when you run it with php artisan queue:work
I think you should use Laravel Cron Job for this purpose. you should make a file in App/Console/Commands/YourCronJobFile.php
<?php
namespace App\Console\Commands;
use App\TestingCron;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class TestingCronJob extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'send:Mail';
/**
* The console command description.
*
* #var string
*/
protected $description = 'This command is use for test cron jobs.';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
DB::table('testing_cron')->insert(['created_at' => now(),'updated_at' => now()]);
}
}
Then go to directory App/Console/Kernel.php
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* #var array
*/
protected $commands = [
Commands\TestingCronJob::class
];
/**
* Define the application's command schedule.
*
* #param \Illuminate\Console\Scheduling\Schedule $schedule
* #return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('send:Mail')->dailyAt('09:00');
}
/**
* RegisterController the commands for the application.
*
* #return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}
https://laravel.com/docs/7.x/scheduling

Laravel notification sent with no content and wrong subject

I've created this notification in app/Notifications:
class StatisticsExportNotification extends Notification implements ShouldQueue
{
use Queueable;
use EmailNotificationViewHTMLTrait;
protected $links;
/**
* Create a new notification instance.
*
*/
public function __construct($links)
{
$this->links = $links;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return [
MailChannel::class
];
}
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
list($subject, $viewData) = $this->getNotificationViewHTML('statistics_export', $notifiable, ['links' => $this->links]);
return (new MailMessage())
->subject($subject)
->view('mail.main', compact('viewData'));
}
}
I'm trying to send it like this:
$member->notify(
new StatisticsExportNotification('testing')
);
Right now, the content of the email HTML template is just:
{{links}}
But I have confirmed that the same issue occurs if the HTML is just a hardcoded string and if I don't try to pass any variables to it.
That issue being, it seems to me that the toMail method is somehow not being called at all here. When I do logger()->debug($links) inside the class constructor, it logs the value of the variable. But when I do logger()->debug('test') inside toMail(), it doesn't log anything!
If I hardcode the subject and content inside the method, it makes no difference:
public function toMail($notifiable)
{
$subject = "foo";
$viewData = ["test" => "bar"];
return (new MailMessage())
->subject($subject)
->view('mail.main', compact('viewData'));
}
Whatever I do, the system sends the notification to the member's email, but the subject is "Statistics Export Notification" and the email body is empty!
What getNotificationViewHTML does is get the template of the message and populate it with the data we pass to it, but again, this code seems to never come into play at all. I've tried adding logging inside that method as well, and even putting in code that I know would throw an error if executed, but whatever I do, the notification sends with the same default subject and the same empty body.
I tried to recreate your Issue, and I think I did somehow, but I can never be sure 100% until you test it yourself. So Here is some advices :
1- While testing, try using direct array instead of compact to convert it
public function toMail($notifiable)
{
$subject = "foo";
$viewData = ["links" => "Here is the Links"];
return (new MailMessage())
->subject($subject)
->view('mail.main', $viewData);
}
2- In your mail.main view file, the correct syntax should embrace the variable name with $ :
{{ $links }}
and not just
{{ links }}
3- Probably your real issue there, MAKE SURE your view file has .blade.php so that Blade is allowed to parse codes like #section, #yield, {{$links}}, etc. Your mail view filename should be :
views/mail/main.blade.php
4- DO NOT FORGET TO RESTART QUEUE Codes after changing any codes from Statistics Notification file. And this should set the $subject successfully :
php artisan queue:restart
and after start it again
php artisan queue:work --tries=3
Try swapping out your via method with an array of strings:
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['mail'];
}
Does the definition of getNotificationViewHTML return an array [subject, html]?
The list keyword unpacks an array's elements, starting with the 0th element. Since you say the subject and content are wrong, my money is on the return value of getNotificationViewHTML.
To test, just hardcode getNotificationViewHTML like this:
public function getNotificationViewHTML()
{
return ["This Is the Subject", "<h1>This is My Message</h1>"];
}
If your mail then comes through with those values, you need to tailor your getNotificationViewHTML return value to match the example's format.

Laravel Test That Job Is Released

I want to test that the job has been released back onto the queue in certain circumstances.
This is my job class:
class ChargeOrder extends Job
{
use InteractsWithQueue, SerializesModels;
/**
* The order model which is to be charged
*/
protected $order;
/**
* The token or card_id which allows us to take payment
*/
protected $source;
/**
* Create a new job instance.
*
* #param App\Order $order;
* #param string $source;
* #return array
*/
public function __construct($order, $source)
{
$this->order = $order;
$this->source = $source;
}
/**
* Execute the job.
*
* #return void
*/
public function handle(Charge $charge)
{
$result = $charge->execute($this->source, $this->order->totalInclVat());
$exception_errors = config('payment.errors.exception_errors');
// If we have an error that isn't caused by the user (anything but a card error)
// We're going to notify ourselves via slack so we can investigate.
if (array_key_exists('error', $result) && in_array($result['error']['code'], array_keys(config('payment.errors.other_error'))))
{
$client = new Client(config('services.slack.channels.payment_errors.url'), config('services.slack.channels.payment_errors.settings'));
$client->send(app()->environment() . ": " . $result['error']['code']);
}
// If the error is in the list of errors that throw an exception, then throw it.
if (array_key_exists('error', $result) && (in_array($result['error']['type'], $exception_errors) || in_array($result['error']['code'], $exception_errors)))
{
$status_code = config('payment.errors')[$result['error']['type']][$result['error']['code']]['status_code'];
$message = config('payment.errors')[$result['error']['type']][$result['error']['code']]['message'];
throw new BillingErrorException($status_code, $message);
}
// If we still have an error, then it something out of the user's control.
// This could be a network error, or an error with the payment system
// Therefore, we're going to throw this job back onto the queue so it can be processed later.
if (array_key_exists('error', $result) && in_array($result['error']['code'], array_keys(config('payment.errors.other_error'))))
{
$this->release(60);
}
}
}
I need to test that "$this->release(60)" is called in certain circumstances.
I'm trying to mock the job contract as so, in my tests:
// Set Up
$this->job = Mockery::mock('Illuminate\Contracts\Queue\Job');
$this->app->instance('Illuminate\Contracts\Queue\Job', $this->job);
And then
// During Test
$this->job->shouldReceive('release')->once();
But this isn't working.
Anybody have any ideas?
Try adding the following in you test before dispatching the job:
Queue::after(function (JobProcessed $event) {
$this->assertTrue($event->job->isReleased());
});
The code above will be triggered after the job is done and checks that the job has been released.
Make sure to remove any calls to Queue::fake()and $this->expectsJob() since these will prevent the actual job from being executed.
I solved this problem by creating an event that is only fired after the job is released back into the queue. Then in my tests I can use the Event mocks to watch for that event after I dispatch a job and know if we released it back into the queue or not.
// In your job
$this->release();
event(new MyJobReleasedEvent()); // Every time you have a release() call
// In your Unit Test
Event::fake([MyJobReleasedEvent::class]);
dispatch(new MyJob();
Event::assertDispatched(MyJobReleasedEvent::class);
If you wanted to get fancy I'm sure you could wire up your own Job class that did this automatically when release() was called, but I needed it infrequently enough to just do it inline as-needed.

Resources