feat: add atomic locks for cache-backed concurrency control#10145
Open
memleakd wants to merge 5 commits intocodeigniter4:4.8from
Open
feat: add atomic locks for cache-backed concurrency control#10145memleakd wants to merge 5 commits intocodeigniter4:4.8from
memleakd wants to merge 5 commits intocodeigniter4:4.8from
Conversation
Signed-off-by: memleakd <[email protected]>
Signed-off-by: memleakd <[email protected]>
Signed-off-by: memleakd <[email protected]>
Signed-off-by: memleakd <[email protected]>
Signed-off-by: memleakd <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR proposes an atomic lock primitive for CodeIgniter 4.
The goal is to give applications a framework-native way to coordinate work across concurrent requests, CLI commands, queue workers, and other long-running processes.
The new lock component includes:
service('locks')CodeIgniter\Lock\LockManagerCodeIgniter\Lock\LockInterfaceCodeIgniter\Lock\LockStoreInterfaceSupported lock operations include:
acquire()for immediate acquisitionblock()for waiting up to a limited number of secondsrun()for acquire/callback/release usagerelease()for owner-checked releaseforceRelease()for administrative releaserefresh()for extending a lock TTLisAcquired()for checking current ownershipowner()for retrieving the owner tokenrestore()for restoring a lock from a known owner tokenBackground
Some application work should only happen once at a time, even when multiple PHP processes are active.
Common examples include:
Today, applications can build these patterns manually with cache keys, database flags, or custom tables, but ownership and expiry details are easy to get subtly wrong. A small framework primitive gives users a safer and more consistent baseline without requiring every project to invent its own locking convention.
Comparison
This is a common framework-level primitive in other ecosystems:
This PR follows the same general idea, while keeping the implementation aligned with CodeIgniter’s existing services and cache-handler architecture.
Although the built-in stores are backed by cache handlers, locks are not cache values. They are a concurrency primitive with ownership, release, refresh, and blocking semantics. Keeping the public API under
CodeIgniter\Lockavoids expandingCacheInterfacewith lock-specific methods and keeps lock support opt-in throughLockStoreInterface.This also leaves room for future non-cache stores, such as database or advisory-lock stores, without changing the user-facing API.
Proposal Scope
This is intended as a conservative proposal for review. I'll be happy to adjust if the team does not want this in core, prefers a different API, or wants it split differently.
This PR intentionally keeps the feature low-level. It currently does not add:
The implementation focuses only on the primitive itself: acquire a named lock, verify ownership, release it safely, and allow the lock to expire if the process disappears.
Higher-level features can build on this later if the team wants them.
Behavior
Locks are advisory. Code that needs protection must explicitly acquire the lock before entering the critical section.
Each acquired lock has an owner token.
release()andrefresh()only succeed for the current owner, which helps avoid one process accidentally releasing another process’s lock after expiry and reacquisition.Each lock has a TTL. This prevents abandoned locks from being held forever, but it also means long-running work must choose a suitable TTL, call
refresh(), or checkisAcquired()before irreversible side effects.Logical lock names are hashed before reaching the cache handler, so applications can use descriptive names without worrying about reserved cache-key characters.
Supported Cache Handlers
This PR adds lock-store support for:
Memcached is intentionally not included in this first version.
Memcached can acquire a lock with atomic
add(), but safe owner-aware release and refresh require compare-and-delete / compare-and-touch semantics. A naiveget owner -> deleteflow can race if the lock expires and another owner acquires it between those operations.I think Memcached support would be better handled in a separate PR with a CAS-based implementation and dedicated live tests, instead of adding a weaker implementation here.
Testing
This PR adds tests for:
run()callback behaviorservice('locks')behaviorChecklist: