symfony/semaphore
Symfony Semaphore Component provides a simple API to manage semaphores and locks, enabling exclusive access to shared resources across processes. Useful for coordinating concurrent jobs, preventing race conditions, and protecting critical sections.
Install the Package:
composer require symfony/semaphore
For Laravel, ensure compatibility with your PHP version (v8.0+ for PHP 8.4+).
Configure a Semaphore Factory: Use Redis (recommended) or Doctrine DBAL. Example for Redis:
use Symfony\Component\Semaphore\RedisFactory;
use Symfony\Component\Semaphore\SemaphoreInterface;
$factory = new RedisFactory('redis://localhost');
First Use Case: Protect a Critical Section
$semaphore = $factory->create('critical_section_key');
if ($semaphore->acquire(30)) { // 30-second timeout
// Execute critical code (e.g., update shared resource)
$semaphore->release();
}
Laravel Integration:
Bind the factory to the container in config/app.php or a service provider:
$app->singleton(SemaphoreInterface::class, function ($app) {
return (new RedisFactory(config('cache.default')))->create('laravel_semaphore');
});
Basic Locking:
$sem = $factory->create('unique_resource_key');
$sem->acquire(10); // Block until acquired or timeout (10s)
try {
// Critical section
} finally {
$sem->release();
}
Concurrency Limits: Limit concurrent executions (e.g., API calls per tenant):
$sem = $factory->create('tenant:123:api_calls', 3); // Max 3 concurrent
if ($sem->acquire(30)) {
// Execute API call
$sem->release();
}
Queue Job Protection: Wrap Horizon jobs to prevent duplicates:
class ProcessPayment implements ShouldQueue
{
public function handle()
{
$sem = app(SemaphoreInterface::class);
if ($sem->acquire(60)) {
try {
// Payment logic
} finally {
$sem->release();
}
}
}
}
CLI Command Locking: Ensure only one instance runs at a time:
class GenerateReportCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output)
{
$sem = app(SemaphoreInterface::class);
if (!$sem->acquire(300)) { // 5-minute timeout
$output->writeln('Report already running.');
return 1;
}
try {
// Generate report
} finally {
$sem->release();
}
}
}
Laravel Cache Integration:
Use Symfony’s CacheFactory for filesystem/APCu backends:
use Symfony\Component\Semaphore\CacheFactory;
$factory = new CacheFactory(app('cache.store'));
Redis Cluster/Sentinel:
Use DSNs like redis+cluster:// (v7.2.4+):
$factory = new RedisFactory('redis+cluster://user:pass@host:6379');
Doctrine DBAL: For legacy systems:
use Symfony\Component\Semaphore\DoctrineDbalFactory;
$factory = new DoctrineDbalFactory(app('db.connection'));
Testing: Mock the factory in unit tests:
$this->mock(SemaphoreInterface::class)->shouldReceive('acquire')->andReturnTrue();
Deadlocks:
release() or exceptions before finally.try/finally and set timeouts:
$sem->acquire(30); // Timeout after 30s
Redis Connection Issues:
try {
$sem->acquire(30);
} catch (\RedisException $e) {
Log::error('Redis lock failed, retrying...');
sleep(1);
retry();
}
Key Collisions:
tenant:{id}:resource).Performance Overhead:
redis-cli keys * # List all semaphore keys
redis-cli get semaphore:critical_section_key
if (!$sem->acquire(30)) {
Log::warning('Failed to acquire lock for critical_section_key');
}
Custom Backends:
Implement SemaphoreInterface for specialized storage (e.g., DynamoDB):
class DynamoSemaphore implements SemaphoreInterface
{
public function acquire(int $timeout): bool { /* ... */ }
public function release(): void { /* ... */ }
}
Event Listeners: Extend the factory to log/notify on lock events:
$factory->create('key')->acquire(30);
event(new SemaphoreAcquired($factory, 'key'));
Laravel Events: Dispatch events for monitoring:
use Illuminate\Support\Facades\Events;
if ($sem->acquire(30)) {
Events::dispatch('semaphore.acquired', ['key' => 'critical_section']);
// ...
}
Redis TTL:
Semaphores auto-expire on release. For persistent locks, use RedisFactory with custom TTL:
$factory = new RedisFactory('redis://localhost', 0); // No auto-expiry
Doctrine DBAL:
Ensure your DB supports SELECT ... FOR UPDATE (PostgreSQL/MySQL).
PHP 8.4+: Use v8.0+ for PHP 8.4 compatibility (e.g., named arguments).
Key Naming:
Use reverse-DNS style (e.g., app.tenant123.report_generation).
Timeouts: Set timeouts based on critical section duration (e.g., 30s for API calls).
Testing:
Use SemaphoreInterface mocks to avoid flaky tests:
$this->mock(SemaphoreInterface::class)->shouldReceive('acquire')->andReturnTrue();
Monitoring: Track lock contention via Redis metrics or custom logging:
if (!$sem->acquire(30)) {
Log::warning('Lock contention detected for key: ' . $sem->getName());
}
How can I help you explore Laravel packages today?