Features
Distributed Locks
Coordinate work safely across processes and nodes
Prevent duplicate or overlapping execution of critical sections across workers or nodes with distributed locks.
Quick Start
Execute with Automatic Lock
use Glueful\Lock\LockManagerInterface;
$lockManager = app(LockManagerInterface::class);
// Execute once, safely across processes
$result = $lockManager->executeWithLock('import:daily-customers', function () {
return (new CustomerImporter())->run();
}, 900); // 15 min TTL
Wait for Lock (Blocking)
try {
$data = $lockManager->waitAndExecute('sync:warehouse', function () {
return runSync();
}, maxWait: 30.0, ttl: 600);
} catch (\Symfony\Component\Lock\Exception\LockConflictedException $e) {
// Timed out waiting
logger()->warning('Warehouse sync busy, skipped');
}
Manual Lock Control
$lock = $lockManager->createLock('rebuild-search-index', ttl: 300);
if ($lock->acquire()) {
try {
rebuildIndex();
} finally {
$lock->release();
}
} else {
logger()->info('Index rebuild already in progress');
}
Use Cases
Prevent Duplicate Job Execution
// In scheduled job
$lockManager->executeWithLock('daily:aggregation', function () {
AggregateJob::run();
}, 3600);
Singleton API Operations
public function rebuildAnalytics(): Response
{
try {
$this->lockManager->executeWithLock('analytics:rebuild', function () {
$this->service->rebuild();
}, 1800);
return Response::success('Rebuild complete');
} catch (\Symfony\Component\Lock\Exception\LockConflictedException) {
return Response::error('Rebuild already in progress', 409);
}
}
Batch Processing with Partition Locks
foreach (array_chunk($userIds, 500) as $chunk) {
$key = 'recalc:users:' . md5(json_encode($chunk));
$lockManager->executeWithLock($key, function () use ($chunk) {
recalcUserStats($chunk);
}, 300);
}
Conditional Execution
if ($lockManager->isLocked('cache:warm')) {
logger()->info('Cache warming already in progress');
return;
}
$lockManager->executeWithLock('cache:warm', function () {
warmCacheLayers();
}, 600);
Configuration
config/lock.php
:
return [
'default' => env('LOCK_DRIVER', 'file'),
'stores' => [
'file' => [
'driver' => 'file',
'path' => env('LOCK_FILE_PATH', 'framework/locks'),
'prefix' => 'lock_',
'extension' => '.lock',
],
'redis' => [
'driver' => 'redis',
// Connection is resolved from the container; prefix/ttl are used by the store
'connection' => env('LOCK_REDIS_CONNECTION', 'default'),
'prefix' => env('LOCK_REDIS_PREFIX', 'glueful_lock_'),
'ttl' => 300, // seconds
],
'database' => [
'driver' => 'database',
'table' => env('LOCK_DB_TABLE', 'locks'),
'id_col' => 'key_id',
'token_col' => 'token',
'expiration_col' => 'expiration',
],
],
'prefix' => env('LOCK_PREFIX', 'glueful_lock_'),
'ttl' => env('LOCK_TTL', 300),
'retry' => [
'times' => env('LOCK_RETRY_TIMES', 10),
'delay' => env('LOCK_RETRY_DELAY', 100), // ms
'max_wait' => env('LOCK_MAX_WAIT', 10), // seconds
],
];
Environment Variables
LOCK_DRIVER=redis
LOCK_REDIS_CONNECTION=default
LOCK_REDIS_PREFIX=glueful_lock_
LOCK_PREFIX=glueful_lock_
LOCK_TTL=300
Lock Stores
File Store (Default)
Pros:
- No external dependencies
- Simple setup
- Good for development
Cons:
- Single server only (unless shared filesystem)
- Slower on network filesystems
Use for: Development, single-server deployments
Redis Store
Pros:
- Fast and atomic
- Works across multiple servers
- Wide support
Cons:
- Requires Redis server
Use for: Production multi-server deployments
LOCK_DRIVER=redis
LOCK_REDIS_CONNECTION=default
Database Store
Pros:
- Uses existing database
- No additional infrastructure
Cons:
- Slower than Redis
- Potential table contention
Use for: Low-volume locks, minimal infrastructure
LOCK_DRIVER=database
LOCK_DB_TABLE=locks
Advanced Patterns
Long-Running Operations
Refresh lock during long operations:
$lock = $lockManager->createLock('large:import', ttl: 600);
if ($lock->acquire(true)) { // blocking acquire
try {
while (!done()) {
processChunk();
// Extend lock if running low
if ($lock->getRemainingLifetime() < 120) {
$lock->refresh(600);
}
}
} finally {
$lock->release();
}
}
Multiple Locks
Acquire multiple locks atomically:
$lockA = $lockManager->createLock('resource:a', ttl: 300);
$lockB = $lockManager->createLock('resource:b', ttl: 300);
if ($lockA->acquire() && $lockB->acquire()) {
try {
// Work with both resources
} finally {
$lockB->release();
$lockA->release();
}
}
Named Locks for Resources
// Per-user locks
$lockManager->executeWithLock("user:{$userId}:avatar", function () {
regenerateAvatar($userId);
});
// Per-resource locks
$lockManager->executeWithLock("report:{$reportId}:generate", function () {
generateReport($reportId);
});
Error Handling
use Symfony\Component\Lock\Exception\LockConflictedException;
try {
$lockManager->executeWithLock('inventory:sync', function () {
syncInventory();
}, 300);
} catch (LockConflictedException $e) {
// Lock already held
logger()->info('Inventory sync skipped: already running');
// Optionally queue for retry
$queue = app(\Glueful\Queue\QueueManager::class);
$queue->push(RetryInventorySyncJob::class, [], queue: null, connection: null);
} catch (\Throwable $e) {
logger()->error('Inventory sync failed', ['error' => $e->getMessage()]);
throw $e;
}
Blocking vs Non-Blocking
Non-Blocking (Default)
Returns immediately if lock unavailable:
$lock = $lockManager->createLock('resource');
if ($lock->acquire()) {
// Got lock
} else {
// Lock busy, skip or retry
}
Blocking
Waits until lock is available:
// Wait indefinitely
$lock->acquire(true);
// Wait with timeout using waitAndExecute
$lockManager->waitAndExecute('resource', function () {
// ...
}, maxWait: 30.0);
Monitoring
Track lock performance:
$start = microtime(true);
$lockManager->executeWithLock('feed:refresh', function () {
logger()->info('Refreshing feed with exclusive lock');
refreshFeed();
});
$duration = microtime(true) - $start;
logger()->info('Lock held for ' . $duration . 's');
Key Metrics
- Lock acquisition latency
- Conflict count per key
- Average lock hold duration
- Expired vs. explicit releases
- Wait time distribution
Best Practices
1. Keep Critical Sections Small
// ✅ Good - minimal lock scope
$lockManager->executeWithLock('counter:increment', function () {
incrementCounter();
}, 5);
// ❌ Bad - large lock scope
$lockManager->executeWithLock('process:all', function () {
loadData();
transformData();
saveResults();
sendNotifications();
}, 600);
2. Use Idempotent Operations
$lockManager->executeWithLock('update:stats', function () {
// Safe to run multiple times
recalculateStats();
});
3. Set Realistic TTLs
// Typical duration + buffer
$lockManager->executeWithLock('import', function () {
importData(); // Usually takes 5 min
}, 600); // 10 min TTL
4. Name Locks Semantically
// ✅ Good - descriptive names
$lockManager->executeWithLock('user:42:avatar:regenerate', ...);
$lockManager->executeWithLock('report:daily-sales:generate', ...);
// ❌ Bad - generic names
$lockManager->executeWithLock('task1', ...);
$lockManager->executeWithLock('process', ...);
5. Avoid Unbounded Waits
// ✅ Good - bounded wait
$lockManager->waitAndExecute('task', $callback, maxWait: 30.0);
// ❌ Bad - indefinite wait
$lock->acquire(true); // Could wait forever
6. Partition for High Contention
// Split by shard instead of single lock
$shard = $userId % 10;
$lockManager->executeWithLock("process:shard:{$shard}", function () {
processUserData($userId);
});
Troubleshooting
Frequent lock conflicts?
- Reduce work inside critical section
- Add partitioning/sharding
- Use finer-grained lock keys
Locks not released after crashes?
- Shorten TTL
- Enable auto-release
- Add monitoring for expired locks
High Redis usage?
- Reduce lock granularity
- Batch operations
- Review lock TTLs
Locks not working across servers?
- File store requires shared filesystem
- Switch to Redis or Database store
Database lock table growing?
- Add cleanup job for expired locks
- Reduce TTL durations
- Implement purge strategy
Scheduler Integration
Locks are automatically used by the scheduler to prevent overlapping jobs:
// Scheduler internally uses:
// executeWithLock('scheduler:job:{$jobName}', ...)
// Configured in config/scheduler.php
[
'name' => 'daily-report',
'schedule' => '0 2 * * *',
'handler_class' => 'App\\Jobs\\DailyReportJob',
// Automatically locked during execution
]
Next Steps
- Queues & Jobs - Background processing
- Scheduling - Scheduled tasks
- Caching - Cache with locks
- Performance - Monitoring locks