Download the PHP package siberfx/laravel-mutex-lock without Composer
On this page you can find all versions of the php package siberfx/laravel-mutex-lock. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download siberfx/laravel-mutex-lock
More information about siberfx/laravel-mutex-lock
Files in siberfx/laravel-mutex-lock
Package laravel-mutex-lock
Short Description Laravel integration for php-lock/lock: database-backed mutexes for MySQL/MariaDB, PostgreSQL and SQLite.
License MIT
Informations about the package laravel-mutex-lock
Table of contents
- Why
- Supported engines
- Requirements
- Installation
- Quick start
- Usage
- Running code under a lock
- Timeouts
- Handling a busy lock
- Double-checked locking
- Choosing a connection
- Dependency injection
- Working with the raw php-lock mutex
- Queue job middleware
- Recipes
- Preventing double spending
- Idempotent webhooks
- Inventory reservation
- Artisan commands that must not overlap
- Cache stampede protection
- Lazy one-time initialisation
- Per-tenant locks
- Configuration
- Exceptions
- API reference
- How it works
- Caveats
- Mutex vs.
Cache::lock() - Testing your code
- Running the package tests
- Changelog
- Credits
- License
Why
Race conditions don't show up in development. They show up in production, when two requests or two workers read the same row at the same moment. This package gives you a simple, explicit way to prevent that:
- No extra infrastructure. Locks use the MySQL, MariaDB, PostgreSQL or SQLite database your app already uses.
- Crash-safe. On MySQL, MariaDB and PostgreSQL, the server releases the lock when the process dies. On SQLite, locks expire.
- Proven core. The locking logic comes from php-lock/lock. This package adds a Laravel-native API, config, a facade, queue middleware and a SQLite driver.
- Modern stack. PHP 8.4+, Laravel 12 and 13, strict types.
Supported engines
| Engine | Mechanism | Driver | Released on crash | Re-entrant* |
|---|---|---|---|---|
| MySQL 5.7+ / 8+ | GET_LOCK() / RELEASE_LOCK() |
Malkusch\Lock\Mutex\MySQLMutex |
✅ When the session closes | ✅ |
| MariaDB 10+ | GET_LOCK() / RELEASE_LOCK() |
Malkusch\Lock\Mutex\MySQLMutex |
✅ When the session closes | ✅ |
| PostgreSQL 12+ | pg_advisory_lock() / pg_try_advisory_lock() |
Malkusch\Lock\Mutex\PostgreSQLMutex |
✅ When the session closes | ✅ |
| SQLite 3.24+ | Row in a lock table (atomic UPSERT) | Siberfx\MutexLock\Mutex\SqliteMutex |
⏱ After sqlite.expire seconds |
❌ |
* Re-entrant means a nested lock with the same name on the same connection won't block itself.
Requirements
- PHP 8.4 or 8.5
- Laravel 12.x or 13.x
- One of
pdo_mysql,pdo_pgsqlorpdo_sqlite
Installation
The service provider and the Mutex facade are auto-discovered.
To publish the config file (optional):
Quick start
The lock is always released, whether the callback returns normally or throws.
Usage
Running code under a lock
synchronized() waits for the lock, runs your callback, releases the lock and returns the callback's result.
Exceptions thrown inside the callback are rethrown unchanged, after the lock is released:
Timeouts
The timeout is how long to wait for the lock, in seconds. It does not limit how long your callback may run.
[!NOTE] MySQL and MariaDB round
GET_LOCK()timeouts up to whole seconds. PostgreSQL and SQLite support fractional timeouts.
Handling a busy lock
If the lock can't be acquired in time, Malkusch\Lock\Exception\LockAcquireTimeoutException is thrown. Your callback does not run.
A small helper for "skip if busy" semantics:
Double-checked locking
Double-checked locking avoids taking a lock when there's nothing to do. The check runs before the lock is acquired and again after. The callback runs only when both checks pass.
Without the second callback, then() returns false when a check fails.
Choosing a connection
By default, locks use mutex-lock.connection, or your default database connection if that's not set. You can override it per call:
[!IMPORTANT] Every process that competes for the same lock must use the same database server.
Dependency injection
The facade proxies Siberfx\MutexLock\MutexManager, a container singleton. You can inject it instead:
It's also available as app('mutex-lock').
Working with the raw php-lock mutex
Mutex::make() returns the underlying Malkusch\Lock\Mutex\Mutex. Use it to pass a mutex into code that is framework-agnostic:
Queue job middleware
WithMutex wraps a job's handle() in a lock. By default it tries once. If the lock is busy, the job goes back on the queue.
| Option | Behaviour |
|---|---|
new WithMutex($name) |
Try once and release the job immediately if busy |
new WithMutex($name, timeout: 5) |
Wait up to 5 seconds for the lock before giving up |
new WithMutex($name, connection: 'pgsql') |
Lock on a specific connection |
->releaseAfter(30) |
If busy, release the job back onto the queue with a 30 second delay |
->dontRelease() |
If busy, throw LockAcquireTimeoutException (the attempt fails) |
[!TIP] Each release counts as an attempt, so set
$tries(orretryUntil()) high enough for your contention level.
Recipes
Preventing double spending
Idempotent webhooks
Payment providers retry webhooks, sometimes several times in parallel. Lock on the event ID and check whether the event was already processed:
Inventory reservation
Artisan commands that must not overlap
This works across servers even without a shared cache:
Cache stampede protection
When a hot cache key expires, only one process rebuilds it. The others wait, then read the fresh value:
Lazy one-time initialisation
Per-tenant locks
Use the prefix option when several apps share one database server:
Or build names that include the tenant:
Configuration
config/mutex-lock.php:
| Key | Env | Default | Description |
|---|---|---|---|
connection |
MUTEX_CONNECTION |
null |
Connection to lock on (null = default connection) |
timeout |
MUTEX_TIMEOUT |
10 |
Seconds to wait for a lock (0 = try once, null = forever) |
prefix |
MUTEX_PREFIX |
'' |
Prepended to every lock name |
sqlite.table |
MUTEX_SQLITE_TABLE |
mutex_locks |
Lock table name |
sqlite.expire |
MUTEX_SQLITE_EXPIRE |
300 |
Seconds after which a lock left by a crashed process can be reclaimed (null = never) |
sqlite.auto_create_table |
MUTEX_SQLITE_AUTO_CREATE_TABLE |
true |
Create the lock table on first use |
Managing the SQLite table with migrations
If you prefer migrations over auto-creation:
MySQL, MariaDB and PostgreSQL don't need a table.
Exceptions
All exceptions come from php-lock and extend Malkusch\Lock\Exception\MutexException (a RuntimeException).
When releasing the lock fails, LockReleaseException still gives you the outcome of your callback:
API reference
| Method | Returns | Description |
|---|---|---|
Mutex::synchronized(string $name, callable $callback, float\|int\|null $timeout = null, ?string $connection = null) |
mixed |
Acquire, run $callback, release and return its result |
Mutex::check(string $name, callable $check, float\|int\|null $timeout = null, ?string $connection = null) |
DoubleCheckedLocking |
Start a double-checked lock. Call ->then($success, ?$fail) on the result |
Mutex::make(string $name, float\|int\|null $timeout = null, ?string $connection = null) |
Malkusch\Lock\Mutex\Mutex |
Build the underlying php-lock mutex |
$timeout: null uses the configured default, 0 tries once, INF waits forever, and any other value is a number of seconds.
How it works
MySQL / MariaDB. SELECT GET_LOCK(name, timeout) takes a named lock that belongs to the database session. RELEASE_LOCK(name) frees it. If the PHP process dies, the connection closes and the server frees the lock. Names are prefixed with php-malkusch-lock:. Names longer than 46 characters are hashed (sha1:…) to stay within MySQL's 64-character limit.
PostgreSQL. The lock name is hashed into two 32-bit keys for pg_advisory_lock(k1, k2). If the timeout is infinite, it blocks on the server. Otherwise it polls pg_try_advisory_lock with exponential backoff until the timeout runs out. Like MySQL, the lock is released when the session ends.
SQLite. SQLite has no advisory locks, so each lock is a row (name, token, expires_at) in the mutex_locks table. A single atomic UPSERT acquires the lock. It succeeds only if the row doesn't exist or has expired. Release deletes the row with the acquiring token, so a process can never release someone else's lock. If the database is busy, the attempt counts as "not acquired yet" and is retried with backoff.
Caveats
- Take locks outside transactions. Wrap
DB::transaction()insidesynchronized(), not the other way around. On SQLite, a lock row written inside an uncommitted transaction is invisible to other connections. - Contention requires separate sessions. Laravel opens one connection per PHP process, so different requests and workers contend correctly. Within the same process and connection, MySQL/MariaDB and PostgreSQL locks are re-entrant: nesting the same name won't block. On SQLite, nesting the same name waits for itself and times out.
- Reconnects drop server-side locks. If the database connection is lost while a MySQL or PostgreSQL lock is held, the server releases the lock. Keep critical sections short. Don't call
DB::reconnect()inside them. - SQLite expiry. If a callback runs longer than
sqlite.expire, another process may take the lock. You'll get anExecutionOutsideLockExceptionon release. Setexpirecomfortably above your longest critical section. - Timeout of 0 on PostgreSQL/SQLite. php-lock's spin loop doesn't run with a timeout of exactly 0, so the package uses a 10 ms window. This gives the same "try once" behaviour.
- Lock order. When a callback needs several locks, always acquire them in the same order (for example, sorted by ID) to avoid deadlocks.
Mutex vs. Cache::lock()
Laravel ships atomic locks through the cache. Both are good tools. Pick based on your infrastructure and failure model:
Mutex (this package) |
Cache::lock() |
|
|---|---|---|
| Backend | Your SQL database | Redis, Memcached, DynamoDB, database, … |
| Released if the process dies | Immediately (MySQL/PG), on expiry (SQLite) | On TTL expiry |
| TTL you must estimate | No (MySQL/PG) | Yes |
| Blocking wait | Server-side (MySQL, PG with INF) |
Polling |
| Double-checked locking | Built in | Manual |
Testing your code
The facade can be mocked like any Laravel facade. To just run the callback in tests:
To simulate a busy lock:
The real implementation also works in tests on an in-memory SQLite database. No mocking is needed when you only care about the callback's effects.
Running the package tests
DB_HOST, DB_PORT and DB_PASSWORD are also read. CI runs the full matrix: PHP 8.4/8.5 × Laravel 12/13 × SQLite/MySQL/MariaDB/PostgreSQL × lowest/latest dependencies.
Changelog
See CHANGELOG.md.
Credits
- Selim Görmüş
- php-lock/lock by Markus Malkusch, Willem Stuursma-Ruwen, Michael Voříšek and contributors
License
The MIT License (MIT). See LICENSE.
All versions of laravel-mutex-lock with dependencies
ext-pdo Version *
illuminate/contracts Version ^12.0 || ^13.0
illuminate/database Version ^12.0 || ^13.0
illuminate/support Version ^12.0 || ^13.0
malkusch/lock Version ^3.0