Skip to content

Instantly share code, notes, and snippets.

@Theodory
Forked from troatie/CreatesWithLock.php
Created November 22, 2022 07:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Theodory/95cef5284d3de13a0ee309a6a6282187 to your computer and use it in GitHub Desktop.
Save Theodory/95cef5284d3de13a0ee309a6a6282187 to your computer and use it in GitHub Desktop.
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment