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

How Laravel prevents your scheduled jobs from overlapping

Updated: Jan 5, 2019 — 2 min Read#scheduling

Sometimes a scheduled job takes more time to run than what we initially expected, and this causes another instance of the job to start while the first one is not done yet, for example imagine that we run a job that generates a report every minute, after sometime when the data gets huge the report generation might take more than 1 minute so another instance of that job starts while the first is still ongoing.

In most scenarios this is fine, but sometimes this should be prevented in order to guarantee correct data or prevent a high server resources consumption, so let's see how you can prevent such scenario in laravel:

$schedule->command('mail:send')->withoutOverlapping();

Laravel will check for the Console\Scheduling\Event::withoutOverlapping class property and if it's set to true it'll try to create a mutex for the job, and will only run the job if creating a mutex was possible.

But what's a mutex?

Here's the most interesting explanation I could find online:

When I am having a big heated discussion at work, I use a rubber chicken which I keep in my desk for just such occasions. The person holding the chicken is the only person who is allowed to talk. If you don't hold the chicken you cannot speak. You can only indicate that you want the chicken and wait until you get it before you speak. Once you have finished speaking, you can hand the chicken back to the moderator who will hand it to the next person to speak. This ensures that people do not speak over each other, and also have their own space to talk. Replace Chicken with Mutex and person with thread and you basically have the concept of a mutex.
-- https://stackoverflow.com/questions/34524/what-is-a-mutex/34558#34558

So Laravel creates a mutex when the job starts the very first time, and then every time the job runs it checks if the mutex exists and only runs the job if it doesn't.

Here's what happens inside the withoutOverlapping method:

public function withoutOverlapping()
{
    $this->withoutOverlapping = true;

    return $this->then(function () {
        $this->mutex->forget($this);
    })->skip(function () {
        return $this->mutex->exists($this);
    });
}

So Laravel creates a filter-callback method that instructs the Schedule Manager to ignore the task if a mutex still exists, it also creates an after-callback that clears the mutex after an instance of the task is done.

Also before running the job, Laravel does the following check inside the Console\Scheduling\Event::run() method:

if ($this->withoutOverlapping && ! $this->mutex->create($this)) {
    return;
}

Where does the mutex property come from?

While the instance of Console\Scheduling\Schedule is being instantiated, laravel checks if an implementation to the Console\Scheduling\Mutex interface was bound to the container, if yes it uses that instance but if not it uses an instance of Console\Scheduling\CacheMutex:

$this->mutex = $container->bound(Mutex::class)
                        ? $container->make(Mutex::class)
                        : $container->make(CacheMutex::class);

Now while the Schedule Manager is registering your events it'll pass an instance of the mutex:

$this->events[] = new Event($this->mutex, $command);

By default Laravel uses a cache-based mutex, but you can override that and implement your own mutex approach & bind it to the container.

The cache-based mutex

The CacheMutex class contains 3 simple methods, it uses the event mutex name as a cache key:

public function create(Event $event)
{
    return $this->cache->add($event->mutexName(), true, 1440);
}

public function exists(Event $event)
{
    return $this->cache->has($event->mutexName());
}

public function forget(Event $event)
{
    $this->cache->forget($event->mutexName());
}

Mutex removal after task finishes

As we've seen before, the manager registers an after-callback that removes the mutex after the task is done, for a task that runs a command on the OS that might be enough to ensure that the mutex is cleared, but for a callback task the script might die while executing the callback, so to prevent that an extra fallback was added in Console\Scheduling\CallbackEvent::run():

register_shutdown_function(function () {
    $this->removeMutex();
});

This removes the mutex in case the script shuts down un-expectedly.

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.