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

Command Lock Bundle Laravel Package

ffreitas-br/command-lock-bundle

A Symfony bundle for locking console commands so scheduled jobs and long-running tasks don’t run concurrently. Prevent duplicate executions and race conditions by applying locks to individual commands with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: it's a Symfony bundle, not a Laravel package. ffreitas-br/command-lock-bundle targets Symfony 2.x (AppKernel, DI extension, console commands) and was last released in 2014. You cannot composer require it into a Laravel app and expect it to work — no service provider, no Illuminate\Console integration, and its Symfony dependencies will conflict with a modern Laravel install. Treat it as a reference implementation of "one command instance at a time" and plan to either (a) use it only inside a legacy Symfony 2 service, or (b) port the ~100 lines of locking logic into Laravel.

If you are on Symfony 2 and want to use it as-is:

  1. Inspect the repo before installing. There is no README worth the name. Look at DependencyInjection/, Command/, and any Lock/Util class to confirm the backend (almost certainly a filesystem lock, flock or a lockfile in var/ or /tmp).
  2. Add it to composer.json with an explicit constraint (or a VCS repository entry), since a 2014 release predates current Symfony semver rules.
  3. Register the bundle in app/AppKernel.php.
  4. Check php -v compatibility — the source is PHP 5.3/5.4-era and may use short array syntax or deprecated calls that break under PHP 8.
  5. First use case: cron-scheduled console commands. A nightly report, an import, or a mailer that occasionally runs long enough to overlap with the next tick. Wrap exactly one command first, then invoke it twice concurrently (php app/console acme:import & php app/console acme:import) and confirm the second exits immediately.

In Laravel, the equivalent first step is $schedule->command('acme:import')->withoutOverlapping(); — start there, and only reach for lower-level locks when you need them outside the scheduler.

Implementation Patterns

Guard at command entry, not mid-command. The bundle's model is acquire-at-start / release-at-end. Any pattern you port should do the same: the lock check must be the first thing in handle(), before any I/O, so a duplicate run costs milliseconds.

Pattern 1 — abstract base command (closest to the bundle's intent).

abstract class LockedCommand extends Command
{
    protected function execute(InputInterface $in, OutputInterface $out): int
    {
        $lock = $this->getContainer()->get('command_lock')->acquire($this->getName());
        if (!$lock) {
            $out->writeln('<comment>Already running, skipping.</comment>');
            return 0; // important: see Gotchas
        }
        try {
            return parent::execute($in, $out);
        } finally {
            $lock->release();
        }
    }
}

Pattern 2 — a console event listener. Register a console.command listener that acquires on start and releases on console.terminate / console.error. This is how you retrofit locking onto commands you don't own (third-party bundles, framework commands) without editing them. It's also the easiest thing to port to Laravel as a CommandStarting/CommandFinished event pair.

Pattern 3 — Laravel native equivalents. This is where most day-to-day leverage actually comes from:

  • Scheduler: ->withoutOverlapping(10) (minutes TTL) or ->onOneServer() for multi-node.
  • Ad-hoc: Cache::lock('import-orders', 600)->get(fn () => ...), or ->block(5, fn () => ...) when you'd rather wait than skip.
  • Queue jobs: ShouldBeUnique / WithoutOverlapping middleware, which is the modern answer to what this bundle did for console commands.

Name locks by command and arguments. acme:import --tenant=42 and acme:import --tenant=43 should be allowed to run together. Hash normalized input into the key: sha1($name . json_encode($args)). Lumping them under one key is the most common cause of "my second tenant's import never runs."

Pick TTLs from observed runtime, not guesses. For file locks released on process death, TTL is irrelevant. For TTL-based locks (Redis/DB), set expiry to roughly p99 runtime × 2. A 5-minute TTL on a job that occasionally takes 20 minutes silently permits overlap for the last 15.

Return 0 when skipping. A skipped duplicate is not a failure. Exiting non-zero trains your monitoring to ignore the command, and some cron mailers will page you every minute during a long run.

Log the skip once. A single Log::info('skipped, lock held', ['key' => $key]) is enough to explain gaps in a job's history later, and it's the first thing you'll want when someone asks why last night's import "didn't run."

Gotchas and Tips

Maintenance risk is the dominant gotcha. Five stars, zero dependents, last release June 2014. There is no one to fix a PHP 8 deprecation, no security process, and effectively no community for troubleshooting. Vendor it (copy the lock class into your repo, MIT permits it) rather than depending on the abandoned tag.

Filesystem locks don't cross hosts. flock and lockfiles coordinate only within a single machine. If you run the command on two app servers, on ECS tasks, or against a shared NFS mount (where flock semantics are notoriously unreliable), you have no mutual exclusion at all. Use Redis or the database for anything distributed — or onOneServer() in Laravel.

Stale lockfiles are the classic failure mode. Process-killed locks behave very differently by backend:

  • flock/LOCK_EX on an open handle: released by the kernel on process death. Safe.
  • A lockfile whose existence is the lock: survives kill -9, OOM, and reboots. The command silently stops running forever, and nobody notices until the data is a week stale. Always pair lockfiles with a PID check or a TTL.

Permissions bite on cron. Web-triggered runs write the lock as www-data; cron runs as deploy or root. Whichever directory holds the locks needs to be writable by both, or one path fails to acquire and the command runs anyway (or crashes). Check the lock directory in the bundle's config and make it explicit rather than the system temp dir.

Signals and long-running commands. Handle SIGTERM so a deploy or systemctl stop releases the lock cleanly. Without it, you're relying entirely on the backend's behavior at process death — and if the lock is refreshed by a heartbeat, a stuck process can hold it indefinitely.

Exit codes vs. monitoring. As above: skip = 0. Reserve non-zero for real errors, otherwise your alerting is noise and the lock will get ripped out during an incident.

Extension point: swap the lock backend. The only genuinely reusable thing here is the "acquire → run → release" contract. Define your own LockStore interface (file, Redis, DB) and keep the command wrapper generic. That way the stale-2014-bundle decision is isolated to one adapter class you can replace in an afternoon.

Don't use this for correctness-critical exclusion. It's an overlap-avoidance convenience, not a transactional guard. If two concurrent runs would corrupt data (ledger postings, inventory decrements), you need a database-level constraint or a proper distributed lock with fencing tokens — not a file on disk.

Debugging checklist when "the command isn't running": (1) is the lock key what you think it is — dump it including args; (2) does the lock file/handle still exist, and what's its mtime and owning PID; (3) can the running user write to the lock path; (4) is an earlier invocation actually still alive (ps aux | grep acme:), or did it die and leave debris.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
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