Laravel Queues in Action (2nd edition) is now available!

Running The Same Queued Job Multiple Times

Updated: Sep 8, 2020 — 2 min Read#queues

Here's the challenge we're trying to deal with:

When users add items to their shopping cart and start the checkout process, you want to reserve these items for them. However, if a user abandoned an order—they never canceled or checked out—you will want to release the reserved items back into stock so other people can order them.

To do this, we're going to schedule a job once a user starts the checkout process. This job will check the order status after an hour and cancel it automatically if it wasn't completed by then.

Delay Processing a Job

Let's see how such job can be dispatched from the controller action:

class CheckoutController
{
    public function store()
    {
        $order = Order::create([
            'status' => Order::PENDING,
            // ...
        ]);

        dispatch(new MonitorPendingOrder($order))
        ->delay(3600);
    }
}

By chaining the delay(3600) method after dispatch(), the MonitorPendingOrder job will be pushed to the queue with a delay of 3600 seconds (1 hour); workers will not process this job before the hour passes.

You can also set the delay using a DateTimeInterface implementation:

dispatch(new MonitorPendingOrder($order))
->delay(
    now()->addHour()
);

{warning} Using the SQS driver, you can only delay a job for 15 minutes. If you want to delay jobs for more, you'll need to delay for 15 minutes first and then keep releasing the job back to queue using release(). You should also know that SQS stores the job for only 12 hours after it was enqueued.

Here's a quick look inside the handle() method of that job:

public function handle()
{
    if ($this->order->status == Order::CONFIRMED ||
        $this->order->status == Order::CANCELED){
        return;
    }

    $this->order->markAsCanceled();
}

When the job runs—after an hour—, we'll check if the order was canceled or confirmed and just return from the handle() method. Using return will make the worker consider the job as successful and remove it from the queue.


👋 This post is part of the Laravel Queues in Action eBook. Check it out for similar queue-related challenges as well as a complete guide on Laravel Queues.


Finally, we're going to cancel the order if it was still pending.

Sending Users a Reminder Before Canceling

It might be a good idea to send the user an SMS notification to remind them about their order before completely canceling it. So let's send an SMS every 15 minutes until the user completes the checkout or we cancel the order after 1 hour.

To do this, we're going to delay dispatching the job for 15 minutes instead of an hour:

dispatch(new MonitorPendingOrder($order))
->delay(
    now()->addMinutes(15)
);

When the job runs, we want to check if an hour has passed and cancel the order.

If we're still within the hour period, then we'll send an SMS reminder and release the job back to queue with a 15-minute delay.

public function handle()
{
    if ($this->order->status == Order::CONFIRMED ||
        $this->order->status == Order::CANCELED){
        return;
    }

    if ($this->order->olderThan(59, 'minutes')){
        $this->order->markAsCanceled();

        return;
    }

    SMS::send(...);

    $this->release(
        now()->addMinutes(15)
    );
}

Using release() inside a job has the same effect as using delay() while dispatching. The job will be released back to the queue and workers will run it again after 15 minutes.

Ensuring the Job Has Enough Attempts

Every time the job is released back to the queue, it'll count as an attempt. We need to make sure our job has enough $tries to run 4 times:

class MonitorPendingOrder implements ShouldQueue
{
    public $tries = 4;
}

This job will now run:

15 minutes after checkout
30 minutes after checkout
45 minutes after checkout
60 minutes after checkout

If the user confirmed or canceled the order say after 20 minutes, the job will be deleted from the queue when it runs on the attempt at 30 minutes and no SMS will be sent.

This is because we have this check at the beginning of the handle() method:

if ($this->order->status == Order::CONFIRMED ||
    $this->order->status == Order::CANCELED){
    return;
}

A Note on Job Delays

There's no guarantee workers will pick the job exactly after the delay period passes. If the queue is busy and not enough workers are running, our MonitorPendingOrder job may not run enough times to send the 3 SMS reminders before canceling the order.

To increase the chance of your delayed jobs getting processed on time, you need to make sure you have enough workers to empty the queue as fast as possible. This way, by the time the job becomes available, a worker process will be available to run it.

Hey! 👋 If you find this content useful, consider sponsoring me on GitHub.

You can also follow me on Twitter, I regularly post about all things Laravel including my latest video tutorials and blog posts.

By Mohamed Said

Hello! I'm a former Laravel core team member & VP of Engineering at Foodics. In this publication, I share everything I know about Laravel's core, packages, and tools.

You can find me on Twitter and Github.

This site was built using Wink. Follow the RSS Feed.