Prevent Race Conditions with Winter Boot Distributed Locking
Winter Boot’s locking abstraction gives you declarative, AOP-based concurrency control over any public bean method. By annotating a method with #[Lockable] you guarantee that only one caller can execute that method at a time — either at the local-node level (default) or across an entire cluster when a distributed LockManager is provided. Parameter expressions such as name: "order-#{id}" allow the lock scope to be narrowed down to a specific resource instance, preventing unnecessary serialization of unrelated requests.
#[Lockable] Options
Section titled “#[Lockable] Options”-
namestring(required) Unique name for the lock. Supports#{param}interpolation to bind method parameters into the lock name (e.g."order-#{id}"). -
lockManagerstring(default:Framework default) Bean name of a class implementingLockManager. Omit to use local node locking. -
waitMilliSecsint(default:0) How long (in milliseconds) to wait for the lock before failing.0means fail immediately if the lock is not available. -
ttlSecondsint(default:0) Maximum time (in seconds) the lock is held before it is automatically released.0means no TTL limit.
Local Node Locking
Section titled “Local Node Locking”The framework provides local node locking out of the box — no extra configuration or bean is needed. Only one process on the same node can execute the annotated method at a time. The #{id} syntax binds the method parameter $id into the lock name, so two concurrent calls with different order IDs proceed in parallel while two calls for the same ID are serialized.
use dev\winterframework\stereotype\concurrent\Lockable;
#[Lockable(name: 'order-#{id}')]public function updateOrderStatus(int $id): void{ // Only one process per $id can run this at a time on this node.}use dev\winterframework\stereotype\concurrent\Lockable;
// Wait up to 3 seconds for the lock; TTL of 30 seconds prevents deadlocks.#[Lockable(name: 'order-#{id}', waitMilliSecs: 3000, ttlSeconds: 30)]public function updateOrderStatus(int $id): void{ // Waits up to 3 s to acquire the lock, then throws LockException.}Distributed Locking
Section titled “Distributed Locking”For deployments with multiple nodes you need a LockManager backed by a shared store so that locks are visible across the entire cluster. Provide a bean that implements LockManager, pass its bean name to the lockManager option, and the #[Lockable] attribute works identically to local locking from your code’s perspective.
Supported backends include Redis, database locks, Apache ZooKeeper, Consul, and anything else you can wrap in a LockManager implementation.
-
Annotate the method with a named lockManager
OrderService.php use dev\winterframework\stereotype\concurrent\Lockable;#[Lockable(name: 'order-#{id}', lockManager: 'redisLockManager')]public function updateOrderStatus(int $id): void{// No two processes across the cluster can run this for the same $id.} -
Register the LockManager bean
LockConfig.php use dev\winterframework\stereotype\Configuration;use dev\winterframework\stereotype\Bean;use dev\winterframework\util\concurrent\LockManager;#[Configuration]class LockConfig{#[Bean('redisLockManager')]public function getRedisLockManager(): LockManager{// Construct and return your LockManager implementation.// E.g. wrap a Symfony RedisStore inside an adapter.return new MyRedisLockManager();}}
Handling LockException
Section titled “Handling LockException”If the lock cannot be acquired within waitMilliSecs milliseconds, Winter Boot throws a LockException. Catch it in your controller or service layer and return an appropriate response to the caller.
use dev\winterframework\util\concurrent\LockException;
try { $this->orderService->updateOrderStatus($id);} catch (LockException $e) { // Lock could not be acquired — handle retry or conflict response return $this->response->conflict('Order is currently being updated. Please retry.');}Common LockException scenarios
| Scenario | Recommended response |
|---|---|
| High-throughput writes to the same resource | Return HTTP 409 Conflict and ask the client to retry with exponential back-off. |
| Background job overlap prevention | Log and skip the current execution; the next scheduled run will proceed normally. |
Deadlock prevention with ttlSeconds |
The lock auto-releases after the TTL; the next caller acquires it cleanly. |