├── src ├── Exception │ └── LockTimeoutException.php ├── Annotation │ ├── LockAspect.php │ ├── Lock.php │ ├── Blockable.php │ └── BlockableAspect.php ├── Functions.php ├── Driver │ ├── LuaScripts.php │ ├── LockInterface.php │ ├── FileSystemLock.php │ ├── RedisLock.php │ ├── CacheLock.php │ ├── DatabaseLock.php │ ├── AbstractLock.php │ └── CoroutineLock.php ├── ConfigProvider.php ├── LockFactory.php └── Listener │ └── RegisterPropertyHandlerListener.php ├── migrations └── create_lock_table.php ├── publish └── lock.php ├── LICENSE ├── README_CN.md ├── composer.json └── README.md /src/Exception/LockTimeoutException.php: -------------------------------------------------------------------------------- 1 | process(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Annotation/Lock.php: -------------------------------------------------------------------------------- 1 | get(LockFactory::class); 23 | 24 | if (is_null($name)) { 25 | return $factory; 26 | } 27 | 28 | return $factory->make($name, $seconds, $owner, $driver); 29 | } 30 | -------------------------------------------------------------------------------- /migrations/create_lock_table.php: -------------------------------------------------------------------------------- 1 | string('key')->unique(); 24 | $table->mediumText('value'); 25 | $table->integer('expiration'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('locks'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /publish/lock.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'driver' => FriendsOfHyperf\Lock\Driver\RedisLock::class, 14 | 'constructor' => ['pool' => 'default', 'prefix' => 'lock:'], 15 | ], 16 | 'file' => [ 17 | 'driver' => FriendsOfHyperf\Lock\Driver\FileSystemLock::class, 18 | 'constructor' => [ 19 | 'config' => ['prefix' => 'lock:'], 20 | ], 21 | ], 22 | 'database' => [ 23 | 'driver' => FriendsOfHyperf\Lock\Driver\DatabaseLock::class, 24 | 'constructor' => ['pool' => 'default', 'table' => 'locks', 'prefix' => 'lock:'], 25 | ], 26 | 'co' => [ 27 | 'driver' => FriendsOfHyperf\Lock\Driver\CoroutineLock::class, 28 | 'constructor' => ['prefix' => 'lock:'], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Taylor Otwell 4 | Copyright (c) D.J.Hwang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Driver/LuaScripts.php: -------------------------------------------------------------------------------- 1 | get()) { 27 | // 获取锁定10秒... 28 | 29 | $lock->release(); 30 | } 31 | ``` 32 | 33 | `get` 方法也可以接收一个闭包。在闭包执行之后,将会自动释放锁: 34 | 35 | ```php 36 | lock('foo')->get(function () { 37 | // 获取无限期锁并自动释放... 38 | }); 39 | ``` 40 | 41 | 如果你在请求时锁无法使用,你可以控制等待指定的秒数。如果在指定的时间限制内无法获取锁,则会抛出 `FriendsOfHyperf\Lock\Exception\LockTimeoutException` 42 | 43 | ```php 44 | use FriendsOfHyperf\Lock\Exception\LockTimeoutException; 45 | 46 | $lock = lock('foo', 10); 47 | 48 | try { 49 | $lock->block(5); 50 | 51 | // 等待最多5秒后获取的锁... 52 | } catch (LockTimeoutException $e) { 53 | // 无法获取锁... 54 | } finally { 55 | $lock->release(); 56 | } 57 | 58 | lock('foo', 10)->block(5, function () { 59 | // 等待最多5秒后获取的锁... 60 | }); 61 | ``` 62 | 63 | 注解方式 64 | 65 | ```php 66 | use FriendsOfHyperf\Lock\Annotation\Lock; 67 | use FriendsOfHyperf\Lock\Driver\LockInterface; 68 | 69 | class Foo 70 | { 71 | #[Lock(name:"foo", seconds:10)] 72 | protected LockInterface $lock; 73 | 74 | public function bar() 75 | { 76 | $this->lock->get(function () { 77 | // 获取无限期锁并自动释放... 78 | }); 79 | } 80 | } 81 | ``` 82 | -------------------------------------------------------------------------------- /src/Annotation/BlockableAspect.php: -------------------------------------------------------------------------------- 1 | arguments['keys'] ?? []; 32 | $annotationMetadata = $proceedingJoinPoint->getAnnotationMetadata(); 33 | /** @var null|Blockable $annotation */ 34 | $annotation = $annotationMetadata->method[Blockable::class] ?? null; 35 | 36 | if (! $annotation || $annotation->seconds <= 0) { 37 | return $proceedingJoinPoint->process(); 38 | } 39 | 40 | $key = StringHelper::format($annotation->prefix, $arguments, $annotation->value); 41 | 42 | return $this->lockFactory->make($key, $annotation->ttl, driver: $annotation->driver) 43 | ->block($annotation->seconds, fn () => $proceedingJoinPoint->process()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 24 | RegisterPropertyHandlerListener::class, 25 | ], 26 | 'aspects' => [ 27 | LockAspect::class, 28 | BlockableAspect::class, 29 | ], 30 | 'publish' => [ 31 | [ 32 | 'id' => 'config', 33 | 'description' => 'The configuration file of lock.', 34 | 'source' => __DIR__ . '/../publish/lock.php', 35 | 'destination' => BASE_PATH . '/config/autoload/lock.php', 36 | ], 37 | [ 38 | 'id' => 'migrations', 39 | 'description' => 'The migrations file of lock', 40 | 'source' => __DIR__ . '/../migrations/create_lock_table.php', 41 | 'destination' => BASE_PATH . '/migrations/2021_01_31_000000_create_lock_table.php', 42 | ], 43 | ], 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/LockFactory.php: -------------------------------------------------------------------------------- 1 | config->has("lock.{$driver}")) { 35 | throw new InvalidArgumentException(sprintf('The lock config %s is invalid.', $driver)); 36 | } 37 | 38 | /** @var class-string $driverClass */ 39 | $driverClass = $this->config->get("lock.{$driver}.driver", RedisLock::class); 40 | /** @var array{config:array} $constructor */ 41 | $constructor = $this->config->get("lock.{$driver}.constructor", ['config' => []]); 42 | 43 | return make($driverClass, [ 44 | 'name' => $name, 45 | 'seconds' => $seconds, 46 | 'owner' => $owner, 47 | 'constructor' => $constructor, 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendsofhyperf/lock", 3 | "description": "The lock component for Hyperf.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "hyperf", 8 | "v3.1" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "huangdijia", 13 | "email": "huangdijia@gmail.com" 14 | } 15 | ], 16 | "support": { 17 | "issues": "https://github.com/friendsofhyperf/components/issues", 18 | "source": "https://github.com/friendsofhyperf/components", 19 | "docs": "https://docs.hdj.me", 20 | "pull-request": "https://github.com/friendsofhyperf/components/pulls" 21 | }, 22 | "require": { 23 | "hyperf/config": "~3.1.0", 24 | "hyperf/context": "~3.1.0", 25 | "hyperf/macroable": "~3.1.0", 26 | "hyperf/stringable": "~3.1.0", 27 | "hyperf/support": "~3.1.0", 28 | "nesbot/carbon": "^2.0 || ^3.0" 29 | }, 30 | "suggest": { 31 | "hyperf/cache": "Require this component for driver 'file'.", 32 | "hyperf/db-connection": "Require this component for driver 'database'.", 33 | "hyperf/redis": "Require this component for driver 'redis'." 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "FriendsOfHyperf\\Lock\\": "src" 38 | }, 39 | "files": [ 40 | "src/Functions.php" 41 | ] 42 | }, 43 | "config": { 44 | "optimize-autoloader": true, 45 | "sort-packages": true 46 | }, 47 | "extra": { 48 | "branch-alias": { 49 | "dev-main": "3.1-dev" 50 | }, 51 | "hyperf": { 52 | "config": "FriendsOfHyperf\\Lock\\ConfigProvider" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Listener/RegisterPropertyHandlerListener.php: -------------------------------------------------------------------------------- 1 | name; 48 | $seconds = (int) $annotation->seconds; 49 | $owner = $annotation->owner; 50 | $driver = $annotation->driver; 51 | 52 | $reflectionProperty->setValue($object, $this->lockFactory->make($name, $seconds, $owner, $driver)); 53 | } 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Driver/LockInterface.php: -------------------------------------------------------------------------------- 1 | get()) { 31 | // Lock acquired for 10 seconds... 32 | 33 | $lock->release(); 34 | } 35 | ``` 36 | 37 | The `get` method also accepts a closure. After the closure is executed, Will automatically release the lock: 38 | 39 | ```php 40 | lock('foo')->get(function () { 41 | // Lock acquired indefinitely and automatically released... 42 | }); 43 | ``` 44 | 45 | If the lock is not available at the moment you request it, you may instruct the lock to wait for a specified number of seconds. If the lock can not be acquired within the specified time limit, an `FriendsOfHyperf\Lock\Exception\LockTimeoutException` will be thrown: 46 | 47 | ```php 48 | use FriendsOfHyperf\Lock\Exception\LockTimeoutException; 49 | 50 | $lock = lock('foo', 10); 51 | 52 | try { 53 | $lock->block(5); 54 | 55 | // Lock acquired after waiting maximum of 5 seconds... 56 | } catch (LockTimeoutException $e) { 57 | // Unable to acquire lock... 58 | } finally { 59 | $lock->release(); 60 | } 61 | 62 | lock('foo', 10)->block(5, function () { 63 | // Lock acquired after waiting maximum of 5 seconds... 64 | }); 65 | ``` 66 | 67 | Using by annotation 68 | 69 | ```php 70 | use FriendsOfHyperf\Lock\Annotation\Lock; 71 | use FriendsOfHyperf\Lock\Driver\LockInterface; 72 | 73 | class Foo 74 | { 75 | #[Lock(name:"foo", seconds:10)] 76 | protected LockInterface $lock; 77 | 78 | public function bar() 79 | { 80 | $this->lock->get(function () { 81 | // Lock acquired indefinitely and automatically released... 82 | }); 83 | } 84 | } 85 | ``` 86 | 87 | ## Contact 88 | 89 | - [Twitter](https://twitter.com/huangdijia) 90 | - [Gmail](mailto:huangdijia@gmail.com) 91 | 92 | ## License 93 | 94 | [MIT](LICENSE) 95 | -------------------------------------------------------------------------------- /src/Driver/FileSystemLock.php: -------------------------------------------------------------------------------- 1 | ['prefix' => 'lock:']], $constructor); 34 | $this->store = make(FileSystemDriver::class, $constructor); 35 | } 36 | 37 | /** 38 | * Attempt to acquire the lock. 39 | */ 40 | #[Override] 41 | public function acquire(): bool 42 | { 43 | if ($this->store->has($this->name)) { 44 | return false; 45 | } 46 | 47 | $result = $this->store->set($this->name, $this->owner, $this->seconds) == true; 48 | 49 | if ($result) { 50 | $this->acquiredAt = microtime(true); 51 | } 52 | 53 | return $result; 54 | } 55 | 56 | /** 57 | * Release the lock. 58 | */ 59 | #[Override] 60 | public function release(): bool 61 | { 62 | if ($this->isOwnedByCurrentProcess()) { 63 | $result = $this->store->delete($this->name); 64 | 65 | if ($result) { 66 | $this->acquiredAt = null; 67 | } 68 | 69 | return $result; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | /** 76 | * Releases this lock in disregard of ownership. 77 | */ 78 | #[Override] 79 | public function forceRelease(): void 80 | { 81 | $this->store->delete($this->name); 82 | $this->acquiredAt = null; 83 | } 84 | 85 | /** 86 | * Refresh the lock expiration time. 87 | */ 88 | #[Override] 89 | public function refresh(?int $ttl = null): bool 90 | { 91 | $ttl = $ttl ?? $this->seconds; 92 | 93 | if ($ttl <= 0) { 94 | return false; 95 | } 96 | 97 | if (! $this->isOwnedByCurrentProcess()) { 98 | return false; 99 | } 100 | 101 | $result = $this->store->set($this->name, $this->owner, $ttl) == true; 102 | 103 | if ($result) { 104 | $this->seconds = $ttl; 105 | $this->acquiredAt = microtime(true); 106 | } 107 | 108 | return $result; 109 | } 110 | 111 | /** 112 | * Returns the owner value written into the driver for this lock. 113 | * @return string 114 | */ 115 | protected function getCurrentOwner() 116 | { 117 | return $this->store->get($this->name); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Driver/RedisLock.php: -------------------------------------------------------------------------------- 1 | 'default', 'prefix' => ''], $constructor); 36 | if ($constructor['prefix']) { 37 | $this->name = ((string) $constructor['prefix']) . $this->name; 38 | } 39 | $this->store = make(RedisProxy::class, $constructor); 40 | } 41 | 42 | /** 43 | * Attempt to acquire the lock. 44 | */ 45 | #[Override] 46 | public function acquire(): bool 47 | { 48 | $result = false; 49 | 50 | if ($this->seconds > 0) { 51 | $result = $this->store->set($this->name, $this->owner, ['NX', 'EX' => $this->seconds]) == true; 52 | } else { 53 | $result = $this->store->setNX($this->name, $this->owner) === true; 54 | } 55 | 56 | if ($result) { 57 | $this->acquiredAt = microtime(true); 58 | } 59 | 60 | return $result; 61 | } 62 | 63 | /** 64 | * Release the lock. 65 | */ 66 | #[Override] 67 | public function release(): bool 68 | { 69 | $result = (bool) $this->store->eval(LuaScripts::releaseLock(), [$this->name, $this->owner], 1); 70 | 71 | if ($result) { 72 | $this->acquiredAt = null; 73 | } 74 | 75 | return $result; 76 | } 77 | 78 | /** 79 | * Releases this lock in disregard of ownership. 80 | */ 81 | #[Override] 82 | public function forceRelease(): void 83 | { 84 | $this->store->del($this->name); 85 | $this->acquiredAt = null; 86 | } 87 | 88 | /** 89 | * Refresh the lock expiration time. 90 | */ 91 | #[Override] 92 | public function refresh(?int $ttl = null): bool 93 | { 94 | $ttl = $ttl ?? $this->seconds; 95 | 96 | if ($ttl <= 0) { 97 | return false; 98 | } 99 | 100 | $result = (bool) $this->store->eval(LuaScripts::refreshLock(), [$this->name, $this->owner, $ttl], 1); 101 | 102 | if ($result) { 103 | $this->seconds = $ttl; 104 | $this->acquiredAt = microtime(true); 105 | } 106 | 107 | return $result; 108 | } 109 | 110 | /** 111 | * Returns the owner value written into the driver for this lock. 112 | * @return string 113 | */ 114 | protected function getCurrentOwner() 115 | { 116 | return $this->store->get($this->name); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Driver/CacheLock.php: -------------------------------------------------------------------------------- 1 | get(CacheManager::class); 35 | $constructor = array_merge(['driver' => 'default'], $constructor); 36 | $this->store = $cacheManager->getDriver($constructor['driver']); 37 | } 38 | 39 | /** 40 | * Attempt to acquire the lock. 41 | */ 42 | #[Override] 43 | public function acquire(): bool 44 | { 45 | if ($this->store->has($this->name)) { 46 | return false; 47 | } 48 | 49 | $result = $this->store->set($this->name, $this->owner, $this->seconds); 50 | 51 | if ($result) { 52 | $this->acquiredAt = microtime(true); 53 | } 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * Release the lock. 60 | */ 61 | #[Override] 62 | public function release(): bool 63 | { 64 | if ($this->isOwnedByCurrentProcess()) { 65 | $result = $this->store->delete($this->name); 66 | 67 | if ($result) { 68 | $this->acquiredAt = null; 69 | } 70 | 71 | return $result; 72 | } 73 | 74 | return false; 75 | } 76 | 77 | /** 78 | * Releases this lock regardless of ownership. 79 | */ 80 | #[Override] 81 | public function forceRelease(): void 82 | { 83 | $this->store->delete($this->name); 84 | $this->acquiredAt = null; 85 | } 86 | 87 | /** 88 | * Refresh the lock expiration time. 89 | */ 90 | #[Override] 91 | public function refresh(?int $ttl = null): bool 92 | { 93 | $ttl = $ttl ?? $this->seconds; 94 | 95 | if ($ttl <= 0) { 96 | return false; 97 | } 98 | 99 | if (! $this->isOwnedByCurrentProcess()) { 100 | return false; 101 | } 102 | 103 | $result = $this->store->set($this->name, $this->owner, $ttl); 104 | 105 | if ($result) { 106 | $this->seconds = $ttl; 107 | $this->acquiredAt = microtime(true); 108 | } 109 | 110 | return $result; 111 | } 112 | 113 | /** 114 | * Returns the owner value written into the driver for this lock. 115 | * @return string 116 | */ 117 | protected function getCurrentOwner() 118 | { 119 | return $this->store->get($this->name); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Driver/DatabaseLock.php: -------------------------------------------------------------------------------- 1 | 'default', 'table' => 'locks', 'prefix' => ''], $constructor); 34 | if ($constructor['prefix']) { 35 | $this->name = ((string) $constructor['prefix']) . $this->name; 36 | } 37 | $this->connection = Db::connection($constructor['pool']); 38 | $this->table = $constructor['table']; 39 | } 40 | 41 | /** 42 | * Attempt to acquire the lock. 43 | */ 44 | #[Override] 45 | public function acquire(): bool 46 | { 47 | $acquired = false; 48 | 49 | try { 50 | $this->connection->table($this->table)->insert([ 51 | 'key' => $this->name, 52 | 'owner' => $this->owner, 53 | 'expiration' => $this->expiresAt(), 54 | ]); 55 | 56 | $acquired = true; 57 | } catch (QueryException) { 58 | $updated = $this->connection->table($this->table) 59 | ->where('key', $this->name) 60 | ->where(fn ($query) => $query->where('owner', $this->owner)->orWhere('expiration', '<=', time())) 61 | ->update([ 62 | 'owner' => $this->owner, 63 | 'expiration' => $this->expiresAt(), 64 | ]); 65 | 66 | $acquired = $updated >= 1; 67 | } 68 | 69 | if ($acquired) { 70 | $this->acquiredAt = microtime(true); 71 | } 72 | 73 | return $acquired; 74 | } 75 | 76 | /** 77 | * Release the lock. 78 | */ 79 | #[Override] 80 | public function release(): bool 81 | { 82 | if ($this->isOwnedByCurrentProcess()) { 83 | $this->connection->table($this->table) 84 | ->where('key', $this->name) 85 | ->where('owner', $this->owner) 86 | ->delete(); 87 | 88 | $this->acquiredAt = null; 89 | 90 | return true; 91 | } 92 | 93 | return false; 94 | } 95 | 96 | /** 97 | * Releases this lock in disregard of ownership. 98 | */ 99 | #[Override] 100 | public function forceRelease(): void 101 | { 102 | $this->connection->table($this->table) 103 | ->where('key', $this->name) 104 | ->delete(); 105 | $this->acquiredAt = null; 106 | } 107 | 108 | /** 109 | * Refresh the lock expiration time. 110 | */ 111 | #[Override] 112 | public function refresh(?int $ttl = null): bool 113 | { 114 | $ttl = $ttl ?? $this->seconds; 115 | 116 | if ($ttl <= 0) { 117 | return false; 118 | } 119 | 120 | $updated = $this->connection->table($this->table) 121 | ->where('key', $this->name) 122 | ->where('owner', $this->owner) 123 | ->update([ 124 | 'expiration' => time() + $ttl, 125 | ]); 126 | 127 | if ($updated >= 1) { 128 | $this->seconds = $ttl; 129 | $this->acquiredAt = microtime(true); 130 | return true; 131 | } 132 | 133 | return false; 134 | } 135 | 136 | /** 137 | * Get the UNIX timestamp indicating when the lock should expire. 138 | */ 139 | protected function expiresAt(): int 140 | { 141 | return $this->seconds > 0 ? time() + $this->seconds : Carbon::now()->addDays(1)->getTimestamp(); 142 | } 143 | 144 | /** 145 | * Returns the owner value written into the driver for this lock. 146 | * @return string 147 | */ 148 | protected function getCurrentOwner() 149 | { 150 | return $this->connection->table($this->table)->where('key', $this->name)->first()?->owner; // @phpstan-ignore-line 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Driver/AbstractLock.php: -------------------------------------------------------------------------------- 1 | owner = $owner ?? Str::random(); 48 | } 49 | 50 | /** 51 | * Attempt to acquire the lock. 52 | */ 53 | abstract public function acquire(): bool; 54 | 55 | /** 56 | * Release the lock. 57 | */ 58 | abstract public function release(): bool; 59 | 60 | /** 61 | * Attempt to acquire the lock. 62 | * {@inheritdoc} 63 | */ 64 | #[Override] 65 | public function get(?callable $callback = null) 66 | { 67 | $result = $this->acquire(); 68 | 69 | if ($result && is_callable($callback)) { 70 | try { 71 | return $callback(); 72 | } finally { 73 | $this->release(); 74 | } 75 | } 76 | 77 | return $result; 78 | } 79 | 80 | /** 81 | * Attempt to acquire the lock for the given number of seconds. 82 | * {@inheritdoc} 83 | */ 84 | #[Override] 85 | public function block(int $seconds, ?callable $callback = null) 86 | { 87 | $starting = ((int) now()->format('Uu')) / 1000; 88 | $milliseconds = $seconds * 1000; 89 | 90 | while (! $this->acquire()) { 91 | $now = ((int) now()->format('Uu')) / 1000; 92 | 93 | if (($now + $this->sleepMilliseconds - $milliseconds) >= $starting) { 94 | throw new LockTimeoutException(); 95 | } 96 | 97 | usleep($this->sleepMilliseconds * 1000); 98 | } 99 | 100 | if (is_callable($callback)) { 101 | try { 102 | return $callback(); 103 | } finally { 104 | $this->release(); 105 | } 106 | } 107 | 108 | return true; 109 | } 110 | 111 | /** 112 | * Returns the current owner of the lock. 113 | */ 114 | public function owner(): string 115 | { 116 | return $this->owner; 117 | } 118 | 119 | /** 120 | * Specify the number of milliseconds to sleep in between blocked lock aquisition attempts. 121 | * @param int $milliseconds 122 | * @return $this 123 | */ 124 | public function betweenBlockedAttemptsSleepFor($milliseconds): self 125 | { 126 | $this->sleepMilliseconds = $milliseconds; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Determine whether this lock is owned by the given identifier. 133 | * 134 | * @param null|string $owner 135 | */ 136 | public function isOwnedBy($owner): bool 137 | { 138 | return $this->getCurrentOwner() === $owner; 139 | } 140 | 141 | /** 142 | * Refresh the lock expiration time. 143 | * {@inheritdoc} 144 | */ 145 | #[Override] 146 | abstract public function refresh(?int $ttl = null): bool; 147 | 148 | /** 149 | * Check if the lock has expired. 150 | * {@inheritdoc} 151 | */ 152 | #[Override] 153 | public function isExpired(): bool 154 | { 155 | if ($this->seconds <= 0) { 156 | return false; 157 | } 158 | 159 | if ($this->acquiredAt === null) { 160 | return true; 161 | } 162 | 163 | return microtime(true) >= ($this->acquiredAt + $this->seconds); 164 | } 165 | 166 | /** 167 | * Get the remaining lifetime of the lock in seconds. 168 | * {@inheritdoc} 169 | */ 170 | #[Override] 171 | public function getRemainingLifetime(): ?float 172 | { 173 | if ($this->seconds <= 0) { 174 | return null; 175 | } 176 | 177 | if ($this->acquiredAt === null) { 178 | return null; 179 | } 180 | 181 | $remaining = ($this->acquiredAt + $this->seconds) - microtime(true); 182 | 183 | return $remaining > 0 ? $remaining : 0.0; 184 | } 185 | 186 | /** 187 | * Returns the owner value written into the driver for this lock. 188 | */ 189 | abstract protected function getCurrentOwner(); 190 | 191 | /** 192 | * Determines whether this lock is allowed to release the lock in the driver. 193 | */ 194 | protected function isOwnedByCurrentProcess(): bool 195 | { 196 | return $this->isOwnedBy($this->owner); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Driver/CoroutineLock.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | protected static array $channels = []; 28 | 29 | /** 30 | * Mapping of channels to their current owners (used for ownership verification). 31 | * 32 | * @var null|WeakMap 33 | */ 34 | protected static ?WeakMap $owners = null; 35 | 36 | /** 37 | * Timer instance for scheduling lock expiration. 38 | */ 39 | protected static ?Timer $timer = null; 40 | 41 | /** 42 | * Mapping of channels to their timer IDs (for clearing expiration timers). 43 | * 44 | * @var null|WeakMap 45 | */ 46 | protected static ?WeakMap $timerIds = null; 47 | 48 | /** 49 | * Mapping of channels to their acquisition timestamps (for tracking expiration). 50 | * 51 | * @var null|WeakMap 52 | */ 53 | protected static ?WeakMap $acquiredTimes = null; 54 | 55 | /** 56 | * Mapping of channels to their TTL values (for tracking remaining lifetime). 57 | * 58 | * @var null|WeakMap 59 | */ 60 | protected static ?WeakMap $ttls = null; 61 | 62 | /** 63 | * Create a new lock instance. 64 | */ 65 | public function __construct( 66 | string $name, 67 | int $seconds, 68 | ?string $owner = null, 69 | array $constructor = [] 70 | ) { 71 | $constructor = array_merge(['prefix' => ''], $constructor); 72 | $name = $constructor['prefix'] . $name; 73 | 74 | parent::__construct($name, $seconds, $owner); 75 | 76 | self::$owners ??= new WeakMap(); 77 | self::$acquiredTimes ??= new WeakMap(); 78 | self::$ttls ??= new WeakMap(); 79 | self::$timer ??= new Timer(); 80 | self::$timerIds ??= new WeakMap(); 81 | } 82 | 83 | /** 84 | * Attempt to acquire the lock. 85 | */ 86 | #[Override] 87 | public function acquire(): bool 88 | { 89 | try { 90 | $chan = self::$channels[$this->name] ??= new Channel(1); 91 | 92 | if (! $chan->push(1, 0.01)) { 93 | return false; 94 | } 95 | 96 | self::$owners[$chan] = $this->owner; 97 | $this->acquiredAt = microtime(true); 98 | self::$acquiredTimes[$chan] = $this->acquiredAt; 99 | self::$ttls[$chan] = $this->seconds; 100 | 101 | if ($timeId = self::$timerIds[$chan] ?? null) { 102 | self::$timer?->clear((int) $timeId); 103 | } 104 | 105 | if ($this->seconds > 0) { 106 | $timeId = self::$timer?->after($this->seconds, fn () => $this->forceRelease()); 107 | $timeId && self::$timerIds[$chan] = $timeId; 108 | } 109 | } catch (Throwable) { 110 | return false; 111 | } 112 | 113 | return true; 114 | } 115 | 116 | /** 117 | * Release the lock. 118 | */ 119 | #[Override] 120 | public function release(): bool 121 | { 122 | if ($this->isOwnedByCurrentProcess()) { 123 | $result = (self::$channels[$this->name] ?? null)?->pop(0.01) ? true : false; 124 | 125 | if ($result) { 126 | $this->acquiredAt = null; 127 | } 128 | 129 | return $result; 130 | } 131 | 132 | return false; 133 | } 134 | 135 | /** 136 | * Releases this lock regardless of ownership. 137 | */ 138 | #[Override] 139 | public function forceRelease(): void 140 | { 141 | if (! $chan = self::$channels[$this->name] ?? null) { 142 | return; 143 | } 144 | 145 | self::$channels[$this->name] = null; 146 | $this->acquiredAt = null; 147 | 148 | $chan->close(); 149 | } 150 | 151 | /** 152 | * Refresh the lock expiration time. 153 | */ 154 | #[Override] 155 | public function refresh(?int $ttl = null): bool 156 | { 157 | $ttl = $ttl ?? $this->seconds; 158 | 159 | if ($ttl <= 0) { 160 | return false; 161 | } 162 | 163 | if (! $this->isOwnedByCurrentProcess()) { 164 | return false; 165 | } 166 | 167 | if (! $chan = self::$channels[$this->name] ?? null) { 168 | return false; 169 | } 170 | 171 | // Clear existing timer 172 | if ($timeId = self::$timerIds[$chan] ?? null) { 173 | self::$timer?->clear((int) $timeId); 174 | } 175 | 176 | // Update TTL and acquired time 177 | $this->seconds = $ttl; 178 | $this->acquiredAt = microtime(true); 179 | self::$acquiredTimes[$chan] = $this->acquiredAt; 180 | self::$ttls[$chan] = $ttl; 181 | 182 | // Set new timer 183 | $timeId = self::$timer?->after($ttl, fn () => $this->forceRelease()); 184 | $timeId && self::$timerIds[$chan] = $timeId; 185 | 186 | return true; 187 | } 188 | 189 | /** 190 | * Check if the lock has expired. 191 | */ 192 | #[Override] 193 | public function isExpired(): bool 194 | { 195 | if ($this->seconds <= 0) { 196 | return false; 197 | } 198 | 199 | if (! $chan = self::$channels[$this->name] ?? null) { 200 | return true; 201 | } 202 | 203 | $acquiredAt = self::$acquiredTimes[$chan] ?? null; 204 | $ttl = self::$ttls[$chan] ?? $this->seconds; 205 | 206 | if ($acquiredAt === null) { 207 | return true; 208 | } 209 | 210 | return microtime(true) >= ($acquiredAt + $ttl); 211 | } 212 | 213 | /** 214 | * Get the remaining lifetime of the lock in seconds. 215 | */ 216 | #[Override] 217 | public function getRemainingLifetime(): ?float 218 | { 219 | if ($this->seconds <= 0) { 220 | return null; 221 | } 222 | 223 | if (! $chan = self::$channels[$this->name] ?? null) { 224 | return null; 225 | } 226 | 227 | $acquiredAt = self::$acquiredTimes[$chan] ?? null; 228 | $ttl = self::$ttls[$chan] ?? $this->seconds; 229 | 230 | if ($acquiredAt === null) { 231 | return null; 232 | } 233 | 234 | $remaining = ($acquiredAt + $ttl) - microtime(true); 235 | 236 | return $remaining > 0 ? $remaining : 0.0; 237 | } 238 | 239 | /** 240 | * Returns the owner value written into the driver for this lock. 241 | * @return string 242 | */ 243 | protected function getCurrentOwner() 244 | { 245 | if (! $chan = self::$channels[$this->name] ?? null) { 246 | return ''; 247 | } 248 | 249 | return self::$owners[$chan] ?? ''; 250 | } 251 | } 252 | --------------------------------------------------------------------------------