feat: Add RefreshableLock interface for long-running task support
#335
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.
Problem
Laravel's cache lock implementation provides basic locking primitives (
acquire,release,forceRelease), but lacks support for long-running tasks that may exceed the lock's TTL. This is a known gap that developers have requested - the ability to refresh a lock's TTL without releasing and reacquiring it. The current workaround of release-then-reacquire is non-atomic and creates a race condition window where another process can steal the lock.Real-world scenarios where this matters:
Solution
Introduce a
RefreshableLockinterface that extends the baseLockcontract with two methods:refresh()Behaviorrefresh()refresh(30)refresh()on permanent locktrue(nothing to refresh)refresh(0)orrefresh(-1)InvalidArgumentExceptionWhy throw for
refresh(0)?Passing zero is not "renewing a lease" - it's changing the lock into something fundamentally different (permanent). This is a semantic cliff that can cause operational issues: accidentally converting a TTL lock into a permanent lock can wedge work until someone manually force-releases it.
If you need a permanent lock, acquire it that way:
Cache::lock('key', 0). Therefresh()method is strictly for extending existing TTLs.Why an interface instead of adding to the base class?
Not all lock drivers can implement atomic refresh.
CacheLockandFileLockuse the genericStoreinterface which lacks atomic check-and-update operations. Adding methods that throw "not supported" would violate Liskov Substitution Principle.The interface approach:
RefreshableLockwhen you need refresh capabilityImplementation
RedisLockif GET == owner then EXPIREDatabaseLockUPDATE ... WHERE key = ? AND owner = ?ArrayLockNoLockCacheLockFileLockRedis Implementation
Uses an inline Lua script for atomicity:
TTL Semantics
getRemainingLifetime()returns:float- seconds remainingnull- lock doesn't exist, has expired, or has no expiry (infinite lock)For Redis, this correctly handles the TTL command's special return values (
-1for no expiry,-2for missing key).Usage
For code that requires refreshable locks:
Tests
Added comprehensive tests for all implementing drivers:
CacheRedisLockTest- New file with 15 testsCacheDatabaseLockTest- 10 new tests added to existing fileCacheArrayStoreTest- 12 new tests added to existing fileCacheNoLockTest- New file with 10 testsTests cover: TTL refresh, custom TTL, ownership checks, permanent lock no-op behavior, and
InvalidArgumentExceptionfor invalid TTL values.Cleanup
LuaScripts.phpfrom the cache package. It contained only one method (releaseLock()) used in one place. The Lua script is now inlined inRedisLock::release()for better locality.References
refresh())