From 8c9bc5d28c324306d134df894fedb1c0ad8ac608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 00:48:34 +0100 Subject: [PATCH 01/12] Rename RedisMutex to AbstractRedlockMutex --- phpstan.neon.dist | 6 ++--- ...edisMutex.php => AbstractRedlockMutex.php} | 4 ++-- src/Mutex/PHPRedisMutex.php | 2 +- src/Mutex/PredisMutex.php | 2 +- ...xTest.php => AbstractRedlockMutexTest.php} | 24 +++++++++---------- 5 files changed, 19 insertions(+), 19 deletions(-) rename src/Mutex/{RedisMutex.php => AbstractRedlockMutex.php} (97%) rename tests/Mutex/{RedisMutexTest.php => AbstractRedlockMutexTest.php} (92%) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5ea53027..0067cb07 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -12,7 +12,7 @@ parameters: ignoreErrors: # TODO - - path: 'src/Mutex/RedisMutex.php' + path: 'src/Mutex/AbstractRedlockMutex.php' identifier: if.condNotBoolean message: '~^Only booleans are allowed in an if condition, mixed given\.$~' count: 1 @@ -22,12 +22,12 @@ parameters: message: '~^Only booleans are allowed in an if condition, mixed given\.$~' count: 1 - - message: '~^Parameter #1 \$(redisAPI|redis) \(Redis\|RedisCluster\) of method Malkusch\\Lock\\Mutex\\PHPRedisMutex::(add|evalScript)\(\) should be contravariant with parameter \$redisAPI \(mixed\) of method Malkusch\\Lock\\Mutex\\RedisMutex::(add|evalScript)\(\)$~' + message: '~^Parameter #1 \$(redisAPI|redis) \(Redis\|RedisCluster\) of method Malkusch\\Lock\\Mutex\\PHPRedisMutex::(add|evalScript)\(\) should be contravariant with parameter \$redisAPI \(mixed\) of method Malkusch\\Lock\\Mutex\\AbstractRedlockMutex::(add|evalScript)\(\)$~' identifier: method.childParameterType path: 'src/Mutex/PHPRedisMutex.php' count: 2 - - message: '~^Parameter #1 \$(redisAPI|client) \(Predis\\ClientInterface\) of method Malkusch\\Lock\\Mutex\\PredisMutex::(add|evalScript)\(\) should be contravariant with parameter \$redisAPI \(mixed\) of method Malkusch\\Lock\\Mutex\\RedisMutex::(add|evalScript)\(\)$~' + message: '~^Parameter #1 \$(redisAPI|client) \(Predis\\ClientInterface\) of method Malkusch\\Lock\\Mutex\\PredisMutex::(add|evalScript)\(\) should be contravariant with parameter \$redisAPI \(mixed\) of method Malkusch\\Lock\\Mutex\\AbstractRedlockMutex::(add|evalScript)\(\)$~' identifier: method.childParameterType path: 'src/Mutex/PredisMutex.php' count: 2 diff --git a/src/Mutex/RedisMutex.php b/src/Mutex/AbstractRedlockMutex.php similarity index 97% rename from src/Mutex/RedisMutex.php rename to src/Mutex/AbstractRedlockMutex.php index f5c6b56c..b370ee77 100644 --- a/src/Mutex/RedisMutex.php +++ b/src/Mutex/AbstractRedlockMutex.php @@ -12,11 +12,11 @@ use Psr\Log\NullLogger; /** - * Mutex based on the Redlock algorithm. + * Distributed mutex based on the Redlock algorithm. * * @see http://redis.io/topics/distlock */ -abstract class RedisMutex extends AbstractSpinlockMutex implements LoggerAwareInterface +abstract class AbstractRedlockMutex extends AbstractSpinlockMutex implements LoggerAwareInterface { use LoggerAwareTrait; diff --git a/src/Mutex/PHPRedisMutex.php b/src/Mutex/PHPRedisMutex.php index f02096bb..86acee6b 100644 --- a/src/Mutex/PHPRedisMutex.php +++ b/src/Mutex/PHPRedisMutex.php @@ -17,7 +17,7 @@ * @see https://github.com/phpredis/phpredis/issues/1477 * @see http://redis.io/topics/distlock */ -class PHPRedisMutex extends RedisMutex +class PHPRedisMutex extends AbstractRedlockMutex { /** * Sets the connected Redis APIs. diff --git a/src/Mutex/PredisMutex.php b/src/Mutex/PredisMutex.php index 30030612..a0322c1a 100644 --- a/src/Mutex/PredisMutex.php +++ b/src/Mutex/PredisMutex.php @@ -14,7 +14,7 @@ * * @see http://redis.io/topics/distlock */ -class PredisMutex extends RedisMutex +class PredisMutex extends AbstractRedlockMutex { /** * Sets the Redis connections. diff --git a/tests/Mutex/RedisMutexTest.php b/tests/Mutex/AbstractRedlockMutexTest.php similarity index 92% rename from tests/Mutex/RedisMutexTest.php rename to tests/Mutex/AbstractRedlockMutexTest.php index 20ca0503..ac87c653 100644 --- a/tests/Mutex/RedisMutexTest.php +++ b/tests/Mutex/AbstractRedlockMutexTest.php @@ -8,7 +8,7 @@ use Malkusch\Lock\Exception\LockReleaseException; use Malkusch\Lock\Exception\MutexException; use Malkusch\Lock\Exception\TimeoutException; -use Malkusch\Lock\Mutex\RedisMutex; +use Malkusch\Lock\Mutex\AbstractRedlockMutex; use phpmock\environment\SleepEnvironmentBuilder; use phpmock\MockEnabledException; use phpmock\phpunit\PHPMock; @@ -21,7 +21,7 @@ * @group redis */ #[Group('redis')] -class RedisMutexTest extends TestCase +class AbstractRedlockMutexTest extends TestCase { use PHPMock; @@ -47,16 +47,16 @@ protected function setUp(): void /** * @param int $count The amount of redis apis * - * @return RedisMutex&MockObject + * @return AbstractRedlockMutex&MockObject */ - private function createRedisMutexMock(int $count, float $timeout = 1): RedisMutex + private function createAbstractRedlockMutexMock(int $count, float $timeout = 1): AbstractRedlockMutex { $redisAPIs = array_map( static fn ($id) => ['id' => $id], range(1, $count) ); - return $this->getMockBuilder(RedisMutex::class) + return $this->getMockBuilder(AbstractRedlockMutex::class) ->setConstructorArgs([$redisAPIs, 'test', $timeout]) ->onlyMethods(['add', 'evalScript']) ->getMock(); @@ -76,7 +76,7 @@ public function testTooFewServerToAcquire(int $count, int $available): void $this->expectException(LockAcquireException::class); $this->expectExceptionCode(MutexException::REDIS_NOT_ENOUGH_SERVERS); - $mutex = $this->createRedisMutexMock($count); + $mutex = $this->createAbstractRedlockMutexMock($count); $i = 0; $mutex->expects(self::exactly($count)) @@ -109,7 +109,7 @@ static function () use (&$i, $available): bool { #[DataProvider('provideMajorityCases')] public function testFaultTolerance(int $count, int $available): void { - $mutex = $this->createRedisMutexMock($count); + $mutex = $this->createAbstractRedlockMutexMock($count); $mutex->expects(self::exactly($count)) ->method('evalScript') ->willReturn(true); @@ -146,7 +146,7 @@ public function testAcquireTooFewKeys(int $count, int $available): void $this->expectException(TimeoutException::class); $this->expectExceptionMessage('Timeout of 1.0 seconds exceeded'); - $mutex = $this->createRedisMutexMock($count); + $mutex = $this->createAbstractRedlockMutexMock($count); $i = 0; $mutex->expects(self::any()) @@ -184,7 +184,7 @@ public function testTimingOut(int $count, float $timeout, float $delay): void $this->expectException(TimeoutException::class); $this->expectExceptionMessage('Timeout of ' . $timeoutStr . ' seconds exceeded'); - $mutex = $this->createRedisMutexMock($count, $timeout); + $mutex = $this->createAbstractRedlockMutexMock($count, $timeout); $mutex->expects(self::exactly($count)) ->method('add') @@ -219,7 +219,7 @@ public static function provideTimingOutCases(): iterable #[DataProvider('provideMajorityCases')] public function testAcquireWithMajority(int $count, int $available): void { - $mutex = $this->createRedisMutexMock($count); + $mutex = $this->createAbstractRedlockMutexMock($count); $mutex->expects(self::exactly($count)) ->method('evalScript') ->willReturn(true); @@ -249,7 +249,7 @@ static function () use (&$i, $available): bool { #[DataProvider('provideMinorityCases')] public function testTooFewServersToRelease(int $count, int $available): void { - $mutex = $this->createRedisMutexMock($count); + $mutex = $this->createAbstractRedlockMutexMock($count); $mutex->expects(self::exactly($count)) ->method('add') ->willReturn(true); @@ -285,7 +285,7 @@ static function () use (&$i, $available): bool { #[DataProvider('provideMinorityCases')] public function testReleaseTooFewKeys(int $count, int $available): void { - $mutex = $this->createRedisMutexMock($count); + $mutex = $this->createAbstractRedlockMutexMock($count); $mutex->expects(self::exactly($count)) ->method('add') ->willReturn(true); From d0ed5c7e364fc29d5cf09b7ec45728b061c238bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 00:58:23 +0100 Subject: [PATCH 02/12] prepare code to merge PredisMutex and PHPRedisMutex classes --- README.md | 36 +++++-------------- phpstan.neon.dist | 9 ++--- .../{PHPRedisMutex.php => RedisMutex.php} | 2 +- tests/Mutex/MutexConcurrencyTest.php | 7 ++-- tests/Mutex/MutexTest.php | 7 ++-- tests/Mutex/PredisMutexTest.php | 6 ++-- ...PRedisMutexTest.php => RedisMutexTest.php} | 8 ++--- 7 files changed, 24 insertions(+), 51 deletions(-) rename src/Mutex/{PHPRedisMutex.php => RedisMutex.php} (98%) rename tests/Mutex/{PHPRedisMutexTest.php => RedisMutexTest.php} (97%) diff --git a/README.md b/README.md index cb27b51e..31edf292 100644 --- a/README.md +++ b/README.md @@ -164,8 +164,7 @@ implementations or create/extend your own implementation. - [`FlockMutex`](#flockmutex) - [`MemcachedMutex`](#memcachedmutex) -- [`PHPRedisMutex`](#phpredismutex) -- [`PredisMutex`](#predismutex) +- [`RedisMutex`](#redismutex) - [`SemaphoreMutex`](#semaphoremutex) - [`TransactionalMutex`](#transactionalmutex) - [`MySQLMutex`](#mysqlmutex) @@ -213,11 +212,12 @@ $mutex->synchronized(function () use ($bankAccount, $amount) { }); ``` -#### PHPRedisMutex +#### RedisMutex -The **PHPRedisMutex** is the distributed lock implementation of -[RedLock](http://redis.io/topics/distlock) which uses the -[`phpredis` extension](https://github.com/phpredis/phpredis). +The **RedisMutex** is the distributed lock implementation of +[RedLock](http://redis.io/topics/distlock) which supports the +[`phpredis` extension](https://github.com/phpredis/phpredis) +or [`Predis` API](https://github.com/nrk/predis). This implementation requires at least `phpredis-2.2.4`. @@ -228,29 +228,9 @@ Example: ```php $redis = new \Redis(); $redis->connect('localhost'); +// OR $redis = new \Predis\Client('redis://localhost'); -$mutex = new PHPRedisMutex([$redis], 'balance'); -$mutex->synchronized(function () use ($bankAccount, $amount) { - $balance = $bankAccount->getBalance(); - $balance -= $amount; - if ($balance < 0) { - throw new \DomainException('You have no credit'); - } - $bankAccount->setBalance($balance); -}); -``` - -#### PredisMutex - -The **PredisMutex** is the distributed lock implementation of -[RedLock](http://redis.io/topics/distlock) which uses the -[`Predis` API](https://github.com/nrk/predis). - -Example: -```php -$redis = new \Predis\Client('redis://localhost'); - -$mutex = new PredisMutex([$redis], 'balance'); +$mutex = new RedisMutex([$redis], 'balance'); $mutex->synchronized(function () use ($bankAccount, $amount) { $balance = $bankAccount->getBalance(); $balance -= $amount; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0067cb07..b78cbdd3 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -22,14 +22,9 @@ parameters: message: '~^Only booleans are allowed in an if condition, mixed given\.$~' count: 1 - - message: '~^Parameter #1 \$(redisAPI|redis) \(Redis\|RedisCluster\) of method Malkusch\\Lock\\Mutex\\PHPRedisMutex::(add|evalScript)\(\) should be contravariant with parameter \$redisAPI \(mixed\) of method Malkusch\\Lock\\Mutex\\AbstractRedlockMutex::(add|evalScript)\(\)$~' + message: '~^Parameter #1 \$(redisAPI|redis) \(Redis\|RedisCluster\) of method Malkusch\\Lock\\Mutex\\RedisMutex::(add|evalScript)\(\) should be contravariant with parameter \$redisAPI \(mixed\) of method Malkusch\\Lock\\Mutex\\AbstractRedlockMutex::(add|evalScript)\(\)$~' identifier: method.childParameterType - path: 'src/Mutex/PHPRedisMutex.php' - count: 2 - - - message: '~^Parameter #1 \$(redisAPI|client) \(Predis\\ClientInterface\) of method Malkusch\\Lock\\Mutex\\PredisMutex::(add|evalScript)\(\) should be contravariant with parameter \$redisAPI \(mixed\) of method Malkusch\\Lock\\Mutex\\AbstractRedlockMutex::(add|evalScript)\(\)$~' - identifier: method.childParameterType - path: 'src/Mutex/PredisMutex.php' + path: 'src/Mutex/RedisMutex.php' count: 2 - path: 'tests/Mutex/*Test.php' diff --git a/src/Mutex/PHPRedisMutex.php b/src/Mutex/RedisMutex.php similarity index 98% rename from src/Mutex/PHPRedisMutex.php rename to src/Mutex/RedisMutex.php index 86acee6b..d9b72581 100644 --- a/src/Mutex/PHPRedisMutex.php +++ b/src/Mutex/RedisMutex.php @@ -17,7 +17,7 @@ * @see https://github.com/phpredis/phpredis/issues/1477 * @see http://redis.io/topics/distlock */ -class PHPRedisMutex extends AbstractRedlockMutex +class RedisMutex extends AbstractRedlockMutex { /** * Sets the connected Redis APIs. diff --git a/tests/Mutex/MutexConcurrencyTest.php b/tests/Mutex/MutexConcurrencyTest.php index 970ea195..e433a9f3 100644 --- a/tests/Mutex/MutexConcurrencyTest.php +++ b/tests/Mutex/MutexConcurrencyTest.php @@ -9,9 +9,8 @@ use Malkusch\Lock\Mutex\MemcachedMutex; use Malkusch\Lock\Mutex\Mutex; use Malkusch\Lock\Mutex\MySQLMutex; -use Malkusch\Lock\Mutex\PHPRedisMutex; +use Malkusch\Lock\Mutex\RedisMutex; use Malkusch\Lock\Mutex\PostgreSQLMutex; -use Malkusch\Lock\Mutex\PredisMutex; use Malkusch\Lock\Mutex\SemaphoreMutex; use Malkusch\Lock\Mutex\TransactionalMutex; use Malkusch\Lock\Util\LockUtil; @@ -284,7 +283,7 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable $uris ); - return new PredisMutex($clients, 'test', $timeout); + return new RedisMutex($clients, 'test', $timeout); }]; if (class_exists(\Redis::class)) { @@ -309,7 +308,7 @@ static function (string $uri): \Redis { $uris ); - return new PHPRedisMutex($apis, 'test', $timeout); + return new RedisMutex($apis, 'test', $timeout); }, ]; } diff --git a/tests/Mutex/MutexTest.php b/tests/Mutex/MutexTest.php index 58dd6b93..f1fcfd33 100644 --- a/tests/Mutex/MutexTest.php +++ b/tests/Mutex/MutexTest.php @@ -12,9 +12,8 @@ use Malkusch\Lock\Mutex\Mutex; use Malkusch\Lock\Mutex\MySQLMutex; use Malkusch\Lock\Mutex\NoMutex; -use Malkusch\Lock\Mutex\PHPRedisMutex; +use Malkusch\Lock\Mutex\RedisMutex; use Malkusch\Lock\Mutex\PostgreSQLMutex; -use Malkusch\Lock\Mutex\PredisMutex; use Malkusch\Lock\Mutex\SemaphoreMutex; use Malkusch\Lock\Mutex\TransactionalMutex; use org\bovigo\vfs\vfsStream; @@ -135,7 +134,7 @@ protected function unlock(): void {} $uris ); - return new PredisMutex($clients, 'test', self::TIMEOUT); + return new RedisMutex($clients, 'test', self::TIMEOUT); }]; if (class_exists(\Redis::class)) { @@ -160,7 +159,7 @@ static function ($uri) { $uris ); - return new PHPRedisMutex($apis, 'test', self::TIMEOUT); + return new RedisMutex($apis, 'test', self::TIMEOUT); }, ]; } diff --git a/tests/Mutex/PredisMutexTest.php b/tests/Mutex/PredisMutexTest.php index d1635325..f804fa8b 100644 --- a/tests/Mutex/PredisMutexTest.php +++ b/tests/Mutex/PredisMutexTest.php @@ -6,7 +6,7 @@ use Malkusch\Lock\Exception\LockAcquireException; use Malkusch\Lock\Exception\LockReleaseException; -use Malkusch\Lock\Mutex\PredisMutex; +use Malkusch\Lock\Mutex\RedisMutex; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Constraint\IsType; use PHPUnit\Framework\MockObject\MockObject; @@ -37,7 +37,7 @@ class PredisMutexTest extends TestCase /** @var ClientInterface&MockObject */ private $client; - /** @var PredisMutex */ + /** @var RedisMutex */ private $mutex; /** @var LoggerInterface&MockObject */ @@ -50,7 +50,7 @@ protected function setUp(): void $this->client = $this->createMock(ClientInterfaceWithSetAndEvalMethods::class); - $this->mutex = new PredisMutex([$this->client], 'test', 2.5); + $this->mutex = new RedisMutex([$this->client], 'test', 2.5); $this->logger = $this->createMock(LoggerInterface::class); $this->mutex->setLogger($this->logger); diff --git a/tests/Mutex/PHPRedisMutexTest.php b/tests/Mutex/RedisMutexTest.php similarity index 97% rename from tests/Mutex/PHPRedisMutexTest.php rename to tests/Mutex/RedisMutexTest.php index a616d872..e0cc5c52 100644 --- a/tests/Mutex/PHPRedisMutexTest.php +++ b/tests/Mutex/RedisMutexTest.php @@ -7,7 +7,7 @@ use Malkusch\Lock\Exception\LockAcquireException; use Malkusch\Lock\Exception\LockReleaseException; use Malkusch\Lock\Exception\MutexException; -use Malkusch\Lock\Mutex\PHPRedisMutex; +use Malkusch\Lock\Mutex\RedisMutex; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RequiresPhpExtension; @@ -68,12 +68,12 @@ public function set($key, $value, $options = null) */ #[RequiresPhpExtension('redis')] #[Group('redis')] -class PHPRedisMutexTest extends TestCase +class RedisMutexTest extends TestCase { /** @var \Redis[] */ private $connections = []; - /** @var PHPRedisMutex */ + /** @var RedisMutex */ private $mutex; #[\Override] @@ -150,7 +150,7 @@ private function _eval(string $script, array $args = [], int $numKeys = 0) $this->connections[] = $connection; } - $this->mutex = new PHPRedisMutex($this->connections, 'test'); + $this->mutex = new RedisMutex($this->connections, 'test'); } #[\Override] From a805f2050dc48ca1787d3bd10954cb65293f6aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 01:06:16 +0100 Subject: [PATCH 03/12] drop no longer relevant comments as PHP 7.4 is required --- README.md | 2 -- src/Mutex/RedisMutex.php | 5 ----- 2 files changed, 7 deletions(-) diff --git a/README.md b/README.md index 31edf292..409714ae 100644 --- a/README.md +++ b/README.md @@ -219,8 +219,6 @@ The **RedisMutex** is the distributed lock implementation of [`phpredis` extension](https://github.com/phpredis/phpredis) or [`Predis` API](https://github.com/nrk/predis). -This implementation requires at least `phpredis-2.2.4`. - If used with a cluster of Redis servers, acquiring and releasing locks will continue to function as long as a majority of the servers still works. diff --git a/src/Mutex/RedisMutex.php b/src/Mutex/RedisMutex.php index d9b72581..7a0ffa72 100644 --- a/src/Mutex/RedisMutex.php +++ b/src/Mutex/RedisMutex.php @@ -10,11 +10,6 @@ /** * Mutex based on the Redlock algorithm using the phpredis extension. * - * This implementation requires at least phpredis-4.0.0. If used together with - * the lzf extension, and phpredis is configured to use lzf compression, at - * least phpredis-4.3.0 is required! For reason, see github issue link. - * - * @see https://github.com/phpredis/phpredis/issues/1477 * @see http://redis.io/topics/distlock */ class RedisMutex extends AbstractRedlockMutex From d7cfae51ae71d0cc1c98e654ff0feb0d19400af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 01:10:37 +0100 Subject: [PATCH 04/12] Rename "redisApi" to "client" --- phpstan.neon.dist | 2 +- src/Mutex/AbstractRedlockMutex.php | 32 ++++++++++++------------ src/Mutex/PredisMutex.php | 6 ++--- src/Mutex/RedisMutex.php | 16 ++++++------ tests/Mutex/AbstractRedlockMutexTest.php | 4 +-- tests/Mutex/MutexConcurrencyTest.php | 2 +- tests/Mutex/MutexTest.php | 2 +- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b78cbdd3..1f64f61c 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -22,7 +22,7 @@ parameters: message: '~^Only booleans are allowed in an if condition, mixed given\.$~' count: 1 - - message: '~^Parameter #1 \$(redisAPI|redis) \(Redis\|RedisCluster\) of method Malkusch\\Lock\\Mutex\\RedisMutex::(add|evalScript)\(\) should be contravariant with parameter \$redisAPI \(mixed\) of method Malkusch\\Lock\\Mutex\\AbstractRedlockMutex::(add|evalScript)\(\)$~' + message: '~^Parameter #1 \$client \(Redis\|RedisCluster\) of method Malkusch\\Lock\\Mutex\\RedisMutex::(add|evalScript)\(\) should be contravariant with parameter \$client \(mixed\) of method Malkusch\\Lock\\Mutex\\AbstractRedlockMutex::(add|evalScript)\(\)$~' identifier: method.childParameterType path: 'src/Mutex/RedisMutex.php' count: 2 diff --git a/src/Mutex/AbstractRedlockMutex.php b/src/Mutex/AbstractRedlockMutex.php index b370ee77..5d58334c 100644 --- a/src/Mutex/AbstractRedlockMutex.php +++ b/src/Mutex/AbstractRedlockMutex.php @@ -24,21 +24,21 @@ abstract class AbstractRedlockMutex extends AbstractSpinlockMutex implements Log private $token; /** @var array */ - private $redisAPIs; + private $clients; /** * Sets the Redis APIs. * - * @param array $redisAPIs - * @param float $timeout The timeout in seconds a lock expires + * @param array $clients + * @param float $timeout The timeout in seconds a lock expires * * @throws \LengthException The timeout must be greater than 0 */ - public function __construct(array $redisAPIs, string $name, float $timeout = 3) + public function __construct(array $clients, string $name, float $timeout = 3) { parent::__construct($name, $timeout); - $this->redisAPIs = $redisAPIs; + $this->clients = $clients; $this->logger = new NullLogger(); } @@ -53,9 +53,9 @@ protected function acquire(string $key, float $expire): bool $errored = 0; $this->token = LockUtil::getInstance()->makeRandomToken(); $exception = null; - foreach ($this->redisAPIs as $index => $redisAPI) { + foreach ($this->clients as $index => $client) { try { - if ($this->add($redisAPI, $key, $this->token, $expire)) { + if ($this->add($client, $key, $this->token, $expire)) { ++$acquired; } } catch (LockAcquireException $exception) { @@ -85,7 +85,7 @@ protected function acquire(string $key, float $expire): bool $this->release($key); // In addition to RedLock it's an exception if too many servers fail. - if (!$this->isMajority(count($this->redisAPIs) - $errored)) { + if (!$this->isMajority(count($this->clients) - $errored)) { assert($exception !== null); // The last exception for some context. throw new LockAcquireException( @@ -114,9 +114,9 @@ protected function release(string $key): bool end '; $released = 0; - foreach ($this->redisAPIs as $index => $redisAPI) { + foreach ($this->clients as $index => $client) { try { - if ($this->evalScript($redisAPI, $script, 1, [$key, $this->token])) { + if ($this->evalScript($client, $script, 1, [$key, $this->token])) { ++$released; } } catch (LockReleaseException $e) { @@ -141,21 +141,21 @@ protected function release(string $key): bool */ private function isMajority(int $count): bool { - return $count > count($this->redisAPIs) / 2; + return $count > count($this->clients) / 2; } /** * Sets the key only if such key doesn't exist at the server yet. * - * @param mixed $redisAPI - * @param float $expire The TTL seconds + * @param mixed $client + * @param float $expire The TTL seconds * * @return bool True if the key was set */ - abstract protected function add($redisAPI, string $key, string $value, float $expire): bool; + abstract protected function add($client, string $key, string $value, float $expire): bool; /** - * @param mixed $redisAPI + * @param mixed $client * @param string $script The Lua script * @param int $numkeys The number of values in $arguments that represent Redis key names * @param list $arguments Keys and values @@ -164,5 +164,5 @@ abstract protected function add($redisAPI, string $key, string $value, float $ex * * @throws LockReleaseException An unexpected error happened */ - abstract protected function evalScript($redisAPI, string $script, int $numkeys, array $arguments); + abstract protected function evalScript($client, string $script, int $numkeys, array $arguments); } diff --git a/src/Mutex/PredisMutex.php b/src/Mutex/PredisMutex.php index a0322c1a..16eac78b 100644 --- a/src/Mutex/PredisMutex.php +++ b/src/Mutex/PredisMutex.php @@ -30,17 +30,17 @@ public function __construct(array $clients, string $name, float $timeout = 3) } /** - * @param ClientInterface $redisAPI + * @param ClientInterface $client * * @throws LockAcquireException */ #[\Override] - protected function add($redisAPI, string $key, string $value, float $expire): bool + protected function add($client, string $key, string $value, float $expire): bool { $expireMillis = (int) ceil($expire * 1000); try { - return $redisAPI->set($key, $value, 'PX', $expireMillis, 'NX') !== null; + return $client->set($key, $value, 'PX', $expireMillis, 'NX') !== null; } catch (PredisException $e) { $message = sprintf( 'Failed to acquire lock for key \'%s\'', diff --git a/src/Mutex/RedisMutex.php b/src/Mutex/RedisMutex.php index 7a0ffa72..86e94c0d 100644 --- a/src/Mutex/RedisMutex.php +++ b/src/Mutex/RedisMutex.php @@ -8,7 +8,7 @@ use Malkusch\Lock\Exception\LockReleaseException; /** - * Mutex based on the Redlock algorithm using the phpredis extension. + * Mutex based on the Redlock algorithm supporting the phpredis extension and Predis API. * * @see http://redis.io/topics/distlock */ @@ -20,29 +20,29 @@ class RedisMutex extends AbstractRedlockMutex * The Redis APIs needs to be connected. I.e. Redis::connect() was * called already. * - * @param array<\Redis|\RedisCluster> $redisAPIs - * @param float $timeout The timeout in seconds a lock expires + * @param array<\Redis|\RedisCluster> $clients + * @param float $timeout The timeout in seconds a lock expires * * @throws \LengthException The timeout must be greater than 0 */ - public function __construct(array $redisAPIs, string $name, float $timeout = 3) + public function __construct(array $clients, string $name, float $timeout = 3) { - parent::__construct($redisAPIs, $name, $timeout); + parent::__construct($clients, $name, $timeout); } /** - * @param \Redis|\RedisCluster $redisAPI + * @param \Redis|\RedisCluster $client * * @throws LockAcquireException */ #[\Override] - protected function add($redisAPI, string $key, string $value, float $expire): bool + protected function add($client, string $key, string $value, float $expire): bool { $expireMillis = (int) ceil($expire * 1000); try { // Will set the key, if it doesn't exist, with a ttl of $expire seconds - return $redisAPI->set($key, $value, ['nx', 'px' => $expireMillis]); + return $client->set($key, $value, ['nx', 'px' => $expireMillis]); } catch (\RedisException $e) { $message = sprintf( 'Failed to acquire lock for key \'%s\'', diff --git a/tests/Mutex/AbstractRedlockMutexTest.php b/tests/Mutex/AbstractRedlockMutexTest.php index ac87c653..9630df01 100644 --- a/tests/Mutex/AbstractRedlockMutexTest.php +++ b/tests/Mutex/AbstractRedlockMutexTest.php @@ -51,13 +51,13 @@ protected function setUp(): void */ private function createAbstractRedlockMutexMock(int $count, float $timeout = 1): AbstractRedlockMutex { - $redisAPIs = array_map( + $clients = array_map( static fn ($id) => ['id' => $id], range(1, $count) ); return $this->getMockBuilder(AbstractRedlockMutex::class) - ->setConstructorArgs([$redisAPIs, 'test', $timeout]) + ->setConstructorArgs([$clients, 'test', $timeout]) ->onlyMethods(['add', 'evalScript']) ->getMock(); } diff --git a/tests/Mutex/MutexConcurrencyTest.php b/tests/Mutex/MutexConcurrencyTest.php index e433a9f3..d79dc973 100644 --- a/tests/Mutex/MutexConcurrencyTest.php +++ b/tests/Mutex/MutexConcurrencyTest.php @@ -9,8 +9,8 @@ use Malkusch\Lock\Mutex\MemcachedMutex; use Malkusch\Lock\Mutex\Mutex; use Malkusch\Lock\Mutex\MySQLMutex; -use Malkusch\Lock\Mutex\RedisMutex; use Malkusch\Lock\Mutex\PostgreSQLMutex; +use Malkusch\Lock\Mutex\RedisMutex; use Malkusch\Lock\Mutex\SemaphoreMutex; use Malkusch\Lock\Mutex\TransactionalMutex; use Malkusch\Lock\Util\LockUtil; diff --git a/tests/Mutex/MutexTest.php b/tests/Mutex/MutexTest.php index f1fcfd33..090b73a5 100644 --- a/tests/Mutex/MutexTest.php +++ b/tests/Mutex/MutexTest.php @@ -12,8 +12,8 @@ use Malkusch\Lock\Mutex\Mutex; use Malkusch\Lock\Mutex\MySQLMutex; use Malkusch\Lock\Mutex\NoMutex; -use Malkusch\Lock\Mutex\RedisMutex; use Malkusch\Lock\Mutex\PostgreSQLMutex; +use Malkusch\Lock\Mutex\RedisMutex; use Malkusch\Lock\Mutex\SemaphoreMutex; use Malkusch\Lock\Mutex\TransactionalMutex; use org\bovigo\vfs\vfsStream; From 831024c321d99b8346faf11b8877516823e0594d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 01:20:05 +0100 Subject: [PATCH 05/12] make AbstractRedlockMutex generic --- src/Mutex/AbstractRedlockMutex.php | 14 ++++++++------ src/Mutex/RedisMutex.php | 14 +++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Mutex/AbstractRedlockMutex.php b/src/Mutex/AbstractRedlockMutex.php index 5d58334c..38902a4c 100644 --- a/src/Mutex/AbstractRedlockMutex.php +++ b/src/Mutex/AbstractRedlockMutex.php @@ -14,6 +14,8 @@ /** * Distributed mutex based on the Redlock algorithm. * + * @template TClient of object + * * @see http://redis.io/topics/distlock */ abstract class AbstractRedlockMutex extends AbstractSpinlockMutex implements LoggerAwareInterface @@ -23,14 +25,14 @@ abstract class AbstractRedlockMutex extends AbstractSpinlockMutex implements Log /** @var string The random value token for key identification */ private $token; - /** @var array */ + /** @var array */ private $clients; /** * Sets the Redis APIs. * - * @param array $clients - * @param float $timeout The timeout in seconds a lock expires + * @param array $clients + * @param float $timeout The timeout in seconds a lock expires * * @throws \LengthException The timeout must be greater than 0 */ @@ -147,15 +149,15 @@ private function isMajority(int $count): bool /** * Sets the key only if such key doesn't exist at the server yet. * - * @param mixed $client - * @param float $expire The TTL seconds + * @param TClient $client + * @param float $expire The TTL seconds * * @return bool True if the key was set */ abstract protected function add($client, string $key, string $value, float $expire): bool; /** - * @param mixed $client + * @param TClient $client * @param string $script The Lua script * @param int $numkeys The number of values in $arguments that represent Redis key names * @param list $arguments Keys and values diff --git a/src/Mutex/RedisMutex.php b/src/Mutex/RedisMutex.php index 86e94c0d..a1967632 100644 --- a/src/Mutex/RedisMutex.php +++ b/src/Mutex/RedisMutex.php @@ -6,10 +6,15 @@ use Malkusch\Lock\Exception\LockAcquireException; use Malkusch\Lock\Exception\LockReleaseException; +use Predis\ClientInterface as PredisClientInterface; /** * Mutex based on the Redlock algorithm supporting the phpredis extension and Predis API. * + * @phpstan-type TClient \Redis|\RedisCluster|PredisClientInterface + * + * @extends AbstractRedlockMutex + * * @see http://redis.io/topics/distlock */ class RedisMutex extends AbstractRedlockMutex @@ -20,8 +25,8 @@ class RedisMutex extends AbstractRedlockMutex * The Redis APIs needs to be connected. I.e. Redis::connect() was * called already. * - * @param array<\Redis|\RedisCluster> $clients - * @param float $timeout The timeout in seconds a lock expires + * @param array $clients + * @param float $timeout The timeout in seconds a lock expires * * @throws \LengthException The timeout must be greater than 0 */ @@ -31,8 +36,6 @@ public function __construct(array $clients, string $name, float $timeout = 3) } /** - * @param \Redis|\RedisCluster $client - * * @throws LockAcquireException */ #[\Override] @@ -53,9 +56,6 @@ protected function add($client, string $key, string $value, float $expire): bool } } - /** - * @param \Redis|\RedisCluster $redis - */ #[\Override] protected function evalScript($redis, string $script, int $numkeys, array $arguments) { From 10da865e67d4f4a6183b69615be2c3952152a892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 01:25:14 +0100 Subject: [PATCH 06/12] fix phpstan --- phpstan.neon.dist | 5 ----- src/Mutex/PredisMutex.php | 15 +++++++-------- tests/Mutex/AbstractRedlockMutexTest.php | 2 +- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1f64f61c..0b72517d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -21,11 +21,6 @@ parameters: identifier: if.condNotBoolean message: '~^Only booleans are allowed in an if condition, mixed given\.$~' count: 1 - - - message: '~^Parameter #1 \$client \(Redis\|RedisCluster\) of method Malkusch\\Lock\\Mutex\\RedisMutex::(add|evalScript)\(\) should be contravariant with parameter \$client \(mixed\) of method Malkusch\\Lock\\Mutex\\AbstractRedlockMutex::(add|evalScript)\(\)$~' - identifier: method.childParameterType - path: 'src/Mutex/RedisMutex.php' - count: 2 - path: 'tests/Mutex/*Test.php' identifier: empty.notAllowed diff --git a/src/Mutex/PredisMutex.php b/src/Mutex/PredisMutex.php index 16eac78b..8832cff6 100644 --- a/src/Mutex/PredisMutex.php +++ b/src/Mutex/PredisMutex.php @@ -6,12 +6,16 @@ use Malkusch\Lock\Exception\LockAcquireException; use Malkusch\Lock\Exception\LockReleaseException; -use Predis\ClientInterface; +use Predis\ClientInterface as PredisClientInterface; use Predis\PredisException; /** * Mutex based on the Redlock algorithm using the Predis API. * + * @phpstan-type TClient PredisClientInterface + * + * @extends AbstractRedlockMutex + * * @see http://redis.io/topics/distlock */ class PredisMutex extends AbstractRedlockMutex @@ -19,8 +23,8 @@ class PredisMutex extends AbstractRedlockMutex /** * Sets the Redis connections. * - * @param ClientInterface[] $clients The Redis clients - * @param float $timeout The timeout in seconds a lock expires + * @param array $clients The Redis clients + * @param float $timeout The timeout in seconds a lock expires * * @throws \LengthException The timeout must be greater than 0 */ @@ -30,8 +34,6 @@ public function __construct(array $clients, string $name, float $timeout = 3) } /** - * @param ClientInterface $client - * * @throws LockAcquireException */ #[\Override] @@ -51,9 +53,6 @@ protected function add($client, string $key, string $value, float $expire): bool } } - /** - * @param ClientInterface $client - */ #[\Override] protected function evalScript($client, string $script, int $numkeys, array $arguments) { diff --git a/tests/Mutex/AbstractRedlockMutexTest.php b/tests/Mutex/AbstractRedlockMutexTest.php index 9630df01..969df73a 100644 --- a/tests/Mutex/AbstractRedlockMutexTest.php +++ b/tests/Mutex/AbstractRedlockMutexTest.php @@ -47,7 +47,7 @@ protected function setUp(): void /** * @param int $count The amount of redis apis * - * @return AbstractRedlockMutex&MockObject + * @return AbstractRedlockMutex&MockObject */ private function createAbstractRedlockMutexMock(int $count, float $timeout = 1): AbstractRedlockMutex { From 083fbfbaddc8701834c4bc2cea6c4533f6be9a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 01:37:49 +0100 Subject: [PATCH 07/12] Move PredisMutex impl. into RedisMutex impl. --- src/Mutex/AbstractRedlockMutex.php | 3 + src/Mutex/PredisMutex.php | 65 ----------------- src/Mutex/RedisMutex.php | 108 ++++++++++++++++------------- 3 files changed, 64 insertions(+), 112 deletions(-) delete mode 100644 src/Mutex/PredisMutex.php diff --git a/src/Mutex/AbstractRedlockMutex.php b/src/Mutex/AbstractRedlockMutex.php index 38902a4c..248b17bd 100644 --- a/src/Mutex/AbstractRedlockMutex.php +++ b/src/Mutex/AbstractRedlockMutex.php @@ -31,6 +31,9 @@ abstract class AbstractRedlockMutex extends AbstractSpinlockMutex implements Log /** * Sets the Redis APIs. * + * The Redis APIs needs to be connected. I.e. Redis::connect() was + * called already. + * * @param array $clients * @param float $timeout The timeout in seconds a lock expires * diff --git a/src/Mutex/PredisMutex.php b/src/Mutex/PredisMutex.php deleted file mode 100644 index 8832cff6..00000000 --- a/src/Mutex/PredisMutex.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * @see http://redis.io/topics/distlock - */ -class PredisMutex extends AbstractRedlockMutex -{ - /** - * Sets the Redis connections. - * - * @param array $clients The Redis clients - * @param float $timeout The timeout in seconds a lock expires - * - * @throws \LengthException The timeout must be greater than 0 - */ - public function __construct(array $clients, string $name, float $timeout = 3) - { - parent::__construct($clients, $name, $timeout); - } - - /** - * @throws LockAcquireException - */ - #[\Override] - protected function add($client, string $key, string $value, float $expire): bool - { - $expireMillis = (int) ceil($expire * 1000); - - try { - return $client->set($key, $value, 'PX', $expireMillis, 'NX') !== null; - } catch (PredisException $e) { - $message = sprintf( - 'Failed to acquire lock for key \'%s\'', - $key - ); - - throw new LockAcquireException($message, 0, $e); - } - } - - #[\Override] - protected function evalScript($client, string $script, int $numkeys, array $arguments) - { - try { - return $client->eval($script, $numkeys, ...$arguments); - } catch (PredisException $e) { - throw new LockReleaseException('Failed to release lock', 0, $e); - } - } -} diff --git a/src/Mutex/RedisMutex.php b/src/Mutex/RedisMutex.php index a1967632..c08cb399 100644 --- a/src/Mutex/RedisMutex.php +++ b/src/Mutex/RedisMutex.php @@ -7,6 +7,7 @@ use Malkusch\Lock\Exception\LockAcquireException; use Malkusch\Lock\Exception\LockReleaseException; use Predis\ClientInterface as PredisClientInterface; +use Predis\PredisException; /** * Mutex based on the Redlock algorithm supporting the phpredis extension and Predis API. @@ -20,19 +21,15 @@ class RedisMutex extends AbstractRedlockMutex { /** - * Sets the connected Redis APIs. - * - * The Redis APIs needs to be connected. I.e. Redis::connect() was - * called already. - * - * @param array $clients - * @param float $timeout The timeout in seconds a lock expires - * - * @throws \LengthException The timeout must be greater than 0 + * @param TClient $client */ - public function __construct(array $clients, string $name, float $timeout = 3) + private function isClientPHPRedis($client): bool { - parent::__construct($clients, $name, $timeout); + $res = $client instanceof \Redis || $client instanceof \RedisCluster; + + \assert($res === !$client instanceof PredisClientInterface); + + return $res; } /** @@ -43,60 +40,77 @@ protected function add($client, string $key, string $value, float $expire): bool { $expireMillis = (int) ceil($expire * 1000); - try { - // Will set the key, if it doesn't exist, with a ttl of $expire seconds - return $client->set($key, $value, ['nx', 'px' => $expireMillis]); - } catch (\RedisException $e) { - $message = sprintf( - 'Failed to acquire lock for key \'%s\'', - $key - ); + if ($this->isClientPHPRedis($client)) { + try { + // Will set the key, if it doesn't exist, with a ttl of $expire seconds + return $client->set($key, $value, ['nx', 'px' => $expireMillis]); + } catch (\RedisException $e) { + $message = sprintf( + 'Failed to acquire lock for key \'%s\'', + $key + ); + + throw new LockAcquireException($message, 0, $e); + } + } else { + try { + return $client->set($key, $value, 'PX', $expireMillis, 'NX') !== null; + } catch (PredisException $e) { + $message = sprintf( + 'Failed to acquire lock for key \'%s\'', + $key + ); - throw new LockAcquireException($message, 0, $e); + throw new LockAcquireException($message, 0, $e); + } } } #[\Override] - protected function evalScript($redis, string $script, int $numkeys, array $arguments) + protected function evalScript($client, string $script, int $numkeys, array $arguments) { - for ($i = $numkeys; $i < count($arguments); ++$i) { - /* - * If a serialization mode such as "php" or "igbinary" is enabled, the arguments must be - * serialized by us, because phpredis does not do this for the eval command. - * - * The keys must not be serialized. - */ - $arguments[$i] = $redis->_serialize($arguments[$i]); + if ($this->isClientPHPRedis($client)) { + for ($i = $numkeys; $i < count($arguments); ++$i) { + /* + * If a serialization mode such as "php" or "igbinary" is enabled, the arguments must be + * serialized by us, because phpredis does not do this for the eval command. + * + * The keys must not be serialized. + */ + $arguments[$i] = $client->_serialize($arguments[$i]); - /* - * If LZF compression is enabled for the redis connection and the runtime has the LZF - * extension installed, compress the arguments as the final step. - */ - if ($this->hasLzfCompression($redis)) { - $arguments[$i] = lzf_compress($arguments[$i]); + /* + * If LZF compression is enabled for the redis connection and the runtime has the LZF + * extension installed, compress the arguments as the final step. + */ + if ($this->isLzfCompressionEnabled($client)) { + $arguments[$i] = lzf_compress($arguments[$i]); + } } - } - try { - return $redis->eval($script, $arguments, $numkeys); - } catch (\RedisException $e) { - throw new LockReleaseException('Failed to release lock', 0, $e); + try { + return $client->eval($script, $arguments, $numkeys); + } catch (\RedisException $e) { + throw new LockReleaseException('Failed to release lock', 0, $e); + } + } else { + try { + return $client->eval($script, $numkeys, ...$arguments); + } catch (PredisException $e) { + throw new LockReleaseException('Failed to release lock', 0, $e); + } } } /** - * Determines if lzf compression is enabled for the given connection. - * - * @param \Redis|\RedisCluster $redis - * - * @return bool True if lzf compression is enabled, false otherwise + * @param \Redis|\RedisCluster $client */ - private function hasLzfCompression($redis): bool + private function isLzfCompressionEnabled($client): bool { if (!\defined('Redis::COMPRESSION_LZF')) { return false; } - return $redis->getOption(\Redis::OPT_COMPRESSION) === \Redis::COMPRESSION_LZF; + return $client->getOption(\Redis::OPT_COMPRESSION) === \Redis::COMPRESSION_LZF; } } From 86dc3319657be13f7a1bb01b6afde07a0f6dd9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 01:42:32 +0100 Subject: [PATCH 08/12] minor test improvements --- tests/Mutex/MutexConcurrencyTest.php | 8 ++++---- tests/Mutex/MutexTest.php | 8 ++++---- ...redisMutexTest.php => RedisMutexWithPredisTest.php} | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) rename tests/Mutex/{PredisMutexTest.php => RedisMutexWithPredisTest.php} (92%) diff --git a/tests/Mutex/MutexConcurrencyTest.php b/tests/Mutex/MutexConcurrencyTest.php index d79dc973..fd4753ee 100644 --- a/tests/Mutex/MutexConcurrencyTest.php +++ b/tests/Mutex/MutexConcurrencyTest.php @@ -17,7 +17,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Constraint\IsType; use PHPUnit\Framework\TestCase; -use Predis\Client; +use Predis\Client as PredisClient; use Spatie\Async\Pool; /** @@ -277,9 +277,9 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable if (getenv('REDIS_URIS')) { $uris = explode(',', getenv('REDIS_URIS')); - yield 'PredisMutex' => [static function ($timeout) use ($uris): Mutex { + yield 'RedisMutex /w Predis' => [static function ($timeout) use ($uris): Mutex { $clients = array_map( - static fn ($uri) => new Client($uri), + static fn ($uri) => new PredisClient($uri), $uris ); @@ -287,7 +287,7 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable }]; if (class_exists(\Redis::class)) { - yield 'PHPRedisMutex' => [ + yield 'RedisMutex /w PHPRedis' => [ static function ($timeout) use ($uris): Mutex { $apis = array_map( static function (string $uri): \Redis { diff --git a/tests/Mutex/MutexTest.php b/tests/Mutex/MutexTest.php index 090b73a5..135c5092 100644 --- a/tests/Mutex/MutexTest.php +++ b/tests/Mutex/MutexTest.php @@ -20,7 +20,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\TestCase; -use Predis\Client; +use Predis\Client as PredisClient; /** * If you want to run integrations tests you should provide these environment variables: @@ -128,9 +128,9 @@ protected function unlock(): void {} if (getenv('REDIS_URIS')) { $uris = explode(',', getenv('REDIS_URIS')); - yield 'PredisMutex' => [static function () use ($uris): Mutex { + yield 'RedisMutex /w Predis' => [static function () use ($uris): Mutex { $clients = array_map( - static fn ($uri) => new Client($uri), + static fn ($uri) => new PredisClient($uri), $uris ); @@ -138,7 +138,7 @@ protected function unlock(): void {} }]; if (class_exists(\Redis::class)) { - yield 'PHPRedisMutex' => [ + yield 'RedisMutex /w PHPRedis' => [ static function () use ($uris): Mutex { $apis = array_map( static function ($uri) { diff --git a/tests/Mutex/PredisMutexTest.php b/tests/Mutex/RedisMutexWithPredisTest.php similarity index 92% rename from tests/Mutex/PredisMutexTest.php rename to tests/Mutex/RedisMutexWithPredisTest.php index f804fa8b..aee09421 100644 --- a/tests/Mutex/PredisMutexTest.php +++ b/tests/Mutex/RedisMutexWithPredisTest.php @@ -11,11 +11,11 @@ use PHPUnit\Framework\Constraint\IsType; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Predis\ClientInterface; +use Predis\ClientInterface as PredisClientInterface; use Predis\PredisException; use Psr\Log\LoggerInterface; -interface ClientInterfaceWithSetAndEvalMethods extends ClientInterface +interface PredisClientInterfaceWithSetAndEvalMethods extends PredisClientInterface { /** * @return mixed @@ -32,9 +32,9 @@ public function set(); * @group redis */ #[Group('redis')] -class PredisMutexTest extends TestCase +class RedisMutexWithPredisTest extends TestCase { - /** @var ClientInterface&MockObject */ + /** @var PredisClientInterface&MockObject */ private $client; /** @var RedisMutex */ @@ -48,7 +48,7 @@ protected function setUp(): void { parent::setUp(); - $this->client = $this->createMock(ClientInterfaceWithSetAndEvalMethods::class); + $this->client = $this->createMock(PredisClientInterfaceWithSetAndEvalMethods::class); $this->mutex = new RedisMutex([$this->client], 'test', 2.5); From 478bab1bbc680ba9e2b0f5a21d2d6a911854a6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 01:49:58 +0100 Subject: [PATCH 09/12] minor improvements --- README.md | 2 +- src/Mutex/RedisMutex.php | 2 +- tests/Mutex/AbstractRedlockMutexTest.php | 18 +++++++++--------- tests/Mutex/AbstractSpinlockMutexTest.php | 14 +++++++------- tests/Mutex/MutexConcurrencyTest.php | 4 ++-- tests/Mutex/MutexTest.php | 4 ++-- tests/Mutex/RedisMutexTest.php | 6 +++--- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 409714ae..882ac816 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ extension if possible or busy waiting if not. #### MemcachedMutex The **MemcachedMutex** is a spinlock implementation which uses the -[`Memcached` API](http://php.net/manual/en/book.memcached.php). +[`Memcached` extension](http://php.net/manual/en/book.memcached.php). Example: ```php diff --git a/src/Mutex/RedisMutex.php b/src/Mutex/RedisMutex.php index c08cb399..f9c4eea8 100644 --- a/src/Mutex/RedisMutex.php +++ b/src/Mutex/RedisMutex.php @@ -10,7 +10,7 @@ use Predis\PredisException; /** - * Mutex based on the Redlock algorithm supporting the phpredis extension and Predis API. + * Distributed mutex based on the Redlock algorithm supporting the phpredis extension and Predis API. * * @phpstan-type TClient \Redis|\RedisCluster|PredisClientInterface * diff --git a/tests/Mutex/AbstractRedlockMutexTest.php b/tests/Mutex/AbstractRedlockMutexTest.php index 969df73a..20dbf963 100644 --- a/tests/Mutex/AbstractRedlockMutexTest.php +++ b/tests/Mutex/AbstractRedlockMutexTest.php @@ -45,11 +45,11 @@ protected function setUp(): void } /** - * @param int $count The amount of redis apis + * @param int $count The amount of redis APIs * * @return AbstractRedlockMutex&MockObject */ - private function createAbstractRedlockMutexMock(int $count, float $timeout = 1): AbstractRedlockMutex + private function createRedlockMutexMock(int $count, float $timeout = 1): AbstractRedlockMutex { $clients = array_map( static fn ($id) => ['id' => $id], @@ -76,7 +76,7 @@ public function testTooFewServerToAcquire(int $count, int $available): void $this->expectException(LockAcquireException::class); $this->expectExceptionCode(MutexException::REDIS_NOT_ENOUGH_SERVERS); - $mutex = $this->createAbstractRedlockMutexMock($count); + $mutex = $this->createRedlockMutexMock($count); $i = 0; $mutex->expects(self::exactly($count)) @@ -109,7 +109,7 @@ static function () use (&$i, $available): bool { #[DataProvider('provideMajorityCases')] public function testFaultTolerance(int $count, int $available): void { - $mutex = $this->createAbstractRedlockMutexMock($count); + $mutex = $this->createRedlockMutexMock($count); $mutex->expects(self::exactly($count)) ->method('evalScript') ->willReturn(true); @@ -146,7 +146,7 @@ public function testAcquireTooFewKeys(int $count, int $available): void $this->expectException(TimeoutException::class); $this->expectExceptionMessage('Timeout of 1.0 seconds exceeded'); - $mutex = $this->createAbstractRedlockMutexMock($count); + $mutex = $this->createRedlockMutexMock($count); $i = 0; $mutex->expects(self::any()) @@ -184,7 +184,7 @@ public function testTimingOut(int $count, float $timeout, float $delay): void $this->expectException(TimeoutException::class); $this->expectExceptionMessage('Timeout of ' . $timeoutStr . ' seconds exceeded'); - $mutex = $this->createAbstractRedlockMutexMock($count, $timeout); + $mutex = $this->createRedlockMutexMock($count, $timeout); $mutex->expects(self::exactly($count)) ->method('add') @@ -219,7 +219,7 @@ public static function provideTimingOutCases(): iterable #[DataProvider('provideMajorityCases')] public function testAcquireWithMajority(int $count, int $available): void { - $mutex = $this->createAbstractRedlockMutexMock($count); + $mutex = $this->createRedlockMutexMock($count); $mutex->expects(self::exactly($count)) ->method('evalScript') ->willReturn(true); @@ -249,7 +249,7 @@ static function () use (&$i, $available): bool { #[DataProvider('provideMinorityCases')] public function testTooFewServersToRelease(int $count, int $available): void { - $mutex = $this->createAbstractRedlockMutexMock($count); + $mutex = $this->createRedlockMutexMock($count); $mutex->expects(self::exactly($count)) ->method('add') ->willReturn(true); @@ -285,7 +285,7 @@ static function () use (&$i, $available): bool { #[DataProvider('provideMinorityCases')] public function testReleaseTooFewKeys(int $count, int $available): void { - $mutex = $this->createAbstractRedlockMutexMock($count); + $mutex = $this->createRedlockMutexMock($count); $mutex->expects(self::exactly($count)) ->method('add') ->willReturn(true); diff --git a/tests/Mutex/AbstractSpinlockMutexTest.php b/tests/Mutex/AbstractSpinlockMutexTest.php index 926219e9..05758faa 100644 --- a/tests/Mutex/AbstractSpinlockMutexTest.php +++ b/tests/Mutex/AbstractSpinlockMutexTest.php @@ -41,7 +41,7 @@ protected function setUp(): void /** * @return AbstractSpinlockMutex&MockObject */ - private function createAbstractSpinlockMutexMock(float $timeout = 3): AbstractSpinlockMutex + private function createSpinlockMutexMock(float $timeout = 3): AbstractSpinlockMutex { return $this->getMockBuilder(AbstractSpinlockMutex::class) ->setConstructorArgs(['test', $timeout]) @@ -56,7 +56,7 @@ public function testFailAcquireLock(): void { $this->expectException(LockAcquireException::class); - $mutex = $this->createAbstractSpinlockMutexMock(); + $mutex = $this->createSpinlockMutexMock(); $mutex->expects(self::any()) ->method('acquire') ->willThrowException(new LockAcquireException()); @@ -74,7 +74,7 @@ public function testAcquireTimesOut(): void $this->expectException(TimeoutException::class); $this->expectExceptionMessage('Timeout of 3.0 seconds exceeded'); - $mutex = $this->createAbstractSpinlockMutexMock(); + $mutex = $this->createSpinlockMutexMock(); $mutex->expects(self::atLeastOnce()) ->method('acquire') ->willReturn(false); @@ -89,7 +89,7 @@ public function testAcquireTimesOut(): void */ public function testExecuteTooLong(): void { - $mutex = $this->createAbstractSpinlockMutexMock(0.5); + $mutex = $this->createSpinlockMutexMock(0.5); $mutex->expects(self::any()) ->method('acquire') ->willReturn(true); @@ -114,7 +114,7 @@ public function testExecuteTooLong(): void */ public function testExecuteBarelySucceeds(): void { - $mutex = $this->createAbstractSpinlockMutexMock(0.5); + $mutex = $this->createSpinlockMutexMock(0.5); $mutex->expects(self::any())->method('acquire')->willReturn(true); $mutex->expects(self::once())->method('release')->willReturn(true); @@ -130,7 +130,7 @@ public function testFailReleasingLock(): void { $this->expectException(LockReleaseException::class); - $mutex = $this->createAbstractSpinlockMutexMock(); + $mutex = $this->createSpinlockMutexMock(); $mutex->expects(self::any())->method('acquire')->willReturn(true); $mutex->expects(self::any())->method('release')->willReturn(false); @@ -142,7 +142,7 @@ public function testFailReleasingLock(): void */ public function testExecuteTimeoutLeavesOneSecondForKeyToExpire(): void { - $mutex = $this->createAbstractSpinlockMutexMock(0.2); + $mutex = $this->createSpinlockMutexMock(0.2); $mutex->expects(self::once()) ->method('acquire') ->with(self::anything(), 1.2) diff --git a/tests/Mutex/MutexConcurrencyTest.php b/tests/Mutex/MutexConcurrencyTest.php index fd4753ee..f91e70cb 100644 --- a/tests/Mutex/MutexConcurrencyTest.php +++ b/tests/Mutex/MutexConcurrencyTest.php @@ -289,7 +289,7 @@ public static function provideExecutionIsSerializedWhenLockedCases(): iterable if (class_exists(\Redis::class)) { yield 'RedisMutex /w PHPRedis' => [ static function ($timeout) use ($uris): Mutex { - $apis = array_map( + $clients = array_map( static function (string $uri): \Redis { $redis = new \Redis(); @@ -308,7 +308,7 @@ static function (string $uri): \Redis { $uris ); - return new RedisMutex($apis, 'test', $timeout); + return new RedisMutex($clients, 'test', $timeout); }, ]; } diff --git a/tests/Mutex/MutexTest.php b/tests/Mutex/MutexTest.php index 135c5092..6898464b 100644 --- a/tests/Mutex/MutexTest.php +++ b/tests/Mutex/MutexTest.php @@ -140,7 +140,7 @@ protected function unlock(): void {} if (class_exists(\Redis::class)) { yield 'RedisMutex /w PHPRedis' => [ static function () use ($uris): Mutex { - $apis = array_map( + $clients = array_map( static function ($uri) { $redis = new \Redis(); @@ -159,7 +159,7 @@ static function ($uri) { $uris ); - return new RedisMutex($apis, 'test', self::TIMEOUT); + return new RedisMutex($clients, 'test', self::TIMEOUT); }, ]; } diff --git a/tests/Mutex/RedisMutexTest.php b/tests/Mutex/RedisMutexTest.php index e0cc5c52..38e285cb 100644 --- a/tests/Mutex/RedisMutexTest.php +++ b/tests/Mutex/RedisMutexTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; if (\PHP_MAJOR_VERSION >= 8) { - trait RedisTestTrait + trait RedisCompatibilityTrait { /** * @param list $args @@ -35,7 +35,7 @@ public function set($key, $value, $options = null): /* \Redis|string| */ bool } } } else { - trait RedisTestTrait + trait RedisCompatibilityTrait { /** * @return mixed @@ -92,7 +92,7 @@ protected function setUp(): void // original Redis::set and Redis::eval calls will reopen the connection $connection = new class extends \Redis { - use RedisTestTrait; + use RedisCompatibilityTrait; /** @var bool */ private $is_closed = false; From 4c1aec72c2b35a0a72acb8eb6d2b64541d47c7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 02:03:55 +0100 Subject: [PATCH 10/12] improve phpstan analysis --- src/Mutex/RedisMutex.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mutex/RedisMutex.php b/src/Mutex/RedisMutex.php index f9c4eea8..8f6f1089 100644 --- a/src/Mutex/RedisMutex.php +++ b/src/Mutex/RedisMutex.php @@ -22,6 +22,8 @@ class RedisMutex extends AbstractRedlockMutex { /** * @param TClient $client + * + * @phpstan-assert-if-true \Redis|\RedisCluster $client */ private function isClientPHPRedis($client): bool { From f7ebba010d34d946b667e25fba6aaf67cbbc165d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 02:09:02 +0100 Subject: [PATCH 11/12] improve README --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 882ac816..39fa0623 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ [![Build Status](https://github.com/php-lock/lock/actions/workflows/test-unit.yml/badge.svg?branch=master)](https://github.com/php-lock/lock/actions?query=branch:master) [![License](https://poser.pugx.org/malkusch/lock/license)](https://packagist.org/packages/malkusch/lock) -This library helps executing critical code in concurrent situations. +This library helps executing critical code in concurrent situations in serialized fashion. -php-lock/lock follows semantic versioning. Read more on [semver.org][1]. +php-lock/lock follows [semantic versioning][1]. ---- @@ -219,6 +219,8 @@ The **RedisMutex** is the distributed lock implementation of [`phpredis` extension](https://github.com/phpredis/phpredis) or [`Predis` API](https://github.com/nrk/predis). +Both Redis and Valkey servers are supported. + If used with a cluster of Redis servers, acquiring and releasing locks will continue to function as long as a majority of the servers still works. @@ -294,6 +296,8 @@ The **MySQLMutex** uses MySQL's [`GET_LOCK`](https://dev.mysql.com/doc/refman/9.0/en/locking-functions.html#function_get-lock) function. +Both MySQL and MariaDB servers are supported. + It supports timeouts. If the connection to the database server is lost or interrupted, the lock is automatically released. From eb1bb6c74f752c49d5b775b44b8a2e31bb7f180d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 7 Dec 2024 02:20:51 +0100 Subject: [PATCH 12/12] add authors to README --- LICENSE | 2 +- README.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 62167823..b146be6c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2024 Willem Stuursma-Ruwen +Copyright (c) 2024 Markus Malkusch, Willem Stuursma-Ruwen, Michael Voříšek and GitHub contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 39fa0623..b3169bb3 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,14 @@ $mutex->synchronized(function () use ($bankAccount, $amount) { }); ``` +## Authors + +Since year 2015 the development was led by Markus Malkusch, Willem Stuursma-Ruwen, Michael Voříšek and many GitHub contributors. + +Currently this library is maintained by Michael Voříšek - [GitHub][https://github.com/mvorisek] and [LinkedIn][https://www.linkedin.com/mvorisek]. + +Commercial support is available. + ## License This project is free and is licensed under the MIT.