Skip to content

A Laravel package that provides a `synchronized` function that uses atomic locks to prevent a critical section of code from running in parallel across multiple requests.

License

Notifications You must be signed in to change notification settings

freerkminnema/synchronized

Repository files navigation

                      _                     _             _ 
                     | |                   (_)           | |
 ___ _   _ _ __   ___| |__  _ __ ___  _ __  _ _______  __| |
/ __| | | | '_ \ / __| '_ \| '__/ _ \| '_ \| |_  / _ \/ _` |
\__ \ |_| | | | | (__| | | | | | (_) | | | | |/ /  __/ (_| |
|___/\__, |_| |_|\___|_| |_|_|  \___/|_| |_|_/___\___|\__,_|
      __/ |                                                 
     |___/                                       for Laravel              

This package installs a global synchronized function into your Laravel application that ensures that the given callable is always executed one at a time, even if multiple requests are coming in.

So if your application receives ten requests in parallel, and part of your code is wrapped in the synchronized function, that block will be executed sequentially.

Requires a supported cache driver

This function uses the Cache Atomic Locks feature of Laravel. To utilize this feature, your application must be using the memcached, redis, dynamodb, database, or file cache driver as your application's default cache driver. In addition, if relevant, all web servers must be communicating with the same central cache server.

Example usage

In its most elegant form, you can pass a simple closure to the synchronized function:

$ticketNumber = synchronized(fn () => Cache::increment('ticket-number'));

How does it work?

Internally, synchronized generates an Atomic Lock Key (which is simply a hashed string) based on the location of and variables in the callable. This is just like how the ✨magic✨ once function works.

Providing your own Atomic Lock Key

In some cases, you may want to provide your own Atomic Lock Key. A contrived example is provided below:

$nameOnTicket = Request::get('name-on-ticket');
$ticket = synchronized(function () use ($nameOnTicket) {
    // This is bad, because everytime $nameOnTicket has a
    // different value, the Atomic Lock Key will be different.
    return [
        'name' => $nameOnTicket,
        'number' => Cache::increment('ticket-number'),
    ];
});

Everytime $nameOnTicket has a different value, a different Atomic Lock Key will be autogenerated, and it will probably not work as you intended.

To resolve this, you may tweak your closure to be less dependent on outside variables OR provide your own $key as the second variable.

$nameOnTicket = Request::get('name-on-ticket');
$ticket = synchronized(function () use ($nameOnTicket) {
    // Now it doesn't matter what the value of
    // $nameOnTicket is, since the Atomic Lock Key is fixed.
    return [
        'name' => $nameOnTicket,
        'number' => Cache::increment('ticket-number'),
    ];
}, 'atomic-ticket-number-increment');

Providing an Eloquent model as Atomic Lock Key

Alternatively, you may provide an instance of a saved Eloquent model to use as the Atomic Lock Key. This approach means the callback will be executed one at a time for every unique record in your database.

use App\Models\TicketDispenser;

$dispenser = TicketDispenser::find(Request::get('ticket-dispenser-id'));
$ticket = synchronized(fn () => $dispenser->nextTicket(), $dispenser);

About

A Laravel package that provides a `synchronized` function that uses atomic locks to prevent a critical section of code from running in parallel across multiple requests.

Resources

License

Stars

Watchers

Forks

Languages