Laravel Queue job holding web service response - laravel

I have implemented queue jobs in my lumen v5.6 projecs. For queue jobs i am using database driver. But the problem is that when I dispatch job and put sleep method for 15 seconds in job handle() method then job api response hold for 15 second untile the whole job is fired. But I have to sent back api response immediatly as I dispatch job in background.
namespace App\Jobs;
use App\Helpers\NotificationProcess;
class SendPushNotification extends Job {
protected $queueData;
protected $activity;
protected $activity_user;
protected $post;
protected $mentions;
protected $noti;
protected $mention_activities;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($data) {
\Log::info("Queue Start Time:: " . microtime());
$this->queueData = $data;
$this->activity = $data['activity'];
$this->activity_user = $data['user'];
$this->post = $data['post'];
$this->mentions = $data['mentions'];
$this->mention_activities = $data['mention_activities'];
$this->noti = new NotificationProcess();
}
/**
* Execute the job.
*
* #return void
*/
public function handle() {
try {
\Log::info("wait for 15 seconds");
sleep(15);
$receiver = \App\Models\DeviceToken::getDeviceTokensByUsers([$this->post->user_id], $this->activity_user->id, $this->activity['domain']);
if (!$receiver->isEmpty() && !empty($this->activity))
$this->noti->sendCommentPushNotification($this->activity, $receiver[0]->toArray(), $this->activity_user);
if (!empty($this->mentions)) {
$receivers = \App\Models\DeviceToken::getDeviceTokensByUsers($this->mentions, $this->activity_user->id, 'M');
if (!$receivers->isEmpty() && !empty($this->mention_activities))
$this->noti->sendMentionPushNotifications($this->mention_activities, $receivers->toArray(), $this->activity_user);
}
\Log::info("==== Notifications sent ===");
return true;
} catch (\Exception $ex) {
\Log::info("Exception::".$ex->getMessage());
return false;
}
}
}
Now I just want to send back response immediatly to api as I dispatch job for background process it should not hold the response of api.
Note: One job can be processed in 2 min but it should not effect the response of api

Related

How to delay Laravel Job Queue

I am trying to learn about Jobs and queues in Laravel , when i try to learn something new i always take a basic example by myself and try to understand the workflow better.
Okay here is the problem
I have created a Job in Laravel as you can see in the handle method i am just trying to print a simple message on the laravel.logger , this works totally fine.
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $email;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($email)
{
$this->email = $email;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
Log::info($this->email . '<<<<<<<<<<<<<<<');
}
}
My problem is that i want to delay this job for 2 minutes then to dispatch it ,
i have tried it this way but without success both logs are printed immediately when index method is being called but i want $job = new SendEmailJob("This will show after 2 minutes"); to be called after 2 minutes an not printed immediately
public function index(){
$on = Carbon::now()->addMinutes(2);
Log::info('Test');
$job = new SendEmailJob("This will show after 2 minutes");
$job->delay($on);
dispatch($job);
return "none";
}
You can take a look at the documentation: https://laravel.com/docs/8.x/queues#delayed-dispatching
You can do following: (new SendEmailJob("This will show after 2 minutes"))->delay(now()->addMinutes(2)); or
SendEmailJob::dispatch("This will show after 2 minutes")->delay(now()->addMinutes(2));

Automatic dependency injection handle exception laravel

Here is my case
I have cron job(console command)
/**
* Execute the console command.
*
* #return mixed
*/
public function handle(OrdersMagentoService $magentoService)
{
try {
$orders = $magentoService->getRemoteOrders();
print_r($orders);
} catch (SoapFault $e) {
$this->line('Error connect to soap api, error: ' . $e->getMessage());
die;
} catch (\Throwable | \Exception $e) {
print_r($e->getMessage());
die;
}
}
In handle method i automaticaly inject OrdersMagentoService, this service do connect to magento soap api and exctend from BaseMagentoService
class OrdersMagentoService extends BaseMagentoService
{
public function getRemoteOrders()
{
$complex = [
'complex_filter' => [
[
'key' => 'status',
'value' =>
[
'key' => 'in',
'value' => 'closed,canceled,holded,processing,complete'
]
],
[
'key' => 'updated_at',
'value' => [
'key' => 'from',
'value' => now()->subDays(200),
]
]
]
];
return $this->salesOrderList($complex);
}
}
class BaseMagentoService
{
/**
* #var
*/
private $client;
/**
* #var
*/
private $session;
/**
* #var \Illuminate\Config\Repository|mixed
*/
protected $config;
/**
* BaseMagentoService constructor.
*/
public function __construct()
{
$this->config = config('services.magento');
$this->connect();
}
/**
* Do connect to soap api v2
*
* #throws \SoapFault
*/
private function connect()
{
$this->client = new SoapClient($this->getApiUrl());
$this->session = $this->client->login($this->config['user_name'], $this->config['password']);
}
public function __call($resource, $arguments)
{
return $this->client->$resource($this->session, ...$arguments);
}
}
In BaseMagentoService constructor i create connection to magento soap api. But if connection throw error(for example wrong username and pass) then i can't handle this in cron job file. I understand that laravel at first create OrdersMagentoService, it throw error and try catch in handle function not works, but i dont know how to fix this.
I can add in handle method
try {
$magentoService = resolve(OrdersMagentoService::class)
$orders = $magentoService->getRemoteOrders();
print_r($orders);
} catch (SoapFault $e) {
$this->line('Error connect to soap api, error: ' . $e->getMessage());
die;
} catch (\Throwable | \Exception $e) {
print_r($e->getMessage());
die;
}
And remove automatic DI, and tit should work well, but i dont want to do it.
Also if i add some try catch in connect method of BaseMagentoService then i cant log this error in my cron job.
What is best way to handle this?
First I would switch the handling from the artisan command to a Job or an Event that might be more suited to the problem. Then your artisan command simply fires off this new job or event each time is executed.
This is also somehow stated in the docs as a best practice:
For greater code reuse, it is good practice to keep your console commands light and let them defer to application services to accomplish their tasks.
Then if you look at the documentation about jobs, there is a section about error handling for failed jobs where you are told that you can define a method that gets triggered once he job has failed. Example excerpt taken from the docs:
class YourJob implements ShouldQueue
{
/**
* Execute the job.
*
* #param AudioProcessor $processor
* #return void
*/
public function handle(AudioProcessor $processor)
{
// Process uploaded podcast...
}
/**
* The job failed to process.
*
* #param Exception $exception
* #return void
*/
public function failed(Exception $exception)
{
// Send user notification of failure, etc...
}
}
You can read more in the documentation. There is also a paragraph about global jobs failure handling that might suit your use case

