phattarachai/ops-laravel
Operational tooling for Laravel apps: health checks, maintenance commands, and deployment helpers to keep production running smoothly. Lightweight and config-driven, it drops into any Laravel project with minimal setup and clear, sensible defaults.
phattarachai/ops-laravel is a small pre-1.0 (v0.1.x) operations helper package for Laravel. Based on the v0.1.2 changelog, its core surface area is lock and mutex lifetime management around Laravel's scheduler and cache lock primitives — i.e. the plumbing you reach for when scheduled work runs on more than one node and you need to guarantee that a crashed or hung process cannot leave a permanent lock behind.
Minimal setup:
composer require phattarachai/ops-laravel
Then, in order:
v0.1.2 the public API is still settling, so treat the repo's README and the src/ tree as the source of truth over any blog post or older example.php artisan vendor:publish --tag=ops-config or similar) before wiring anything into routes/console.php. Any TTL bounds introduced in this release will almost certainly be config-driven.redis, memcached, database, dynamodb, or file (Laravel 10+). The array driver silently gives you per-process locks, which will make your multi-node tests pass while production double-runs.First use case: take your most fragile scheduled command — the one that overlaps on deploy, or the one that got stuck behind a stale withoutOverlapping lock and stopped running for a week — and move it onto the package's mutex handling. That single migration is the fastest way to validate whether the package earns its place in your stack.
Bound every lock you create. The headline of v0.1.2 is that scheduler mutexes and flexible-cache locks now get bounded expiries. Lean into that: never create a lock without an explicit TTL, and set the TTL from a config value rather than a magic number in the command.
// config/ops.php (shape inferred — verify against the README)
return [
'locks' => [
'max_ttl' => env('OPS_LOCK_MAX_TTL', 900),
],
];
Pair with Laravel's native scheduler guards rather than replacing them. withoutOverlapping() and onOneServer() already use cache locks; the value this package adds is the bound. Keep the native calls and let the package clamp the expiry:
Schedule::command('reports:generate')
->dailyAt('02:00')
->onOneServer()
->withoutOverlapping(10); // minutes — must exceed worst-case runtime
Treat TTL as a function of runtime, not a guess. A good working rule: ttl = p95_runtime * 3, capped by the package's bound. If the bound in v0.1.2 is lower than your longest legitimate job, you have introduced a correctness bug — two overlapping runs — in exchange for faster recovery from stale locks. Measure first.
Wrap critical sections, not whole commands, where possible. Narrow locks mean shorter TTLs mean less exposure to the bound:
$lock = Cache::lock("invoice-batch:{$batch->id}", ttl: 120);
if (! $lock->get()) {
return; // another node owns this batch
}
try {
$batch->process();
} finally {
$lock->release();
}
Test the failure path explicitly. A lock library that is only tested on the happy path is untested. Write a test that acquires a lock, simulates abandonment, and asserts the next attempt succeeds after the bound — using Redis or database as the store, not array.
Integration tip: if you deploy multiple environments against a shared Redis, namespace your locks (APP_ENV prefix). Staging and production colliding on a mutex name is a genuinely confusing class of bug that looks like "the scheduler randomly didn't run."
v0.1.2 is a patch release, but in a 0.x package the maintainer is not obligated to preserve API compatibility on minors. Pin with composer require phattarachai/ops-laravel:0.1.* and read the diff of PR #3 before upgrading.withoutOverlapping() duration and every explicit Cache::lock() TTL against your actual job runtimes after upgrading. This is the single most likely way this release breaks you.->description() or the mutex name argument when using closures.redis-cli KEYS 'laravel_cache:*lock*', or the cache_locks table) before reaching for logs. Laravel's lock keys are opaque but the pattern is predictable. Cache::lock($name)->get() returning false tells you someone holds it; only the store tells you who and for how long.handle(). Avoid monkey-patching the package's internals — at 0.1.x they will move.! $lock->get() branch early; without it, "the cron didn't run" investigations start from zero every time.Verdict: a narrow, useful fix in a young package. Adopt for the bounded-mutex behavior specifically, pin the version, and re-check your TTLs against real runtimes on every upgrade.
How can I help you explore Laravel packages today?