I have a queue that sends requests to a remote service. Sometimes this service undergoes a maintenance. I want all queue tasks to pause and retry in 10 minutes when such situation is encountered. How do I implement that?
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));
}
}
}
Related
I am using the laravel middleware withoutOverlapping in laravel but it does not seem to work when jobs are dispatched at the same time (and take < 1 second)
Example
class TestWaitJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public Charge $charge;
public function __construct(Charge $charge)
{
$this->charge = $charge;
}
/**
* #return array
*/
public function middleware(): array
{
return [
(new WithoutOverlapping($this->charge->id))
->releaseAfter(30)
];
}
/**
* #return void
*/
public function handle()
{
$charge = $this->charge->current_state_key_name;
return;
}
}
This is an absolutely simple job. It does nothing and it is what I have used to test.
If I go to my application and dispatch two copies of the job at the same time (using tinker)
TestWaitJob::dispatch(Charge::Find(1)); TestWaitJob::dispatch(Charge::find(1));1;
Both jobs are processed by horizon at the same time.
If I add a super simple sleep(1) line to the handle method of the job I get the expected behaviour which is
Job begins processing and acquires lock
The next job cannot acquire lock so is released back to the queue.
So with this sleep line, I have a job processed immediately (in 1 second) and the next job completes 31 seconds later which matches up exactly with the releaseAfter(30 seconds)
I have been looking at this for hours and everytime I introduce a delay the jobs process as expected but with no delay they process at the same time. The application is financial in nature so I cannot afford to potentially process jobs at the same time
Any help/advice would be greatly appreciated. (ps I am using Redis as cache)
I'm trying to dispatch jobs from a controller, but sometimes the queue works, and sometimes it will execute the code immediately. On the other hand, dispatching from a command works fine, so I wonder what the difference could be.
Things that I have tried:
I tried database and Redis connections and got the problem for both connections
Without running queue listen/work, the job will randomly execute, so listen/work cannot be the problem
Checked webserver/PHP log for errors, no result
Something goes wrong before the static dispatch function. When I put a die in the static dispatch function, it will sometimes kill the app, and sometimes it will still run decode in the handle immediately....
vendor/laravel/framework/src/Illuminate/Foundation/Bus/Dispatchable.php
public static function dispatch()
{
die('xxx');
return new PendingDispatch(new static(...func_get_args()));
}
I'm trying to find what happened before the dispatch function, but I cannot find any function that executes before the static handle function. I have the feeling that something is crashing in PHP and alternatively execute the job immediately, but I cannot find where that happens in the core.
Controller
class CreateTestJob
{
public function __invoke(Request $request)
{
TestJob::dispatch(rand(0, 999999));
}
}
Job
class TestJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct($test)
{
$this->test = $test;
}
public function handle()
{
sleep(30);
echo 'done';
}
}
I also tried to create a syntax error in de handle function of the TestJob so that I can see the stack trace, but it shows "1 unknown frame". I wonder what that is
you can check fail_jobs table if there is any error while running your job.
The job will be immediately executed only when you use synchronous driver or synchronous dispatch(for use in local development).
<?php Somejob::dispatchSync();
Jobs on Redis and database connection will be executed only when you run queue worker.
php artisan queue:work
I've got a problem while using Laravel's job Deserialization.
This is the Job class that is queued in the database:
class SendRatingEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $order, $user;
public function __construct(User $user, Order $order)
{
$this->order = $order;
$this->user = $user;
}
public function handle()
{
if ($this->order->isRatedByUser($this->user->id)) {
return;
}
Mail::to($this->user->email)->queue(new RatingEmail($this->order, $this->user));
}
}
In the class Order.php, I dispatch this job like this:
class Order {
function queueRating()
{
$when = Carbon::now()->addDays(env('ORDER_DAYS_RATING', 8));
dispatch((new SendRatingEmail($this->buyer, $this))->delay($when));
}
}
So the problem is in the job's handle() function, specifically the error is:
Call to undefined method Illuminate\Database\Query\Builder::isRatedByUser()
It seems as though Laravel gives me the wrong object, instead of App\Order it gives me the QueryBuilder. In the queueRating() function I have checked that the types given in the constructor are the expected types. I have even tested a workaround which also didn't seem to work:
if($this->order instanceof \Illuminate\Database\Query\Builder) {
$this->order = $this->order->first();
}
Also I have looked in the jobs table, and it seems as if the saved models are correct (App\Order)
Edit
Here is the code where the queueRating() function is called. The file is StripeController which handles credit card payments.
public function orderPaid($order) {
$order->payment_done = 1;
$order->save();
$order->chat->open = 1;
$order->chat->save();
$order->queueRating();
}
I found the problem, as always the problem is not where I looked. As it turns out, the code is completely fine, but I forgot to restart the queue worker on the staging server, which meant that changes in the code base were not applied in the queue worker. So the application and queue used different versions.
It is even stated in the Laravel Queue Documentation:
Remember, queue workers are long-lived processes and store the booted application state in memory. As a result, they will not notice changes in your code base after they have been started. So, during your deployment process, be sure to restart your queue workers.
I use the bugtracker Bugsnag, which shows the error and also the line numbers in the code. I had noticed that the line number where the error occurred mismatched the real line number, but couldn't figure out that this was causing the problem.
Introduction / System Architecture
Server A - Laravel Installation, Database A
Server B - RabbitMQ Server
Server C - Laravel Installation, Database B
Server A is an API Endpoint, only receiving calls from remote sources. Depending on the call, it'll add a job to the RabbitMQ Server (Server B), which in turn is processed/listened to by Server C.
Server A contains a local copy of the exact same job that is processed by Server C. The job handler code and constructor are shown below (Job Code).
The Issue:
Server A can not serialize the job and add it to the queue, as it attempts to access information about a monitor that only exists in Database B (Server C). Server A has a copy of the model, but does not contain the actual database tables or records as it has no use for them - it's only meant to serialize the job and say "This is what you (Server C) should be doing."
However, upon issuing the job, it's also attempting to fetch database information (likely to serialize the exact data that will be required), which it fails to do as the records don't exist there.
My understanding of Laravel's SerializesModels was specifically that it would only serialize the model call itself, without actually doing anything database related. This does not appear to function, or I am misunderstanding/using it incorrectly - although very little documentation appears to be available.
Workarounds: One possible workaround would be to simply give Server A access to the database on Server C. This is in this case not an option, as it would break the design which is intended for high availability (where the API endpoint and queue should never be unavailable, but where the queue processor might be).
The code
Relevant Job Code
// Models
use App\UptimeMonitor;
// Notifications
use App\Notifications\StatusQueue as StatusNotification;
class StatusQueue implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $monitor_source;
protected $monitor_data;
protected $monitor_repository;
/**
* Create a new job instance.
*
* #param string[] monitor_source (Eg: HetrixTools)
* #param array[] monitor_data (All data passed from the source)
*
*/
public function __construct($monitor_source, array $monitor_data)
{
$this->monitor_source = $monitor_source;
$this->monitor_data = $monitor_data;
if($this->monitor_source === 'centric')
return $this->centric();
}
/**
* Centric Uptime Monitoring
*/
public function centric()
{
$result = ($this->monitor_data['monitor_status'] == 'online') ? 'online' : 'timeout';
try {
$monitor = UptimeMonitor::where('identifier', '=', $this->monitor_data['monitor_id'])->firstOrFail();
$status = $monitor->status()->firstOrFail();
$contacts = $monitor->contacts()->get();
} catch (Exception $e) {
return Log::error('[JOBS::StatusQueue::centric] - ' . $e);
}
$status->state = $result;
if(!$contacts)
return true;
foreach($contacts as $contact) {
$contact->notify(new StatusNotification($monitor, $status));
}
}
}
Other code
If you do require any other code, let me know! This should however cover the entire functionality of the job class itself. Other than that, all that's happening is issuing that job - and how that's done is obvious based on the constructor.
Question
The final question from all of this: Why is this failing (as in; why can it not serialize the job, without needing the database information?) - and do you see a way to work around this issue, to where I do not need access to the database from Server C to queue the job from Server A, still using Laravel's Queue mechanics?
Much obliged, as always!
Turns out, the easiest solution is almost always the right one.
Server A does not need to have a replica of the job that Server B will process - it can have a completely empty job with the same class, and server B will still process it correctly.
As a result, this is now the Job on Server A:
class AlertQueue implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $job;
/**
* Create a new job instance.
*
*/
public function __construct()
{
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
//
}
}
Whilst Server B also has an AlertQueue job, which has all of the logic that will actually get performed.
I have a job in my Laravel project (v.4.2). which is used for inserting data into database. The class named "ProductUpdate". I use Amazon SQS for queue service.
What makes me confuse now is, when I changed the code in class "ProductUpdate",
it seems that the job is running by using old version of the class.
I even deleted all lines of code in the class but the jobs can still be able to run ( it stills inserts data).
Following is the job class.
The file of this class is at app/jobs/ProductUpdate.php
In my understanding, job class is the only place that will be called from queue, but why it can still be able to run when I deleted all the codes?
<?php
/**
* Here is a class to run a queued item sent from SQS
* Default method to use is fire()
**/
class ProductUpdate
{
public function fire($job, $data)
{
// Disable query log
DB::connection()->disableQueryLog();
// Set the job as a var so it will be used across functions
$this->job = $job;
$product = Product::find($productID);
if($product->product_type != 18) {
// Call the updater from library
$updater = App::make('Product\PriceUpdater');
$updater->update($product);
}
// Done and delete
$this->success();
}
private function success()
{
$this->job->delete();
}
private function fail($messages = array())
{
Log::error('Job processing fail', $messages);
$this->job->delete();
}
}
Your problem is related to cache.
Run this command in terminal to remove all cached data.
php artisan cache:clear
other way:-
Illuminate\Cache\FileStore has the function flush, so you can also use it:
Cache::flush();
This link will also help you :)