Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Semaphore Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the Package:

    composer require symfony/semaphore
    

    For Laravel, ensure compatibility with your PHP version (v8.0+ for PHP 8.4+).

  2. 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');
    
  3. 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();
    }
    
  4. 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');
    });
    

Implementation Patterns

Core Workflows

  1. Basic Locking:

    $sem = $factory->create('unique_resource_key');
    $sem->acquire(10); // Block until acquired or timeout (10s)
    try {
        // Critical section
    } finally {
        $sem->release();
    }
    
  2. 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();
    }
    
  3. 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();
                }
            }
        }
    }
    
  4. 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();
            }
        }
    }
    

Integration Tips

  • 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();
    

Gotchas and Tips

Pitfalls

  1. Deadlocks:

    • Cause: Forgetting to call release() or exceptions before finally.
    • Fix: Always use try/finally and set timeouts:
      $sem->acquire(30); // Timeout after 30s
      
  2. Redis Connection Issues:

    • Cause: Network splits or misconfigured DSNs.
    • Fix: Use sentinel/cluster modes (v7.2.4+) or retry logic:
      try {
          $sem->acquire(30);
      } catch (\RedisException $e) {
          Log::error('Redis lock failed, retrying...');
          sleep(1);
          retry();
      }
      
  3. Key Collisions:

    • Cause: Reusing keys across unrelated resources.
    • Fix: Use namespaced keys (e.g., tenant:{id}:resource).
  4. Performance Overhead:

    • Cause: Redis round-trips add ~10–50ms latency.
    • Fix: Cache semaphores locally for short-lived locks (e.g., in-memory for CLI jobs).

Debugging

  • Check Redis:
    redis-cli keys *  # List all semaphore keys
    redis-cli get semaphore:critical_section_key
    
  • Laravel Logs: Log acquisition failures:
    if (!$sem->acquire(30)) {
        Log::warning('Failed to acquire lock for critical_section_key');
    }
    

Extension Points

  1. Custom Backends: Implement SemaphoreInterface for specialized storage (e.g., DynamoDB):

    class DynamoSemaphore implements SemaphoreInterface
    {
        public function acquire(int $timeout): bool { /* ... */ }
        public function release(): void { /* ... */ }
    }
    
  2. Event Listeners: Extend the factory to log/notify on lock events:

    $factory->create('key')->acquire(30);
    event(new SemaphoreAcquired($factory, 'key'));
    
  3. Laravel Events: Dispatch events for monitoring:

    use Illuminate\Support\Facades\Events;
    
    if ($sem->acquire(30)) {
        Events::dispatch('semaphore.acquired', ['key' => 'critical_section']);
        // ...
    }
    

Configuration Quirks

  • 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).

Best Practices

  1. Key Naming: Use reverse-DNS style (e.g., app.tenant123.report_generation).

  2. Timeouts: Set timeouts based on critical section duration (e.g., 30s for API calls).

  3. Testing: Use SemaphoreInterface mocks to avoid flaky tests:

    $this->mock(SemaphoreInterface::class)->shouldReceive('acquire')->andReturnTrue();
    
  4. Monitoring: Track lock contention via Redis metrics or custom logging:

    if (!$sem->acquire(30)) {
        Log::warning('Lock contention detected for key: ' . $sem->getName());
    }
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport