Skip to content

RFC: 5.next - Add Lock component for distributed locking#19370

Draft
dereuromark wants to merge 7 commits intocakephp:5.nextfrom
dereuromark:feature/lock-component
Draft

RFC: 5.next - Add Lock component for distributed locking#19370
dereuromark wants to merge 7 commits intocakephp:5.nextfrom
dereuromark:feature/lock-component

Conversation

@dereuromark
Copy link
Copy Markdown
Member

@dereuromark dereuromark commented Mar 28, 2026

Summary

Introduces a new Lock component that provides distributed locking mechanisms to prevent race conditions when multiple processes access shared resources.

This was identified as a gap in CakePHP compared to other frameworks (Symfony Lock component, Laravel Cache::lock()). The implementation follows CakePHP's existing patterns from the Cache component.

Features

  • LockInterface - defines the contract for lock engines
  • LockInstance - immutable value object representing an acquired lock
  • Lock - static facade for convenient access (similar to Cache)
  • LockRegistry - registry for managing engine instances

Lock Engines

Engine Use Case
RedisLockEngine Production, distributed systems. Uses Redis SET NX with Lua scripts for atomicity
MemcachedLockEngine Production. Uses Memcached add() for atomic acquisition
FileLockEngine Single-server deployments. Uses flock() for local filesystem locking
NullLockEngine Testing and development. No-op engine

Key Capabilities

  • Non-blocking acquire() and blocking acquireBlocking() with timeout
  • Lock refresh to extend TTL for long-running operations
  • Owner verification on release (prevents releasing others' locks)
  • Force release for administrative purposes
  • synchronized() helper for automatic lock/unlock around callbacks

Example Usage

// Configure
Lock::setConfig('default', [
    'className' => RedisLockEngine::class,
    'host' => '127.0.0.1',
]);

// Basic usage
$lock = Lock::acquire('my-resource');
if ($lock !== null) {
    try {
        // Critical section
    } finally {
        Lock::release($lock);
    }
}

// Blocking with timeout
$lock = Lock::acquireBlocking('payment-' . $orderId, ttl: 60, timeout: 10);

// Synchronized helper (auto-release)
$result = Lock::synchronized('my-resource', function () {
    return doExpensiveWork();
});

Namespace: Lock

Namespaces WITHOUT split packages (monolith-only):

  • Command
  • Controller
  • Error
  • Lock (the new one)
  • Mailer
  • Network
  • Routing
  • TestSuite
  • View

This seems to fit, since we dont need its own package usually here.

Related Discussion

This addresses the gap identified when comparing CakePHP to other frameworks. See: Symfony Lock component, Laravel's atomic locks.

We could also make this a plugin. But it seems this could be a core feature.

PS: Symfony added 2 years after Lock also Semaphore functionality. I descoped that for now, could also be something added within the next 2 years if thats what people need/want.

Introduces a new Lock component that provides distributed locking
mechanisms to prevent race conditions when multiple processes access
shared resources.

Features:
- LockInterface defines the contract for lock engines
- LockInstance value object represents an acquired lock
- Lock static facade for convenient access (similar to Cache)
- LockRegistry for managing engine instances
- Four lock engines:
  - RedisLockEngine: Uses Redis SET NX with Lua scripts for atomicity
  - MemcachedLockEngine: Uses Memcached add() for atomic acquisition
  - FileLockEngine: Uses flock() for local filesystem locking
  - NullLockEngine: No-op engine for testing and development

Key capabilities:
- Non-blocking acquire() and blocking acquireBlocking()
- Lock refresh to extend TTL
- Owner verification on release (prevents releasing others' locks)
- Force release for administrative purposes
- synchronized() helper for automatic lock/unlock around callbacks

Example usage:
    Lock::setConfig('default', ['className' => RedisLockEngine::class]);

    $lock = Lock::acquire('my-resource');
    if ($lock !== null) {
        try {
            // Critical section
        } finally {
            Lock::release($lock);
        }
    }

    // Or using synchronized helper:
    $result = Lock::synchronized('my-resource', function () {
        return doWork();
    });
- Use empty array comparison instead of count() in MemcachedLockEngine
- Combine nested if statements in RedisLockEngine
Use getConfig() instead of direct array access to properly handle
defaults when config is not fully initialized.
Import the env() function from Cake\Core to fix "undefined function" errors.
Use getVersion() to verify the Memcached connection actually works,
allowing tests to be properly skipped when connection fails.
Wrap cleanupTestLocks in try-catch to prevent test failures
when Redis connection drops during cleanup.
@dereuromark dereuromark added this to the 5.4.0 milestone Mar 28, 2026
@dereuromark dereuromark changed the title Add Lock component for distributed locking RFC: 5.next - Add Lock component for distributed locking Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant