roadrunner-php/lock
PHP integration for the RoadRunner Lock plugin, enabling fast, lightweight distributed lock management in high-traffic apps and microservices. Acquire, release, and manage locks with optional blocking waits via a simple RPC-based API. Requires PHP 8.1+.
Install via Composer: composer require roadrunner/lock
⚠️ Package renamed in 1.2.0. The old name
roadrunner-php/lockis superseded byroadrunner/lock. Existing projects must runcomposer remove roadrunner-php/lockandcomposer require roadrunner/lock. Update anyrequireentries and lockfile accordingly.
Ensure RoadRunner v3 is running with the lock plugin enabled. Version 1.2.0 requires RoadRunner v3 and uses v3's flat, versioned config layout (no services: wrapper, plugins are top-level):
version: '3'
rpc:
listen: tcp://127.0.0.1:6001
lock:
driver: redis
redis:
addr: "127.0.0.1:6379"
💡 The default driver is
memory(per-worker isolation). For true distributed locking across workers, useredisormemcached. If you are migrating from RRv2, the whole YAML needs theversion: '3'header and plugins promoted out ofservices:— the package won't talk to an RRv2 server.
First use case: Protect a shared resource (e.g. updating a counter) inside a long-running RoadRunner worker:
use RoadRunner\Lock\Lock;
$lock = app(Lock::class); // bound via DI/factory, see below
$key = 'update_counter';
$lock->lock($key);
try {
$counter = (int) file_get_contents('counter.txt');
file_put_contents('counter.txt', $counter + 1);
} finally {
$lock->unlock($key);
}
Use wrap() for atomic blocks:
$lock->wrap($key, fn() => $this->expensiveOperation());
Guarantees
unlock()runs even on exceptions — the safest default for critical sections.
Bind a singleton in a service provider so the client is shared and injectable:
// AppServiceProvider::register()
$this->app->singleton(\RoadRunner\Lock\Lock::class, function ($app) {
return new \RoadRunner\Lock\Lock(
$app->make(\RoadRunner\Lock\LockServiceInterface::class)
);
});
The
RoadRunner\Lock\*namespaces are consistent under the v3 SDK. Make sure you're resolving the v3 client interface — mixing v2 (Spiral\RoadRunner\...) and v3 (RoadRunner\...) classes in the same project will fail.
Per-operation key naming — namespace your keys so they're observable and collision-free:
$key = sprintf('order:process:%d', $orderId);
Non-blocking acquisition with a timeout via tryLock():
if ($lock->tryLock($key, $timeout = 1.5)) {
try {
// critical section
} finally {
$lock->unlock($key);
}
} else {
// busy — retry later or enqueue
}
Coordinating queued jobs across workers — guard against duplicate processing when the same job ID can be picked up concurrently:
if ($lock->tryLock("job:{$jobId}")) {
try {
// process once
} finally {
$lock->unlock("job:{$jobId}");
}
}
roadrunner-php/lock → roadrunner/lock) and RoadRunner v3 requirement (config format change). Verify both before rolling out.services: { ... } nesting, add version: '3' at the top, and confirm the rpc listener is still reachable from your worker. RRv3 changed several backend config key names — copy the example from the v3 docs rather than reusing v2 snippets.memory driver is worker-local — locks won't coordinate across separate worker processes. For real distributed guarantees, use Redis or Memcached.lock() inside another locked section without a consistent ordering. Prefer wrap() for exception-safe scoping.$logger->info('Acquired lock', ['key' => $key, 'worker' => getmypid()]);
connect_timeout / timeout in the Redis block of .rr.yaml to avoid hung workers when the backend is degraded.LockServiceInterface can be implemented in userland if you need a custom backend (DynamoDB, external lock HTTP service, etc.). Prefer doing this against the v3 interface.KEYS * in monitoring — use SCAN, INFO, or CLUSTER commands.How can I help you explore Laravel packages today?