Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Guard against race conditions in Laravel's firstOrCreate and updateOrCreate
trait CreatesWithLock
{
public static function updateOrCreate(array $attributes, array $values = [])
{
return static::advisoryLock(function () use ($attributes, $values) {
// emulate the code found in Illuminate\Database\Eloquent\Builder
return (new static)->newQuery()->updateOrCreate($attributes, $values);
});
}
public static function firstOrCreate(array $attributes, array $values = [])
{
return static::advisoryLock(function () use ($attributes, $values) {
return (new static)->newQuery()->firstOrCreate($attributes, $values);
});
}
/**
* In my project, this advisoryLock method actually lives as a function on the global namespace (similar to Laravel Helpers).
* In that case the $lockName, and default lock duration are pased in as arguments.
*/
private static function advisoryLock(callable $callback)
{
// Lock name based on Model.
$lockName = substr(static::class . ' *OrCreate lock', -64);
// Lock for at most 10 seconds. This is the MySQL >5.7.5 implementation.
// Older MySQL versions have some weird behavior with GET_LOCK().
// Other databases have a different implementation.
\DB::statement("SELECT GET_LOCK('" . $lockName . "', 10)");
$output = $callback();
\DB::statement("SELECT RELEASE_LOCK('" . $lockName . "')");
return $output;
}
}
@Richtermeister

This comment has been minimized.

Copy link

Richtermeister commented Jul 2, 2018

Big fan of this approach - glad to see you mention it over at https://murze.be/breaking-laravels-firstorcreate-using-race-conditions
Just a quick clarification for any passer-bys: The second parameter is not a timeout on the obtained lock, but a timeout on trying to obtain the lock. Once you hold the lock it is valid until you release it or connection closes. -1 is used to wait indefinitely, 0 just executes a check and moves on, much like a Redis lock would.

@williamjulianvicary

This comment has been minimized.

Copy link

williamjulianvicary commented Aug 20, 2018

Great approach to the problem, thank you.

@amenk

This comment has been minimized.

Copy link

amenk commented Oct 28, 2018

Is that a solution for laravel/framework#19372 ? Maybe make a pull request? @troatie

@francislavoie

This comment has been minimized.

Copy link

francislavoie commented Nov 8, 2018

@amenk problem is this is MySQL-specific. For Laravel to accept the PR, it would need to be DB agnostic.

@trevorgehman

This comment has been minimized.

Copy link

trevorgehman commented Mar 12, 2019

This is great, a big help.

@olivernybroe

This comment has been minimized.

Copy link

olivernybroe commented Apr 24, 2019

Nice solution, It however only solves it when on the model itself. In our use case we also call updateOrCreate on relationships.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.