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

Ninja Mutex Laravel Package

arvenil/ninja-mutex

arvenil/ninja-mutex is a lightweight PHP mutex library for inter-process locking. It offers a simple API and pluggable adapters such as flock, MySQL, Redis, Memcached, and semaphores to protect critical sections and coordinate concurrent processes.

View on GitHub
Deep Wiki
Context7

Getting Started

Requires PHP 5.4+ era code; install with Composer:

composer require arvenil/ninja-mutex

First files to read, in order:

  1. NinjaMutex\Lock\LockInterface — the small contract every backend implements (acquire, release, isLocked).
  2. NinjaMutex\Mutex — the thin orchestration layer you will actually call.
  3. The Lock\*Lock implementations — pick one based on where your locking must hold.

The core pattern is always the same: construct a lock backend, wrap it in a Mutex with a stable name, and guard a critical section.

use NinjaMutex\Lock\FlockLock;
use NinjaMutex\Mutex;

$lock  = new FlockLock(storage_path('locks/reports.lock'));
$mutex = new Mutex('reports:nightly', $lock);

if (! $mutex->acquireLock(5)) {          // wait up to 5 seconds
    return;                              // someone else is running
}

try {
    $this->generateReports();
} finally {
    $mutex->releaseLock();
}

That cron single-instance guard is the classic first use case: "don't run this job twice concurrently." The first backend to reach for is FlockLock on a single host, or a Redis-backed lock if the job runs on more than one server.

Implementation Patterns

Backend selection is architectural, not stylistic. Choose by blast radius:

Topology Backend
Single host, local filesystem FlockLock, DirectoryLock, SemaphoreLock
Multiple app servers PHPRedisLock (phpredis), PredisRedisLock (Predis), MemcacheLock/MemcachedLock, MysqlLock
Shared DB, no cache tier MysqlLock (session-scoped, good crash safety)

Always try/finally. The library will not release a lock for you. Wrap every critical section; this is non-negotiable in long-running queue workers where an exception mid-job would otherwise wedge the lock.

A runExclusive helper pays for itself immediately. You will repeat this pattern everywhere in Laravel; centralize it:

class LockManager
{
    public function __construct(private LockInterface $lock, private LoggerInterface $logger) {}

    public function run(string $name, callable $callback, float $timeout = 5.0): mixed
    {
        $mutex = new Mutex($name, $this->lock, $this->logger);
        if (! $mutex->acquireLock($timeout)) {
            throw new LockUnavailable("Could not acquire {$name}");
        }
        try {
            return $callback();
        } finally {
            $mutex->releaseLock();
        }
    }
}

Bind LockInterface in a service provider so jobs/controllers resolve it from the container, and make the backend configurable per environment (flock locally, Redis in production).

Cache stampede protection. The classic second use case: read-through cache regeneration under load.

$value = Cache::get($key);
if ($value === null) {
    $mutex = new Mutex("cache:{$key}", $redisLock);
    if ($mutex->acquireLock(2)) {
        try {
            $value = Cache::get($key) ?? $this->regenerate($key);
        } finally {
            $mutex->releaseLock();
        }
    }
}

Note the re-check of the cache after acquiring — otherwise every waiter regenerates once the lock frees.

Namespace every lock name. Prefix with environment and resource (prod:invoice:1234:recalc). Lock names are global per backend; a staging worker and a production worker sharing one Redis DB will fight over invoice:1234.

Inject a PSR-3 logger as the third constructor argument. It is the only built-in observability hook and makes "who is holding this lock" questions answerable in logs.

Polling is built in. acquireLock($timeout) blocks and retries with a short sleep. Don't add your own retry loop around it; do choose a timeout that matches your request/latency budget and let the failure path be explicit (dispatch a job, return 503, etc.).

Gotchas and Tips

  • No TTL on Redis/Memcache locks. A process killed between acquireLock and releaseLock leaves the key behind, and the next run blocks until someone deletes it. For crash-prone workloads prefer MysqlLock (MySQL GET_LOCK is connection-scoped and auto-releases on disconnect) or FlockLock (the OS releases on process death). Add external TTL handling if you must use Redis.
  • flock is per-host and NFS-hostile. It coordinates processes on one machine against one file. It does not work across containers, autoscaled hosts, or unreliable network filesystems.
  • Don't double-acquire the same name on one Mutex instance. It throws; the mutex is not reentrant. For nested/recursive work, either pass the already-held lock down or use separate, distinctly named mutexes. Guard against reentry in your own helper if your code paths can nest.
  • Timeouts are seconds and blocking. acquireLock($t) waits up to $t seconds, polling. In an HTTP request keep it in the low single digits; a lock wait is user-facing latency. In CLI workers a long or null timeout is fine.
  • isLocked() is a snapshot, not a reservation. It's fine for dashboards and "is a job stuck?" checks; never use it as a check-then-act guard — the race is exactly what the mutex is for.
  • Sanitize lock names. Redis and Memcached key length/charset limits apply, and DirectoryLock/FlockLock-style backends may map names onto paths. Hash long or user-derived names (sha1($name)) rather than interpolating raw input.
  • Extension prerequisites. PHPRedisLock needs the redis extension and takes a connected client; PredisRedisLock takes a Predis\Client; SemaphoreLock needs sysvsem; MysqlLock needs a live PDO connection — and that connection must stay open for the duration of the lock.
  • It is unmaintained (last release 2016). Pin the version in composer.json, and be aware there are no fixes coming. For new greenfield work, weigh symfony/lock (active, TTLs, store abstraction) or malkusch/lock. Keep ninja-mutex where it already works and the semantics are understood.
  • Testing. Use FlockLock/DirectoryLock in tests and point them at a per-test temp path; clean up in tearDown. Avoid exercising a real Redis lock in unit tests unless the CI environment provides one — the failure mode (tests hanging on a timeout) is painful to debug.
  • Debugging a stuck lock. Check isLocked($name) first, then inspect the backend directly (GET lockname in Redis, IS_USED_LOCK in MySQL, lsof the lock file). If isLocked says free but your code still blocks, you are almost certainly using a different backend instance or a different lock name than you think.
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.
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
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle