Skip to content

Concurrency Limiting

Concurrency limiting prevents the same executable from running simultaneously. It uses Laravel's Cache::withoutOverlapping() under the hood, acquiring a lock before execute() runs and releasing it after. When the lock is already held, execution waits up to waitFor seconds to acquire it.

This works for both sync and queued execution.

Queue-only? Use middleware instead

Waiting for the lock blocks the worker process. For executables that only run on the queue, Laravel's WithoutOverlapping middleware should normally be a better fit since it releases the job back to the queue instead of occupying a worker. Use concurrency limiting when the executable may also run synchronously.

Attribute

For static lock keys, use the #[ConcurrencyLimit] attribute:

php
use Havn\Executable\Attributes\ConcurrencyLimit;

#[ConcurrencyLimit("process-orders")]
class ProcessOrder
{
    use Executable;

    public function execute(Order $order): void
    {
        /* ... */
    }
}

The attribute accepts the same options as Cache::withoutOverlapping(): key, lockFor, waitFor, and store.

php
#[ConcurrencyLimit(key: "process-orders", lockFor: 120, waitFor: 30, store: "redis")]
class ProcessOrder
{
    /* ... */
}

Method

When the lock key depends on execute-arguments, define a concurrencyLimit() method instead. It receives arguments by name, like other lifecycle methods:

php
class ProcessOrder
{
    use Executable;

    public function concurrencyLimit(Order $order): ConcurrencyLimit
    {
        return new ConcurrencyLimit(key: "process-order-{$order->id}");
    }

    public function execute(Order $order): void
    {
        /* ... */
    }
}

This locks per order instead of globally.

The method must return a ConcurrencyLimit instance. All the same options (lockFor, waitFor, store) are available as constructor arguments.

Priority

When both a concurrencyLimit() method and a #[ConcurrencyLimit] attribute are present, the method takes priority. The attribute is ignored.