From df5a329f59aabb9080931e94b73b27a7ee71647e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:19:23 +0000 Subject: [PATCH 1/6] Initial plan From 18cb4e8a6a9963dc8b17803933c1cb124b73924f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:02:51 +0000 Subject: [PATCH 2/6] Add refresh, isExpired, getRemainingLifetime methods to lock drivers Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --- src/lock/src/Driver/AbstractLock.php | 50 +++++++++ src/lock/src/Driver/CacheLock.php | 43 +++++++- src/lock/src/Driver/CoroutineLock.php | 112 ++++++++++++++++++- src/lock/src/Driver/DatabaseLock.php | 35 ++++++ src/lock/src/Driver/FileSystemLock.php | 43 +++++++- src/lock/src/Driver/LockInterface.php | 19 ++++ src/lock/src/Driver/LuaScripts.php | 17 +++ src/lock/src/Driver/RedisLock.php | 43 +++++++- tests/Lock/AbstractLockTest.php | 145 ++++++++++++++++++++++++- tests/Lock/LuaScriptsTest.php | 21 ++++ tests/Lock/RedisLockTest.php | 95 ++++++++++++++++ 11 files changed, 614 insertions(+), 9 deletions(-) diff --git a/src/lock/src/Driver/AbstractLock.php b/src/lock/src/Driver/AbstractLock.php index 80fbc7708..09543591e 100644 --- a/src/lock/src/Driver/AbstractLock.php +++ b/src/lock/src/Driver/AbstractLock.php @@ -32,6 +32,11 @@ abstract class AbstractLock implements LockInterface */ protected int $sleepMilliseconds = 250; + /** + * The timestamp when the lock was acquired. + */ + protected ?float $acquiredAt = null; + /** * Create a new lock instance. */ @@ -143,4 +148,49 @@ protected function isOwnedByCurrentProcess(): bool { return $this->isOwnedBy($this->owner); } + + /** + * Refresh the lock expiration time. + * {@inheritdoc} + */ + #[Override] + abstract public function refresh(?int $ttl = null): bool; + + /** + * Check if the lock has expired. + * {@inheritdoc} + */ + #[Override] + public function isExpired(): bool + { + if ($this->seconds <= 0) { + return false; + } + + if ($this->acquiredAt === null) { + return true; + } + + return microtime(true) >= ($this->acquiredAt + $this->seconds); + } + + /** + * Get the remaining lifetime of the lock in seconds. + * {@inheritdoc} + */ + #[Override] + public function getRemainingLifetime(): ?float + { + if ($this->seconds <= 0) { + return null; + } + + if ($this->acquiredAt === null) { + return null; + } + + $remaining = ($this->acquiredAt + $this->seconds) - microtime(true); + + return $remaining > 0 ? $remaining : 0.0; + } } diff --git a/src/lock/src/Driver/CacheLock.php b/src/lock/src/Driver/CacheLock.php index 8c6bfbd11..da276e400 100644 --- a/src/lock/src/Driver/CacheLock.php +++ b/src/lock/src/Driver/CacheLock.php @@ -46,7 +46,13 @@ public function acquire(): bool return false; } - return $this->store->set($this->name, $this->owner, $this->seconds); + $result = $this->store->set($this->name, $this->owner, $this->seconds); + + if ($result) { + $this->acquiredAt = microtime(true); + } + + return $result; } /** @@ -56,7 +62,13 @@ public function acquire(): bool public function release(): bool { if ($this->isOwnedByCurrentProcess()) { - return $this->store->delete($this->name); + $result = $this->store->delete($this->name); + + if ($result) { + $this->acquiredAt = null; + } + + return $result; } return false; @@ -69,6 +81,33 @@ public function release(): bool public function forceRelease(): void { $this->store->delete($this->name); + $this->acquiredAt = null; + } + + /** + * Refresh the lock expiration time. + */ + #[Override] + public function refresh(?int $ttl = null): bool + { + $ttl = $ttl ?? $this->seconds; + + if ($ttl <= 0) { + return false; + } + + if (! $this->isOwnedByCurrentProcess()) { + return false; + } + + $result = $this->store->set($this->name, $this->owner, $ttl); + + if ($result) { + $this->seconds = $ttl; + $this->acquiredAt = microtime(true); + } + + return $result; } /** diff --git a/src/lock/src/Driver/CoroutineLock.php b/src/lock/src/Driver/CoroutineLock.php index 099b134d1..c7303cadd 100644 --- a/src/lock/src/Driver/CoroutineLock.php +++ b/src/lock/src/Driver/CoroutineLock.php @@ -36,6 +36,16 @@ class CoroutineLock extends AbstractLock */ protected static ?WeakMap $timers = null; + /** + * @var null|WeakMap + */ + protected static ?WeakMap $acquiredTimes = null; + + /** + * @var null|WeakMap + */ + protected static ?WeakMap $ttls = null; + /** * Create a new lock instance. */ @@ -53,6 +63,8 @@ public function __construct( self::$owners ??= new WeakMap(); self::$timers ??= new WeakMap(); self::$timer ??= new Timer(); + self::$acquiredTimes ??= new WeakMap(); + self::$ttls ??= new WeakMap(); } /** @@ -69,6 +81,9 @@ public function acquire(): bool } self::$owners[$chan] = $this->owner; + $this->acquiredAt = microtime(true); + self::$acquiredTimes[$chan] = $this->acquiredAt; + self::$ttls[$chan] = $this->seconds; if ($timeId = self::$timers[$chan] ?? null) { self::$timer?->clear((int) $timeId); @@ -92,7 +107,13 @@ public function acquire(): bool public function release(): bool { if ($this->isOwnedByCurrentProcess()) { - return (self::$channels[$this->name] ?? null)?->pop(0.01) ? true : false; + $result = (self::$channels[$this->name] ?? null)?->pop(0.01) ? true : false; + + if ($result) { + $this->acquiredAt = null; + } + + return $result; } return false; @@ -109,10 +130,99 @@ public function forceRelease(): void } self::$channels[$this->name] = null; + $this->acquiredAt = null; $chan->close(); } + /** + * Refresh the lock expiration time. + */ + #[Override] + public function refresh(?int $ttl = null): bool + { + $ttl = $ttl ?? $this->seconds; + + if ($ttl <= 0) { + return false; + } + + if (! $this->isOwnedByCurrentProcess()) { + return false; + } + + if (! $chan = self::$channels[$this->name] ?? null) { + return false; + } + + // Clear existing timer + if ($timeId = self::$timers[$chan] ?? null) { + self::$timer?->clear((int) $timeId); + } + + // Update TTL and acquired time + $this->seconds = $ttl; + $this->acquiredAt = microtime(true); + self::$acquiredTimes[$chan] = $this->acquiredAt; + self::$ttls[$chan] = $ttl; + + // Set new timer + $timeId = self::$timer?->after($ttl * 1000, fn () => $this->forceRelease()); + $timeId && self::$timers[$chan] = $timeId; + + return true; + } + + /** + * Check if the lock has expired. + */ + #[Override] + public function isExpired(): bool + { + if ($this->seconds <= 0) { + return false; + } + + if (! $chan = self::$channels[$this->name] ?? null) { + return true; + } + + $acquiredAt = self::$acquiredTimes[$chan] ?? null; + $ttl = self::$ttls[$chan] ?? $this->seconds; + + if ($acquiredAt === null) { + return true; + } + + return microtime(true) >= ($acquiredAt + $ttl); + } + + /** + * Get the remaining lifetime of the lock in seconds. + */ + #[Override] + public function getRemainingLifetime(): ?float + { + if ($this->seconds <= 0) { + return null; + } + + if (! $chan = self::$channels[$this->name] ?? null) { + return null; + } + + $acquiredAt = self::$acquiredTimes[$chan] ?? null; + $ttl = self::$ttls[$chan] ?? $this->seconds; + + if ($acquiredAt === null) { + return null; + } + + $remaining = ($acquiredAt + $ttl) - microtime(true); + + return $remaining > 0 ? $remaining : 0.0; + } + /** * Returns the owner value written into the driver for this lock. * @return string diff --git a/src/lock/src/Driver/DatabaseLock.php b/src/lock/src/Driver/DatabaseLock.php index 90012be7b..5180eb625 100644 --- a/src/lock/src/Driver/DatabaseLock.php +++ b/src/lock/src/Driver/DatabaseLock.php @@ -68,6 +68,10 @@ public function acquire(): bool $acquired = $updated >= 1; } + if ($acquired) { + $this->acquiredAt = microtime(true); + } + return $acquired; } @@ -83,6 +87,8 @@ public function release(): bool ->where('owner', $this->owner) ->delete(); + $this->acquiredAt = null; + return true; } @@ -98,6 +104,35 @@ public function forceRelease(): void $this->connection->table($this->table) ->where('key', $this->name) ->delete(); + $this->acquiredAt = null; + } + + /** + * Refresh the lock expiration time. + */ + #[Override] + public function refresh(?int $ttl = null): bool + { + $ttl = $ttl ?? $this->seconds; + + if ($ttl <= 0) { + return false; + } + + $updated = $this->connection->table($this->table) + ->where('key', $this->name) + ->where('owner', $this->owner) + ->update([ + 'expiration' => time() + $ttl, + ]); + + if ($updated >= 1) { + $this->seconds = $ttl; + $this->acquiredAt = microtime(true); + return true; + } + + return false; } /** diff --git a/src/lock/src/Driver/FileSystemLock.php b/src/lock/src/Driver/FileSystemLock.php index de703ec8f..d8b17f3dc 100644 --- a/src/lock/src/Driver/FileSystemLock.php +++ b/src/lock/src/Driver/FileSystemLock.php @@ -44,7 +44,13 @@ public function acquire(): bool return false; } - return $this->store->set($this->name, $this->owner, $this->seconds) == true; + $result = $this->store->set($this->name, $this->owner, $this->seconds) == true; + + if ($result) { + $this->acquiredAt = microtime(true); + } + + return $result; } /** @@ -54,7 +60,13 @@ public function acquire(): bool public function release(): bool { if ($this->isOwnedByCurrentProcess()) { - return $this->store->delete($this->name); + $result = $this->store->delete($this->name); + + if ($result) { + $this->acquiredAt = null; + } + + return $result; } return false; @@ -67,6 +79,33 @@ public function release(): bool public function forceRelease(): void { $this->store->delete($this->name); + $this->acquiredAt = null; + } + + /** + * Refresh the lock expiration time. + */ + #[Override] + public function refresh(?int $ttl = null): bool + { + $ttl = $ttl ?? $this->seconds; + + if ($ttl <= 0) { + return false; + } + + if (! $this->isOwnedByCurrentProcess()) { + return false; + } + + $result = $this->store->set($this->name, $this->owner, $ttl) == true; + + if ($result) { + $this->seconds = $ttl; + $this->acquiredAt = microtime(true); + } + + return $result; } /** diff --git a/src/lock/src/Driver/LockInterface.php b/src/lock/src/Driver/LockInterface.php index 518d27b0f..466186758 100644 --- a/src/lock/src/Driver/LockInterface.php +++ b/src/lock/src/Driver/LockInterface.php @@ -47,4 +47,23 @@ public function owner(): string; * Releases this lock in disregard of ownership. */ public function forceRelease(): void; + + /** + * Refresh the lock expiration time. + * + * @param null|int $ttl the new time-to-live in seconds, or null to use the original TTL + */ + public function refresh(?int $ttl = null): bool; + + /** + * Check if the lock has expired. + */ + public function isExpired(): bool; + + /** + * Get the remaining lifetime of the lock in seconds. + * + * @return null|float the remaining lifetime in seconds, or null if the lock doesn't expire + */ + public function getRemainingLifetime(): ?float; } diff --git a/src/lock/src/Driver/LuaScripts.php b/src/lock/src/Driver/LuaScripts.php index 678bc398c..de69d63c8 100644 --- a/src/lock/src/Driver/LuaScripts.php +++ b/src/lock/src/Driver/LuaScripts.php @@ -26,6 +26,23 @@ public static function releaseLock(): string else return 0 end +LUA; + } + + /** + * Get the Lua script to atomically refresh a lock's expiration. + * KEYS[1] - The name of the lock + * ARGV[1] - The owner key of the lock instance trying to refresh it. + * ARGV[2] - The new TTL in seconds. + */ + public static function refreshLock(): string + { + return <<<'LUA' +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("expire",KEYS[1],ARGV[2]) +else + return 0 +end LUA; } } diff --git a/src/lock/src/Driver/RedisLock.php b/src/lock/src/Driver/RedisLock.php index 11e399577..23ae3d29f 100644 --- a/src/lock/src/Driver/RedisLock.php +++ b/src/lock/src/Driver/RedisLock.php @@ -45,11 +45,19 @@ public function __construct(string $name, int $seconds, ?string $owner = null, a #[Override] public function acquire(): bool { + $result = false; + if ($this->seconds > 0) { - return $this->store->set($this->name, $this->owner, ['NX', 'EX' => $this->seconds]) == true; + $result = $this->store->set($this->name, $this->owner, ['NX', 'EX' => $this->seconds]) == true; + } else { + $result = $this->store->setNX($this->name, $this->owner) === true; + } + + if ($result) { + $this->acquiredAt = microtime(true); } - return $this->store->setNX($this->name, $this->owner) === true; + return $result; } /** @@ -58,7 +66,13 @@ public function acquire(): bool #[Override] public function release(): bool { - return (bool) $this->store->eval(LuaScripts::releaseLock(), [$this->name, $this->owner], 1); + $result = (bool) $this->store->eval(LuaScripts::releaseLock(), [$this->name, $this->owner], 1); + + if ($result) { + $this->acquiredAt = null; + } + + return $result; } /** @@ -68,6 +82,29 @@ public function release(): bool public function forceRelease(): void { $this->store->del($this->name); + $this->acquiredAt = null; + } + + /** + * Refresh the lock expiration time. + */ + #[Override] + public function refresh(?int $ttl = null): bool + { + $ttl = $ttl ?? $this->seconds; + + if ($ttl <= 0) { + return false; + } + + $result = (bool) $this->store->eval(LuaScripts::refreshLock(), [$this->name, $this->owner, $ttl], 1); + + if ($result) { + $this->seconds = $ttl; + $this->acquiredAt = microtime(true); + } + + return $result; } /** diff --git a/tests/Lock/AbstractLockTest.php b/tests/Lock/AbstractLockTest.php index ebbe3b5d0..40b10039b 100644 --- a/tests/Lock/AbstractLockTest.php +++ b/tests/Lock/AbstractLockTest.php @@ -18,6 +18,8 @@ class TestLock extends AbstractLock private bool $canRelease = true; + private bool $canRefresh = true; + private string $currentOwner = ''; public function setCanAcquire(bool $canAcquire): void @@ -30,24 +32,64 @@ public function setCanRelease(bool $canRelease): void $this->canRelease = $canRelease; } + public function setCanRefresh(bool $canRefresh): void + { + $this->canRefresh = $canRefresh; + } + public function setCurrentOwner(string $owner): void { $this->currentOwner = $owner; } + public function setAcquiredAt(?float $acquiredAt): void + { + $this->acquiredAt = $acquiredAt; + } + + public function getAcquiredAt(): ?float + { + return $this->acquiredAt; + } + + public function getSeconds(): int + { + return $this->seconds; + } + public function acquire(): bool { + if ($this->canAcquire) { + $this->acquiredAt = microtime(true); + } return $this->canAcquire; } public function release(): bool { + if ($this->canRelease) { + $this->acquiredAt = null; + } return $this->canRelease; } public function forceRelease(): void { - // Do nothing for test implementation + $this->acquiredAt = null; + } + + public function refresh(?int $ttl = null): bool + { + if (! $this->canRefresh) { + return false; + } + $ttl = $ttl ?? $this->seconds; + if ($ttl <= 0) { + return false; + } + $this->seconds = $ttl; + $this->acquiredAt = microtime(true); + return true; } public function getSleepMilliseconds(): int @@ -245,3 +287,104 @@ public function forceRelease(): void expect($result)->toBe($lock); expect($lock->getSleepMilliseconds())->toBe(500); }); + +test('refresh method returns true when lock can be refreshed', function () { + $lock = new TestLock('test', 60, 'owner'); + $lock->setCanRefresh(true); + $lock->acquire(); + + $result = $lock->refresh(); + + expect($result)->toBeTrue(); +}); + +test('refresh method returns false when ttl is zero or negative', function () { + $lock = new TestLock('test', 0, 'owner'); + $lock->setCanRefresh(true); + + $result = $lock->refresh(); + + expect($result)->toBeFalse(); +}); + +test('refresh method updates seconds when new ttl provided', function () { + $lock = new TestLock('test', 60, 'owner'); + $lock->setCanRefresh(true); + $lock->acquire(); + + $lock->refresh(120); + + expect($lock->getSeconds())->toBe(120); +}); + +test('refresh method updates acquired at timestamp', function () { + $lock = new TestLock('test', 60, 'owner'); + $lock->setCanRefresh(true); + $lock->acquire(); + + $originalAcquiredAt = $lock->getAcquiredAt(); + usleep(1000); // Sleep 1ms + $lock->refresh(); + + expect($lock->getAcquiredAt())->toBeGreaterThan($originalAcquiredAt); +}); + +test('isExpired returns false when lock has no expiration', function () { + $lock = new TestLock('test', 0, 'owner'); + $lock->acquire(); + + expect($lock->isExpired())->toBeFalse(); +}); + +test('isExpired returns true when acquiredAt is null', function () { + $lock = new TestLock('test', 60, 'owner'); + $lock->setAcquiredAt(null); + + expect($lock->isExpired())->toBeTrue(); +}); + +test('isExpired returns false when lock is still valid', function () { + $lock = new TestLock('test', 60, 'owner'); + $lock->acquire(); + + expect($lock->isExpired())->toBeFalse(); +}); + +test('isExpired returns true when lock has expired', function () { + $lock = new TestLock('test', 1, 'owner'); + $lock->setAcquiredAt(microtime(true) - 2); // Acquired 2 seconds ago + + expect($lock->isExpired())->toBeTrue(); +}); + +test('getRemainingLifetime returns null when lock has no expiration', function () { + $lock = new TestLock('test', 0, 'owner'); + $lock->acquire(); + + expect($lock->getRemainingLifetime())->toBeNull(); +}); + +test('getRemainingLifetime returns null when acquiredAt is null', function () { + $lock = new TestLock('test', 60, 'owner'); + $lock->setAcquiredAt(null); + + expect($lock->getRemainingLifetime())->toBeNull(); +}); + +test('getRemainingLifetime returns positive value for valid lock', function () { + $lock = new TestLock('test', 60, 'owner'); + $lock->acquire(); + + $remaining = $lock->getRemainingLifetime(); + + expect($remaining)->toBeFloat(); + expect($remaining)->toBeGreaterThan(0); + expect($remaining)->toBeLessThanOrEqual(60); +}); + +test('getRemainingLifetime returns zero when lock has expired', function () { + $lock = new TestLock('test', 1, 'owner'); + $lock->setAcquiredAt(microtime(true) - 2); // Acquired 2 seconds ago + + expect($lock->getRemainingLifetime())->toBe(0.0); +}); diff --git a/tests/Lock/LuaScriptsTest.php b/tests/Lock/LuaScriptsTest.php index 0218e0a8e..259387a8b 100644 --- a/tests/Lock/LuaScriptsTest.php +++ b/tests/Lock/LuaScriptsTest.php @@ -30,3 +30,24 @@ expect($script)->toContain('return 0'); expect($script)->toContain('end'); }); + +test('refresh lock script returns valid lua script', function () { + $script = LuaScripts::refreshLock(); + + expect($script)->toBeString(); + expect($script)->toContain('redis.call("get",KEYS[1])'); + expect($script)->toContain('ARGV[1]'); + expect($script)->toContain('redis.call("expire",KEYS[1],ARGV[2])'); +}); + +test('refresh lock script has correct logic structure', function () { + $script = LuaScripts::refreshLock(); + + // The script should check if the current owner matches + expect($script)->toContain('if redis.call("get",KEYS[1]) == ARGV[1] then'); + // Should extend the expiration if owner matches + expect($script)->toContain('return redis.call("expire",KEYS[1],ARGV[2])'); + // Should return 0 if owner doesn't match + expect($script)->toContain('return 0'); + expect($script)->toContain('end'); +}); diff --git a/tests/Lock/RedisLockTest.php b/tests/Lock/RedisLockTest.php index a524edea0..6ea1c5fcb 100644 --- a/tests/Lock/RedisLockTest.php +++ b/tests/Lock/RedisLockTest.php @@ -240,3 +240,98 @@ public function setStore($store): void expect($result)->toBeFalse(); }); + +test('can refresh lock successfully', function () { + $redis = m::mock(RedisProxy::class); + $redis->shouldReceive('eval')->with(LuaScripts::refreshLock(), ['test_lock', 'owner123', 60], 1)->andReturn(1); + + $lock = new class('test_lock', 60, 'owner123') extends RedisLock { + public function __construct(string $name, int $seconds, ?string $owner = null) + { + $this->name = $name; + $this->seconds = $seconds; + $this->owner = $owner ?? 'default'; + } + + public function setStore($store): void + { + $this->store = $store; + } + }; + + $lock->setStore($redis); + $result = $lock->refresh(); + + expect($result)->toBeTrue(); +}); + +test('refresh with custom ttl uses provided value', function () { + $redis = m::mock(RedisProxy::class); + $redis->shouldReceive('eval')->with(LuaScripts::refreshLock(), ['test_lock', 'owner123', 120], 1)->andReturn(1); + + $lock = new class('test_lock', 60, 'owner123') extends RedisLock { + public function __construct(string $name, int $seconds, ?string $owner = null) + { + $this->name = $name; + $this->seconds = $seconds; + $this->owner = $owner ?? 'default'; + } + + public function setStore($store): void + { + $this->store = $store; + } + }; + + $lock->setStore($redis); + $result = $lock->refresh(120); + + expect($result)->toBeTrue(); +}); + +test('refresh returns false when script returns 0', function () { + $redis = m::mock(RedisProxy::class); + $redis->shouldReceive('eval')->with(LuaScripts::refreshLock(), ['test_lock', 'owner123', 60], 1)->andReturn(0); + + $lock = new class('test_lock', 60, 'owner123') extends RedisLock { + public function __construct(string $name, int $seconds, ?string $owner = null) + { + $this->name = $name; + $this->seconds = $seconds; + $this->owner = $owner ?? 'default'; + } + + public function setStore($store): void + { + $this->store = $store; + } + }; + + $lock->setStore($redis); + $result = $lock->refresh(); + + expect($result)->toBeFalse(); +}); + +test('refresh returns false when ttl is zero or negative', function () { + $redis = m::mock(RedisProxy::class); + + $lock = new class('test_lock', 0, 'owner123') extends RedisLock { + public function __construct(string $name, int $seconds, ?string $owner = null) + { + $this->name = $name; + $this->seconds = $seconds; + $this->owner = $owner ?? 'default'; + } + + public function setStore($store): void + { + $this->store = $store; + } + }; + + $lock->setStore($redis); + $result = $lock->refresh(); + + expect($result)->toBeFalse(); +}); From 01d8b8400b195f742752f1200dd18d4f37ac3d02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:04:25 +0000 Subject: [PATCH 3/6] Add documentation for CoroutineLock WeakMaps Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --- src/lock/src/Driver/CoroutineLock.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lock/src/Driver/CoroutineLock.php b/src/lock/src/Driver/CoroutineLock.php index c7303cadd..1340dff46 100644 --- a/src/lock/src/Driver/CoroutineLock.php +++ b/src/lock/src/Driver/CoroutineLock.php @@ -20,28 +20,41 @@ class CoroutineLock extends AbstractLock { /** + * Mapping of lock names to their corresponding channels. + * * @var array */ protected static array $channels = []; /** + * Mapping of channels to their current owners (used for ownership verification). + * * @var null|WeakMap */ protected static ?WeakMap $owners = null; + /** + * Timer instance for scheduling lock expiration. + */ protected static ?Timer $timer = null; /** + * Mapping of channels to their timer IDs (for clearing expiration timers). + * * @var null|WeakMap */ protected static ?WeakMap $timers = null; /** + * Mapping of channels to their acquisition timestamps (for tracking expiration). + * * @var null|WeakMap */ protected static ?WeakMap $acquiredTimes = null; /** + * Mapping of channels to their TTL values (for tracking remaining lifetime). + * * @var null|WeakMap */ protected static ?WeakMap $ttls = null; From f8551c90c926a3d5808348705baa60ae28ca4468 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:00:06 +0800 Subject: [PATCH 4/6] Move methods to end of AbstractLock class Relocated the getCurrentOwner and isOwnedByCurrentProcess methods to the end of the AbstractLock class for improved code organization. No functional changes were made. Co-Authored-By: Deeka Wong <8337659+huangdijia@users.noreply.github.com> --- src/lock/src/Driver/AbstractLock.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lock/src/Driver/AbstractLock.php b/src/lock/src/Driver/AbstractLock.php index 09543591e..736bf15bb 100644 --- a/src/lock/src/Driver/AbstractLock.php +++ b/src/lock/src/Driver/AbstractLock.php @@ -136,19 +136,6 @@ public function isOwnedBy($owner): bool return $this->getCurrentOwner() === $owner; } - /** - * Returns the owner value written into the driver for this lock. - */ - abstract protected function getCurrentOwner(); - - /** - * Determines whether this lock is allowed to release the lock in the driver. - */ - protected function isOwnedByCurrentProcess(): bool - { - return $this->isOwnedBy($this->owner); - } - /** * Refresh the lock expiration time. * {@inheritdoc} @@ -193,4 +180,17 @@ public function getRemainingLifetime(): ?float return $remaining > 0 ? $remaining : 0.0; } + + /** + * Returns the owner value written into the driver for this lock. + */ + abstract protected function getCurrentOwner(); + + /** + * Determines whether this lock is allowed to release the lock in the driver. + */ + protected function isOwnedByCurrentProcess(): bool + { + return $this->isOwnedBy($this->owner); + } } From 120e8771b950db004bc7fa373db92a6b075e0d50 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:51:19 +0800 Subject: [PATCH 5/6] fix: rename timers to timerIds in CoroutineLock for clarity --- .vscode/cspell.json | 1 + src/lock/src/Driver/CoroutineLock.php | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 09fdcc60f..e0bef7f62 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -84,6 +84,7 @@ "Symfony", "Thich", "Traceparent", + "ttls", "tunlp", "ucsplit", "undot", diff --git a/src/lock/src/Driver/CoroutineLock.php b/src/lock/src/Driver/CoroutineLock.php index 1340dff46..975a2596e 100644 --- a/src/lock/src/Driver/CoroutineLock.php +++ b/src/lock/src/Driver/CoroutineLock.php @@ -43,7 +43,7 @@ class CoroutineLock extends AbstractLock * * @var null|WeakMap */ - protected static ?WeakMap $timers = null; + protected static ?WeakMap $timerIds = null; /** * Mapping of channels to their acquisition timestamps (for tracking expiration). @@ -74,10 +74,10 @@ public function __construct( parent::__construct($name, $seconds, $owner); self::$owners ??= new WeakMap(); - self::$timers ??= new WeakMap(); - self::$timer ??= new Timer(); self::$acquiredTimes ??= new WeakMap(); self::$ttls ??= new WeakMap(); + self::$timer ??= new Timer(); + self::$timerIds ??= new WeakMap(); } /** @@ -98,13 +98,13 @@ public function acquire(): bool self::$acquiredTimes[$chan] = $this->acquiredAt; self::$ttls[$chan] = $this->seconds; - if ($timeId = self::$timers[$chan] ?? null) { + if ($timeId = self::$timerIds[$chan] ?? null) { self::$timer?->clear((int) $timeId); } if ($this->seconds > 0) { $timeId = self::$timer?->after($this->seconds * 1000, fn () => $this->forceRelease()); - $timeId && self::$timers[$chan] = $timeId; + $timeId && self::$timerIds[$chan] = $timeId; } } catch (Throwable) { return false; @@ -169,7 +169,7 @@ public function refresh(?int $ttl = null): bool } // Clear existing timer - if ($timeId = self::$timers[$chan] ?? null) { + if ($timeId = self::$timerIds[$chan] ?? null) { self::$timer?->clear((int) $timeId); } @@ -181,7 +181,7 @@ public function refresh(?int $ttl = null): bool // Set new timer $timeId = self::$timer?->after($ttl * 1000, fn () => $this->forceRelease()); - $timeId && self::$timers[$chan] = $timeId; + $timeId && self::$timerIds[$chan] = $timeId; return true; } From 4bcd2695296b5cab632601d923fe069a531ecd1f Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:55:44 +0800 Subject: [PATCH 6/6] fix: adjust timer duration in CoroutineLock to use seconds directly --- src/lock/src/Driver/CoroutineLock.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lock/src/Driver/CoroutineLock.php b/src/lock/src/Driver/CoroutineLock.php index 975a2596e..e050c129e 100644 --- a/src/lock/src/Driver/CoroutineLock.php +++ b/src/lock/src/Driver/CoroutineLock.php @@ -103,7 +103,7 @@ public function acquire(): bool } if ($this->seconds > 0) { - $timeId = self::$timer?->after($this->seconds * 1000, fn () => $this->forceRelease()); + $timeId = self::$timer?->after($this->seconds, fn () => $this->forceRelease()); $timeId && self::$timerIds[$chan] = $timeId; } } catch (Throwable) { @@ -180,7 +180,7 @@ public function refresh(?int $ttl = null): bool self::$ttls[$chan] = $ttl; // Set new timer - $timeId = self::$timer?->after($ttl * 1000, fn () => $this->forceRelease()); + $timeId = self::$timer?->after($ttl, fn () => $this->forceRelease()); $timeId && self::$timerIds[$chan] = $timeId; return true;