Weave 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

Lock Laravel Package

symfony/lock

Symfony Lock provides a simple, reliable way to create and manage locks, giving exclusive access to shared resources across processes and servers. It supports multiple stores like files, Redis, and databases, with blocking and non-blocking lock modes.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the package:

    composer require symfony/lock
    

    Pin a patched version. This assessment targets the 8.1.x line; Symfony also backports security fixes to the 5.4 LTS branch. The v5.4.54 release is a security patch (see Gotchas → Redis Lock Tokens). If you are constrained to 5.4, require ^5.4.54 or newer:

    composer require "symfony/lock:^5.4.54"
    

    Verify what you actually have before debugging lock behaviour:

    composer show symfony/lock
    
  2. Configure a store (e.g., Redis):

    use Symfony\Component\Lock\Store\RedisStore;
    use Symfony\Component\Lock\LockFactory;
    
    $store = new RedisStore(new \Redis());
    $factory = new LockFactory($store);
    
  3. First use case: Protect a critical section in a job or command:

    use Symfony\Component\Lock\LockFactory;
    
    public function handle()
    {
        $lock = $this->lockFactory->createLock('inventory_update_' . $this->orderId, 30);
        $acquired = $lock->acquire(true); // Block until acquired
    
        if ($acquired) {
            try {
                // Critical section: update inventory
                Inventory::decrement($this->productId);
            } finally {
                $lock->release(); // Release when done
            }
        }
    }
    
  4. Laravel integration: Bind the factory in AppServiceProvider:

    public function register()
    {
        $this->app->singleton(LockFactory::class, function ($app) {
            $store = new RedisStore($app['redis']);
            return new LockFactory($store);
        });
    }
    

    Use it in controllers/jobs:

    $lock = app(LockFactory::class)->createLock('critical_section', 10);
    if (! $lock->acquire()) { /* already running */ }
    

    There is no first-party Laravel Lock facade; if you want a facade-like helper, alias the factory yourself:

    'aliases' => [
        'Lock' => Symfony\Component\Lock\LockFactory::class,
    ],
    

Implementation Patterns

Core Workflows

1. Job/Queue Protection

  • Pattern: Wrap job execution in a lock to prevent duplicates.
public function handle(LockFactory $locks)
{
    $lock = $locks->createLock('job_' . $this->data['id'], 60);
    if ($lock->acquire()) {
        try {
            $this->processData();
        } finally {
            $lock->release();
        }
    }
}
  • Laravel Queue Tip: Use dispatchSync() with locks for fire-and-forget jobs requiring idempotency.

2. Database Transaction Coordination

  • Pattern: Combine locks with transactions for atomicity.
$lock = $locks->createLock('account_' . $accountId, 30);
$lock->acquire(true);

try {
    DB::transaction(function () use ($accountId) {
        // Transaction logic
    });
} finally {
    $lock->release();
}
  • PostgreSQL Note: Use PdoStore with LOCK TABLE for heavy DB workloads (see Gotchas).

3. Distributed Cache Synchronization

  • Pattern: Use Redis for low-latency locks across services.
$store = new RedisStore($redisClient, 'locks_');
$factory = new LockFactory($store);
  • TTL Strategy: Set TTLs based on expected job duration (e.g., 30 seconds for short jobs).

4. Command-Line Safety

  • Pattern: Protect Artisan commands from concurrent runs.
public function handle(LockFactory $locks)
{
    $lock = $locks->createLock('command_' . $this->getName(), 3600);
    if (!$lock->acquire()) {
        throw new \RuntimeException('Command already running.');
    }

    try {
        // command body
    } finally {
        $lock->release();
    }
}

Integration Tips

Laravel-Specific

  • Service Container: Bind stores dynamically:

    $this->app->bind(RedisStore::class, function ($app) {
        return new RedisStore($app['redis'], 'laravel_locks_');
    });
    
  • Lock Factory: Extend for custom logic:

    class AppLockFactory extends LockFactory
    {
        public function createNamedLock(string $name, int $ttl = 30): LockInterface
        {
            return $this->createLock("app_{$name}", $ttl);
        }
    }
    

Multi-Store Strategies

  • Fallback Chain: Combine stores for resilience (implement a small composite PersistingStoreInterface/StoreInterface that tries each backend in order, or run primary/secondary explicitly):

    $primary  = new RedisStore($redis);
    $fallback = new FlockStore('/tmp/locks');
    
    try {
        $lock = (new LockFactory($primary))->createLock('report_' . $id, 60);
        $acquired = $lock->acquire();
    } catch (LockAcquiringException|LockProviderNotFoundException $e) {
        $lock = (new LockFactory($fallback))->createLock('report_' . $id, 60);
        $acquired = $lock->acquire();
    }
    
  • Store-Specific Keys: Prefix keys to avoid collisions:

    $store = new RedisStore($redis, 'myapp_');
    

Event Listeners

  • Lock in Events: Use locks for idempotent listeners:
    public function handle(OrderPlaced $event, LockFactory $locks)
    {
        $lock = $locks->createLock('order_' . $event->order->id, 120);
        if ($lock->acquire()) {
            try {
                // Process once
            } finally {
                $lock->release();
            }
        }
    }
    

Gotchas and Tips

Pitfalls

  1. Lock Leaks

    • Issue: Unreleased locks block resources indefinitely (e.g., crashes, timeouts).
    • Fix: Use try-finally:
      $lock = $locks->createLock('resource', 60);
      try {
          if ($lock->acquire()) {
              // Work
          }
      } finally {
          if ($lock->isAcquired()) {
              $lock->release();
          }
      }
      
    • Laravel Tip: Always release in finally, and keep the critical section shorter than the TTL.
  2. Redis Lock Tokens — Upgrade Past v5.4.54 / Current Patch Level (security release)

    • Issue: Symfony can store the lock token in Redis under a member named __write__. Prior to the fix (5.4.54 on the LTS branch, with the equivalent fix on the current line), that reserved member could be picked up as if it were a lock token, meaning token parsing could be influenced by the store's own bookkeeping rather than the actual lock owner. The changelog entry is:

      Never take the reserved "__write__" member as a Redis lock token

    • Fix: Run a patched version.
      composer require "symfony/lock:^5.4.54"   # LTS branch (backport)
      composer show symfony/lock               # verify what is installed
      
      Treat this as a minimum-version floor across all environments (local, CI, prod) — a stale composer.lock on one worker is enough to keep the exposure alive.
    • Practical implications:
      • Never hand-roll lock token storage, comparison, or parsing against the Redis members directly. Use Key/LockInterface and let the store own the format.
      • Any custom StoreInterface or InstrumentedStore decorator that enumerates Redis members must skip reserved/internal members rather than assuming every member is a lock name.
      • Do not share a Redis namespace/database with software that writes arbitrary members into your lock prefix — prefix your keys (RedisStore($redis, 'myapp_')) so bookkeeping and lock names cannot collide.
  3. Only the owning key can release a lock (clarified in the v8.1.8 docs)

    • Issue: A lock is owned by the LockKey that acquired it. release() on a different instance/process — or on an instance whose lock already expired and was re-acquired by someone else — will not free the current holder's lock. Stores that track ownership tokens (Redis, PDO, etc.) throw Symfony\Component\Lock\Exception\LockReleasingException rather than deleting a lock they no longer own.
    • Practical implications:
      • Never construct a new Lock object just to release something acquired elsewhere — pass the same LockInterface instance (or persist the LockKey's unique token deliberately).
      • Treat a late release() after TTL expiry as a no-op/exception, not as "unlocking the other guy".
      • Guard with isAcquired() and isExpired() before releasing:
        if ($lock->isAcquired() && ! $lock->isExpired()) {
            $lock->release();
        }
        
      • If you catch LockReleasingException, log it — it usually means your TTL was too short for the work being done.
  4. Version Branch Confusion

    • Issue: Symfony maintains both an LTS branch (5.4.x) and current lines (8.x). Security fixes land on current lines first and are backported to 5.4 — so "latest tag" in a changelog feed is not necessarily the line you run.
    • Fix: Pin the constraint to your branch and audit both. A ^5.4 constraint resolves forward to patched releases automatically; a hard-coded version does not.
  5. PostgreSQL Transaction Contention

    • Issue: PdoStore with LOCK TABLE can abort outer transactions (fixed in v8.0.9+).
    • Workaround: Use SELECT FOR UPDATE instead:
      $store = new PdoStore($pdo, 'locks', 'SELECT * FROM locks WHERE name = :name FOR UPDATE');
      
  6. Redis Cluster Quirks

    • Issue: Redis Cluster may fail with evalSha (fixed in v7.2.6+).
    • Fix: Ensure Redis 6.2+ or use RedisStore with useEval: false:
      $store = new RedisStore($redis, null, null, false);
      
  7. Key Normalization

    • Issue: Custom lock names may not serialize deterministically (e.g., objects, resources).
    • Fix: Normalize to a stable string before createLock():
      $name = 'user_' . $user->getKey() . '_' . md5(serialize($payload));
      $lock = $locks->createLock($name, 30);
      
  8. Windows File Locks

    • Issue: FlockStore may fail on network drives.
    • Fix: Use RedisStore or PdoStore for cross-platform reliability.

Debugging

  1. Lock Timeouts

    • Debug: Check TTL vs. actual execution time:
      $lock = $locks->createLock('resource', 10); // 10-second TTL
      if (!$lock->acquire(true, 5)) { // Wait 5s
          throw new \RuntimeException('Lock acquisition timed out.');
      }
      
  2. Redis Errors

    • Debug: Enable Redis logging and sane timeouts:
      $redis = new \Redis();
      $redis->connect('127.0.0.1', 6379, 0, null, 5.0, 2000, \Redis::OPT_SERIALIZER, \Redis::SERIALIZER_IGBINARY);
      $store = new RedisStore($redis);
      
  3. Suspicious Redis Lock Contents

    • Debug: Inspect the raw members under your lock prefix when ownership looks wrong — you want to confirm the token belongs to your Key and that no stray bookkeeping member (such as __write__) is being mistaken for a lock:
      redis-cli --scan --pattern 'laravel_locks_*'
      redis-cli HGETALL laravel_locks_my_resource
      
    • Record the installed symfony/lock version alongside the dump; unpatched versions are the first suspect for token anomalies.
  4. PostgreSQL Locks

    • Debug: Monitor pg_locks:
      SELECT * FROM pg_locks WHERE relation::regclass = 'locks';
      
  5. Ownership/Release Failures

    • Debug: Catch and inspect the exception thrown on release:
      try {
          $lock->release();
      } catch (\Symfony\Component\Lock\Exception\LockReleasingException $e) {
          Log::warning('Lock not released', [
              'lock' => $lock->getKey() ?? null,
              'expired' => $lock->isExpired(),
              'error' => $e->getMessage(),
          ]);
      }
      
    • Correlate with TTL overruns: if this fires regularly, raise the TTL or split the work.

Extension Points

  1. Custom Stores

    • Implement the store interface for new backends (e.g., DynamoDB):
      class DynamoStore implements StoreInterface
      {
          public function save(Key $key) { /* ... */ }
          public function delete(Key $key) { /* ... */ }
          public function exists(Key $key): bool { /* ... */ }
          public function putOffExpiration(Key $key, $ttl) { /* ... */ }
      }
      
  2. Lock Strategies

    • Extend LockFactory for custom acquisition logic:
      class RetryLockFactory extends LockFactory
      {
          public function createLock(string $name, int $ttl = 30): LockInterface
          {
              return new RetryLock($this->store, $name, $ttl, 3); // Retry 3 times
          }
      }
      
  3. Observability

    • symfony/lock does not ship a listener API on the factory; wrap acquisition in a decorator or custom store to emit metrics/logs:
      class InstrumentedStore implements StoreInterface
      {
          public function __construct(private StoreInterface $inner) {}
      
          public function save(Key $key)
          {
              Log::info('lock.save', ['key' => $key->getKey()]);
              $this->inner->save($key);
          }
      
          // delegate delete(), exists(), putOffExpiration()
      }
      
    • Do not implement decorators by reading or rewriting the store's internal token representation — that is exactly the layer the __write__ fix tightened. Stay on the Key abstraction.
  4. Ownership Tokens

    • If you must release from another process (e.g., a web request cancelling a job), persist the Key/token yourself and rebuild the Key explicitly — and only if the store supports it. Otherwise prefer a dedicated "cancel" flag over cross-process unlocking.

Performance Tips

  1. TTL Tuning
    • Set TTLs based on p95 workload duration plus a safety margin; too short causes LockReleasingException on late release, too long stalls retries after a crash.
    • Prefer non-blocking acquire() + queue retry over long acquire(true, $timeout) calls that tie up workers.
    • Use putOffExpiration() for genuinely long jobs instead of setting a huge TTL up front.
    • Keep lock names short, stable and prefixed — they're keys in Redis/DB.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle