1

The problem:

I have a job in Laravel, based on the condition that an API is reachable it should either run or be released a day later.

The condition to check if the API is reachable works perfectly. The problem, however, occurs when the job is released again. I defined it as $this->release($dayInSeconds); where $dayInSeconds = 86400;. So, according to my understanding, the Job should be released to queue again, after 86400 seconds (a day).

The docs defines this behaviour here: Manually releasing a job, and this (old) answer also confirms that I understand the release() method correctly. Laravel 4.2 queues what does $job->release() do?.

However, when I call $this->release($dayInSeconds) the job is released again, ranging with a delay of 6 minutes to 4 hours. (We get notifications in a dedicated Teams channel when this happens). However, this should only happen after a day, not after 6 minutes or 4 hours.

The question:

Why is my Job not being released after a day, even though I think I have the correct understanding of the release() method? Am I missing something or somehow still understanding the release() method wrong?

Useful information:

  • Laravel version: 8
  • Queue driver: database

Useful code snippets:

The Job:

class SendOrderTo<REDACTED> implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ConsoleBaseMethodsTrait;

    private int $dayInSeconds = 86400;
    public $tries = 5;
    public $timeout = 60;
    public $backoff = 300;
    public $order;

    public function __construct (Order $order)
    {
        $this->order = $order;
    }

    public function handle ()
    {
        if (!$this->isApiReachable()) {

            // Re-schedule the job for a day later
            $this->release($this->dayInSeconds);

            // Notify on Teams Alert channel.
            $orderId = $this->order->id;
            $orderHostName = $this->order->host->name ?? NULL;
            TeamsTrait::notifyOnTeams('<REDACTED> Job ' . $orderHostName . ' order ' . $orderId . ' has been re-scheduled.',
                'Due to an outage in the <REDACTED> Service this job has been delayed by a day.');
        }

        // Other logic in the handle() that is not relevant for the question.
    }

    public function failed (Exception $e)
    {
        // Just some logging, also not relevant.
    }

    private function isApiReachable () : bool
    {
        $data = getServicesAvailabilityFile();

        return $data->services->api ?? false;
    }

Clarifications:

I used REDACTED in some spaces, this means I am unable to publicly show this name, should not impact the question.

$data in the isApiReachable() method is a JSON file, looks something like this, it returns either true or false:

{"services":{"api":true,"other":true,}}
geertjanknapen
  • 1,159
  • 8
  • 23

3 Answers3

1

If I understood correctly, you want to use the queue more like a cron runner, scheduling tasks over long periods of time.

These queues are more intended to respond to messages fairly quickly. Hence it's not really built for very long release timeouts. The values in Laravel documentation are more like 5 or 10 seconds. It's more intended to do some short term delays intended to keep the queue functioning properly.

If you want to use it with longer timeouts, it will probably still work, after modifying any other timeouts that release the job earlier in your application configuration.

The jobs are ultimately selected on the available_at field, it should be the right date calculated from your $delay. See DatabaseQueue. Does your job's database record have the expected date in that field? If it does, it means something else is releasing the job early.

/**
 * Modify the query to check for available jobs.
 *
 * @param  \Illuminate\Database\Query\Builder  $query
 * @return void
 */
protected function isAvailable($query)
{
    $query->where(function ($query) {
        $query->whereNull('reserved_at')
              ->where('available_at', '<=', $this->currentTime());
    });
}

Task scheduling

Laravel also comes with a task scheduling component. If you schedule it to run only once a day, you don't even need to make the job unique.

This seems like a better choice if you need cron like behavior.

Unique lock

Another thing you could try in your existing code, as you already have a unique identifier ($this->order->id), is "unique jobs". You can specify how long it needs to be unique, and that has a 1 hour value in the example. Probably it will work for a day too, but I'm not sure if it would affect if the job itself is released.

More info would be nice

Can you give more detail about the use case? Why is the delay one day? It's hard to imagine a case where this would work well. You don't need that much bad luck to delay it by multiple days. If you happen to be executing always at a bad time of day, the order might never complete. Hence using a CRON schedule helps you control the timings better.

inwerpsel
  • 2,677
  • 1
  • 14
  • 21
  • As for some more info: It sends an order made in our systems to a 3rd party API so we can get a track and trace, shipping details, and the whole shebang. Unfortunately, sometimes that API is down for reasons still unbeknownst to us. Therefore we want to have this job that checks if the service is up, and send the order. If the service is down, we want to delay sending that order by a day so it is tried again the next day. Down again? Delay a day again. Sorry for late reply, been on holiday, help would still be appreciated. – geertjanknapen Aug 23 '22 at 12:43
  • I guess what I really want is that the job gets removed from the queue and then put back on the queue after a day, but since I am a junior developer I haven't found a good way to figure out how to do that. And to be frank, I find the Laravel docs on tasks, etc. a tad confusing. My team lead wanted me to implement a solution as explained in the question, and in theory (read; my brain) it works, however in practice it does not work as intended. – geertjanknapen Aug 23 '22 at 12:45
  • @geertjanknapen You could create a separate queue, to which you only add the failed items. Then you can run a scheduled job on that other queue to retry all failed items. That way it can efficiently bail out on them all if the service is not available. It also maximizes your ability to use the time periods where this 3rd party API is available. – inwerpsel Aug 23 '22 at 13:24
  • That seems easier to me than figuring out the configuration changes needed to make it hold a job for that long. You could still have a shot at that too, though, I guess it should be possible to find out why a job is being released by inspecting the database records. – inwerpsel Aug 23 '22 at 13:27
  • Yet another approach would be to just let it retry more often. If you ensure that checking the API status is the first thing the job does, it wouldn't have much impact to run it very frequently. If this is possible it's by far the easiest way forward for you. – inwerpsel Aug 23 '22 at 13:32
0

i was with the same issue, and discover that when you want to release a job, after do the command you need to return:

 public function handle ()
{
    if (!$this->isApiReachable()) {

        // Re-schedule the job for a day later
        $this->release($this->dayInSeconds);

        // Notify on Teams Alert channel.
        // Your code...

        // DO THE RETURN 
        return;
    }

    // Other logic in the handle() that is not relevant for the question.
}

It works for me, hope can help you too.

0

I had a problem like this, which retried 5 times (public int $tries property) immediately. And I realized that was because the job had unhandled exception, before reach $this->release($delay) line. So Laravel retried automatically the job as many as $tries property.

So you can wrap that code block in a try-catch block to prevent this.

More information about error handling in Laravel