Laravel: How to write a integration test for Notification that sends email

How one would test email is sent as a final outcome after triggering a notification or doing an action that triggers notification?
Ideally, there is a notification merely for sending an email. My first thought was to trigger it and then check if Mail::assertSent() is sent. However, it appears that this does not work as Notification returns Mailable but does not invoke Mail::send().
Relevant GitHub issue: https://github.com/laravel/framework/issues/27848
My first approach for test:
/** #test */
public function notification_should_send_email()
{
Mail::fake();
Mail::assertNothingSent();
// trigger notification
Notification::route('mail', 'email#example.com')
->notify(new SendEmailNotification());
Mail::assertSent(FakeMailable::class);
}
while the Notification toMail() method looks as:
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\FakeMailable
*/
public function toMail($notifiable)
{
return (new FakeMailable())
->to($notifiable->routes['mail']);
}
The set-up example is available https://github.com/flexchar/laravel_mail_testing_issue
You can use mailCatcher then extends your TestCase
class MailCatcherTestCase extends TestCase
{
protected $mailCatcher;
/**
* MailCatcherTestCase constructor.
* #param $mailCatcher
*/
public function __construct($name = null, array $data = [], $dataName = ''
) {
parent::__construct($name, $data, $dataName);
$this->mailCatcher = new Client(['base_uri' => "http://127.0.0.1:1080"]);
}
protected function removeAllEmails() {
$this->mailCatcher->delete('/messages');
}
protected function getLastEmail() {
$emails = $this->getAllEmail();
$emails[count($emails) - 1];
$emailId = $emails[count($emails) - 1]['id'];
return $this->mailCatcher->get("/messages/{$emailId}.json");
}
protected function assertEmailWasSentTo($recipient, $email) {
$recipients = json_decode(((string)$email->getBody()),
true)['recipients'];
$this->assertContains("<{$recipient}>", $recipients);
}
}
then you can use in you test
/** #test */
public function notification_should_send_email()
{
// trigger notification
Notification::route('mail', 'email#example.com')
->notify(new SendEmailNotification());
$email = $this->getLastEmail();
$this->assertEmailWasSentTo($email, 'email#example.com');
}
since you can fetch the mail, so that you can test mail body, subject, cc, attachment etc.
don't forget to remove all mails in tearDown
hope this helps.

How to fail a job and make it skip next attempts in the queue in Laravel?

I'm writing a simple queue.
namespace App\Jobs;
use App\SomeMessageSender;
class MessageJob extends Job
{
protected $to;
protected $text;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($to, $text)
{
$this->to = $to;
$this->text = $text;
}
/**
* Execute the job.
*
* #return void
*/
public function handle(SomeMessageSender $sender)
{
if ($sender->paramsAreValid($this->to, $this->text) {
$sender->sendMessage($this->to, $this->text);
}
else {
// Fail without being attempted any further
throw new Exception ('The message params are not valid');
}
}
}
If the params are not valid the above code will throw an exception which causes the job to fail but if it still has attempts left, it will be tried again. Instead I want to force this to fail instantly and never attempt again.
How can I do this?
Use the InteractsWithQueue trait and call either delete() if you want to delete the job, or fail($exception = null) if you want to fail it. Failing the job means it will be deleted, logged into the failed_jobs table and the JobFailed event is triggered.
You can specify the number of times the job may be attempted by using $tries in your job.
namespace App\Jobs;
use App\SomeMessageSender;
class MessageJob extends Job
{
/**
* The number of times the job may be attempted.
*
* #var int
*/
public $tries = 1;
}

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