symfony/semaphore
Symfony Semaphore Component provides semaphores for coordinating access to shared resources across processes and threads. Use it to enforce mutual exclusion, limit concurrency, and prevent race conditions via an easy, reusable API.
Install the Package:
composer require symfony/semaphore
For Laravel, the facade is pre-installed (Illuminate\Contracts\Bus\QueueingDispatcher uses it under the hood).
Basic Usage (Filesystem Backend):
use Symfony\Component\Semaphore\Semaphore;
$semaphore = new Semaphore('/path/to/lockfile');
$semaphore->acquire(); // Blocks until lock is acquired
try {
// Critical section
} finally {
$semaphore->release(); // Always release!
}
First Use Case:
use Symfony\Component\Semaphore\Semaphore;
$semaphore = new Semaphore(storage_path('app/locks/backup.lock'));
if ($semaphore->acquire(30)) { // Timeout: 30 seconds
// Run backup logic
$semaphore->release();
} else {
throw new RuntimeException('Backup already in progress');
}
app('semaphore') or Illuminate\Bus\QueueingDispatcher for built-in queue locking.RedisSemaphore (see source).Short-Lived Locks (e.g., Queue Jobs):
$semaphore = app('semaphore');
$semaphore->acquire('job:process_payment', 10); // 10-second timeout
try {
// Process payment
} finally {
$semaphore->release('job:process_payment');
}
Long-Lived Locks (e.g., Maintenance Mode):
$semaphore = new Semaphore(storage_path('app/locks/maintenance.lock'));
if ($semaphore->acquire(0, 3600)) { // No timeout, 1-hour lease
// Enable maintenance mode
$semaphore->release(3600); // Auto-release after 1 hour
}
Redis-Backed Distributed Locks:
use Symfony\Component\Semaphore\RedisSemaphore;
$redis = new Redis();
$redis->connect('redis://localhost');
$semaphore = new RedisSemaphore($redis, 'app_');
$semaphore->acquire('distributed_task', 5);
Laravel Queues:
Use the built-in Semaphore via Queue::later() or Bus::dispatch() with Semaphore middleware.
Example:
$job = new ProcessPaymentJob($user);
Bus::dispatch($job)->onQueue('high')->withSemaphore('payment_processing');
Database Transactions + Locks: Combine with DB transactions for atomicity:
DB::transaction(function () use ($semaphore) {
$semaphore->acquire('inventory_update');
try {
// Update inventory
} finally {
$semaphore->release('inventory_update');
}
});
Custom Backends:
Implement SemaphoreInterface for specialized storage (e.g., Memcached):
class MemcachedSemaphore implements SemaphoreInterface {
// ...
}
Graceful Degradation: Fallback to a no-op semaphore in tests:
$semaphore = new class implements SemaphoreInterface {
public function acquire($name, $timeout = 0, $lease = 0) { return true; }
public function release($name) {}
};
Forgetting to Release:
finally blocks or context managers (e.g., Laravel’s Semaphore facade handles this automatically).try-finally or use a custom SemaphoreContext class.Filesystem Locks in Distributed Systems:
$semaphore = new RedisSemaphore($redis, 'app_');
Deadlocks:
Stale Locks:
$semaphore->acquire('task', 0, 300); // Auto-release after 5 minutes
Timeout Misuse:
acquire($name, $timeout) blocks for $timeout seconds. Use 0 for indefinite waits.acquire($name, 0, $lease) for long-running tasks with auto-release./path/to/lockfile for stale locks (delete manually if needed).redis-cli to inspect keys:
redis-cli keys app_*
$semaphore->acquire('task');
logger()->info('Acquired lock', ['name' => 'task']);
Custom Backends:
Extend AbstractSemaphore for new storage (e.g., DynamoDB):
class DynamoDBSemaphore extends AbstractSemaphore {
protected function doAcquire($name, $timeout, $lease) { /* ... */ }
protected function doRelease($name) { /* ... */ }
}
Event Listeners: Hook into lock events (e.g., notify Slack on contention):
$semaphore->addListener(function ($event) {
if ($event->isAcquired()) {
// Log or alert
}
});
Laravel Service Provider: Bind a custom semaphore to the container:
$this->app->bind('semaphore', function () {
return new RedisSemaphore($redis, 'app_');
});
redis-cli --latency for lock contention.Queue::later() uses semaphores internally. For custom queues:
$connection->push('queue', $job, ['semaphore' => 'custom_lock']);
if (!$semaphore->acquire('command:backup')) {
$this->error('Command already running.');
return 1;
}
How can I help you explore Laravel packages today?