From a9f8890607471bd7c9ef14c6f45f37398d911c74 Mon Sep 17 00:00:00 2001 From: Colemanator Date: Tue, 8 Jul 2025 11:18:39 +1000 Subject: [PATCH 01/10] Add Timeout Support to PostgreSQLMutex --- README.md | 5 ++-- src/Mutex/PostgreSQLMutex.php | 27 +++++++++++++++-- tests/Mutex/PostgreSQLMutexTest.php | 46 ++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 38e7a113..cc84195c 100644 --- a/README.md +++ b/README.md @@ -248,8 +248,9 @@ functions. Named locks are offered. PostgreSQL locking functions require integers but the conversion is handled automatically. -No timeouts are supported. If the connection to the database server is lost or -interrupted, the lock is automatically released. +Timeouts are optional, if not provided, it will block indefinitely. If the +connection to the database server is lost or interrupted, the lock is +automatically released. ```php $pdo = new \PDO('pgsql:host=localhost;dbname=test', 'username'); diff --git a/src/Mutex/PostgreSQLMutex.php b/src/Mutex/PostgreSQLMutex.php index 8982a805..e7a0e7e3 100644 --- a/src/Mutex/PostgreSQLMutex.php +++ b/src/Mutex/PostgreSQLMutex.php @@ -5,6 +5,7 @@ namespace Malkusch\Lock\Mutex; use Malkusch\Lock\Util\LockUtil; +use Malkusch\Lock\Util\Loop; class PostgreSQLMutex extends AbstractLockMutex { @@ -13,9 +14,12 @@ class PostgreSQLMutex extends AbstractLockMutex /** @var array{int, int} */ private array $key; - public function __construct(\PDO $PDO, string $name) + private ?float $timeout; + + public function __construct(\PDO $PDO, string $name, ?float $timeout = null) { $this->pdo = $PDO; + $this->timeout = $timeout; [$keyBytes1, $keyBytes2] = str_split(md5(LockUtil::getInstance()->getKeyPrefix() . ':' . $name, true), 4); @@ -35,8 +39,13 @@ public function __construct(\PDO $PDO, string $name) #[\Override] protected function lock(): void { - $statement = $this->pdo->prepare('SELECT pg_advisory_lock(?, ?)'); + if (isset($this->timeout)) { + $this->tryLock(); + + return; + } + $statement = $this->pdo->prepare('SELECT pg_advisory_lock(?, ?)'); $statement->execute($this->key); } @@ -46,4 +55,18 @@ protected function unlock(): void $statement = $this->pdo->prepare('SELECT pg_advisory_unlock(?, ?)'); $statement->execute($this->key); } + + protected function tryLock(): void + { + $loop = new Loop(); + + $loop->execute(function () use ($loop): void { + $statement = $this->pdo->prepare('SELECT pg_try_advisory_lock(?, ?)'); + $statement->execute($this->key); + + if ($statement->fetchColumn()) { + $loop->end(); + } + }, $this->timeout); + } } diff --git a/tests/Mutex/PostgreSQLMutexTest.php b/tests/Mutex/PostgreSQLMutexTest.php index c2e0fc31..1ee2f5f1 100644 --- a/tests/Mutex/PostgreSQLMutexTest.php +++ b/tests/Mutex/PostgreSQLMutexTest.php @@ -4,6 +4,8 @@ namespace Malkusch\Lock\Tests\Mutex; +use Eloquent\Liberator\Liberator; +use Malkusch\Lock\Exception\LockAcquireTimeoutException; use Malkusch\Lock\Mutex\PostgreSQLMutex; use PHPUnit\Framework\Constraint\IsType; use PHPUnit\Framework\MockObject\MockObject; @@ -24,7 +26,7 @@ protected function setUp(): void $this->pdo = $this->createMock(\PDO::class); - $this->mutex = new PostgreSQLMutex($this->pdo, 'test-one-negative-key'); + $this->mutex = Liberator::liberate(new PostgreSQLMutex($this->pdo, 'test-one-negative-key')); // @phpstan-ignore assign.propertyType } private function isPhpunit9x(): bool @@ -97,4 +99,46 @@ public function testReleaseLock(): void \Closure::bind(static fn ($mutex) => $mutex->unlock(), null, PostgreSQLMutex::class)($this->mutex); } + + public function testAcquireTimeoutOccurs(): void + { + $statement = $this->createMock(\PDOStatement::class); + + $this->pdo->expects(self::atLeastOnce()) + ->method('prepare') + ->with('SELECT pg_try_advisory_lock(?, ?)') + ->willReturn($statement); + + $statement->expects(self::atLeastOnce()) + ->method('execute') + ->with(self::logicalAnd( + new IsType(IsType::TYPE_ARRAY), + self::countOf(2), + self::callback(function (...$arguments) { + if ($this->isPhpunit9x()) { // https://github.com/sebastianbergmann/phpunit/issues/5891 + $arguments = $arguments[0]; + } + + foreach ($arguments as $v) { + self::assertLessThan(1 << 32, $v); + self::assertGreaterThanOrEqual(-(1 << 32), $v); + self::assertIsInt($v); + } + + return true; + }), + [533558444, -1716795572] + )); + + $statement->expects(self::atLeastOnce()) + ->method('fetchColumn') + ->willReturn(false); + + $this->mutex->timeout = 1.0; // @phpstan-ignore property.private + + $this->expectException(LockAcquireTimeoutException::class); + $this->expectExceptionMessage('Lock acquire timeout of 1.0 seconds has been exceeded'); + + \Closure::bind(static fn ($mutex) => $mutex->lock(), null, PostgreSQLMutex::class)($this->mutex); + } } From 7ee091ea66aaec7e606f2ab8ba08707adb6ceb76 Mon Sep 17 00:00:00 2001 From: Colemanator Date: Fri, 11 Jul 2025 12:58:11 +1000 Subject: [PATCH 02/10] Mimic Existing Naming/Defaults Convenction --- src/Mutex/PostgreSQLMutex.php | 10 +++++----- tests/Mutex/PostgreSQLMutexTest.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mutex/PostgreSQLMutex.php b/src/Mutex/PostgreSQLMutex.php index e7a0e7e3..c053a012 100644 --- a/src/Mutex/PostgreSQLMutex.php +++ b/src/Mutex/PostgreSQLMutex.php @@ -14,12 +14,12 @@ class PostgreSQLMutex extends AbstractLockMutex /** @var array{int, int} */ private array $key; - private ?float $timeout; + private float $acquireTimeout; - public function __construct(\PDO $PDO, string $name, ?float $timeout = null) + public function __construct(\PDO $PDO, string $name, float $acquireTimeout = \INF) { $this->pdo = $PDO; - $this->timeout = $timeout; + $this->acquireTimeout = $acquireTimeout; [$keyBytes1, $keyBytes2] = str_split(md5(LockUtil::getInstance()->getKeyPrefix() . ':' . $name, true), 4); @@ -39,7 +39,7 @@ public function __construct(\PDO $PDO, string $name, ?float $timeout = null) #[\Override] protected function lock(): void { - if (isset($this->timeout)) { + if ($this->acquireTimeout !== \INF) { $this->tryLock(); return; @@ -67,6 +67,6 @@ protected function tryLock(): void if ($statement->fetchColumn()) { $loop->end(); } - }, $this->timeout); + }, $this->acquireTimeout); } } diff --git a/tests/Mutex/PostgreSQLMutexTest.php b/tests/Mutex/PostgreSQLMutexTest.php index 1ee2f5f1..dc358a61 100644 --- a/tests/Mutex/PostgreSQLMutexTest.php +++ b/tests/Mutex/PostgreSQLMutexTest.php @@ -134,7 +134,7 @@ public function testAcquireTimeoutOccurs(): void ->method('fetchColumn') ->willReturn(false); - $this->mutex->timeout = 1.0; // @phpstan-ignore property.private + $this->mutex->acquireTimeout = 1.0; // @phpstan-ignore property.private $this->expectException(LockAcquireTimeoutException::class); $this->expectExceptionMessage('Lock acquire timeout of 1.0 seconds has been exceeded'); From 6488a3dd2309188e4a5c55856a4e183b4036c646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 27 Jul 2025 03:27:57 +0200 Subject: [PATCH 03/10] unify README with MySQL --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc84195c..f4c16a8d 100644 --- a/README.md +++ b/README.md @@ -248,9 +248,8 @@ functions. Named locks are offered. PostgreSQL locking functions require integers but the conversion is handled automatically. -Timeouts are optional, if not provided, it will block indefinitely. If the -connection to the database server is lost or interrupted, the lock is -automatically released. +It supports timeouts. If the connection to the database server is lost or +interrupted, the lock is automatically released. ```php $pdo = new \PDO('pgsql:host=localhost;dbname=test', 'username'); From 9593a625d40b7d98e9ef4a7fb93e0e41a0dea5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 27 Jul 2025 03:28:30 +0200 Subject: [PATCH 04/10] a little more unification with FlockMutex --- src/Mutex/PostgreSQLMutex.php | 40 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/Mutex/PostgreSQLMutex.php b/src/Mutex/PostgreSQLMutex.php index c053a012..c844aa3f 100644 --- a/src/Mutex/PostgreSQLMutex.php +++ b/src/Mutex/PostgreSQLMutex.php @@ -36,27 +36,7 @@ public function __construct(\PDO $PDO, string $name, float $acquireTimeout = \IN ]; } - #[\Override] - protected function lock(): void - { - if ($this->acquireTimeout !== \INF) { - $this->tryLock(); - - return; - } - - $statement = $this->pdo->prepare('SELECT pg_advisory_lock(?, ?)'); - $statement->execute($this->key); - } - - #[\Override] - protected function unlock(): void - { - $statement = $this->pdo->prepare('SELECT pg_advisory_unlock(?, ?)'); - $statement->execute($this->key); - } - - protected function tryLock(): void + private function lockBusy(): void { $loop = new Loop(); @@ -69,4 +49,22 @@ protected function tryLock(): void } }, $this->acquireTimeout); } + + #[\Override] + protected function lock(): void + { + if ($this->acquireTimeout === \INF) { + $statement = $this->pdo->prepare('SELECT pg_advisory_lock(?, ?)'); + $statement->execute($this->key); + } else { + $this->lockBusy(); + } + } + + #[\Override] + protected function unlock(): void + { + $statement = $this->pdo->prepare('SELECT pg_advisory_unlock(?, ?)'); + $statement->execute($this->key); + } } From 0671e99616dddf758e17141c09cc1557e862f324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sun, 27 Jul 2025 03:36:48 +0200 Subject: [PATCH 05/10] unify cs --- tests/Mutex/PostgreSQLMutexTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Mutex/PostgreSQLMutexTest.php b/tests/Mutex/PostgreSQLMutexTest.php index dc358a61..4cfe932b 100644 --- a/tests/Mutex/PostgreSQLMutexTest.php +++ b/tests/Mutex/PostgreSQLMutexTest.php @@ -138,7 +138,6 @@ public function testAcquireTimeoutOccurs(): void $this->expectException(LockAcquireTimeoutException::class); $this->expectExceptionMessage('Lock acquire timeout of 1.0 seconds has been exceeded'); - \Closure::bind(static fn ($mutex) => $mutex->lock(), null, PostgreSQLMutex::class)($this->mutex); } } From 4c0ade6d7d53288207bd472956c6a8b4bd80b5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 28 Jul 2025 01:00:05 +0200 Subject: [PATCH 06/10] improve non-pr related cs --- src/Mutex/MySQLMutex.php | 4 ++-- src/Mutex/PostgreSQLMutex.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mutex/MySQLMutex.php b/src/Mutex/MySQLMutex.php index d21cd63a..07d48e83 100644 --- a/src/Mutex/MySQLMutex.php +++ b/src/Mutex/MySQLMutex.php @@ -20,9 +20,9 @@ class MySQLMutex extends AbstractLockMutex /** * @param float $acquireTimeout In seconds */ - public function __construct(\PDO $PDO, string $name, float $acquireTimeout = 0) + public function __construct(\PDO $pdo, string $name, float $acquireTimeout = 0) { - $this->pdo = $PDO; + $this->pdo = $pdo; $namePrefix = LockUtil::getInstance()->getKeyPrefix() . ':'; diff --git a/src/Mutex/PostgreSQLMutex.php b/src/Mutex/PostgreSQLMutex.php index c844aa3f..3375c22d 100644 --- a/src/Mutex/PostgreSQLMutex.php +++ b/src/Mutex/PostgreSQLMutex.php @@ -16,9 +16,9 @@ class PostgreSQLMutex extends AbstractLockMutex private float $acquireTimeout; - public function __construct(\PDO $PDO, string $name, float $acquireTimeout = \INF) + public function __construct(\PDO $pdo, string $name, float $acquireTimeout = \INF) { - $this->pdo = $PDO; + $this->pdo = $pdo; $this->acquireTimeout = $acquireTimeout; [$keyBytes1, $keyBytes2] = str_split(md5(LockUtil::getInstance()->getKeyPrefix() . ':' . $name, true), 4); From 66ccc72accbb6d0aea067cd87f8177026d9d791b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 28 Jul 2025 01:15:12 +0200 Subject: [PATCH 07/10] unify phpdoc timeout comments --- src/Mutex/FlockMutex.php | 1 + src/Mutex/PostgreSQLMutex.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/Mutex/FlockMutex.php b/src/Mutex/FlockMutex.php index f04b3093..b0908c33 100644 --- a/src/Mutex/FlockMutex.php +++ b/src/Mutex/FlockMutex.php @@ -24,6 +24,7 @@ class FlockMutex extends AbstractLockMutex /** @var resource */ private $fileHandle; + /** In seconds */ private float $acquireTimeout; /** @var self::STRATEGY_* */ diff --git a/src/Mutex/PostgreSQLMutex.php b/src/Mutex/PostgreSQLMutex.php index 3375c22d..eb3dac9a 100644 --- a/src/Mutex/PostgreSQLMutex.php +++ b/src/Mutex/PostgreSQLMutex.php @@ -14,8 +14,12 @@ class PostgreSQLMutex extends AbstractLockMutex /** @var array{int, int} */ private array $key; + /** In seconds */ private float $acquireTimeout; + /** + * @param float $acquireTimeout In seconds + */ public function __construct(\PDO $pdo, string $name, float $acquireTimeout = \INF) { $this->pdo = $pdo; From b03be68c135fec12ee588bb4c931ef413738b116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 28 Jul 2025 01:16:53 +0200 Subject: [PATCH 08/10] minor cs --- src/Mutex/PostgreSQLMutex.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Mutex/PostgreSQLMutex.php b/src/Mutex/PostgreSQLMutex.php index eb3dac9a..7a892977 100644 --- a/src/Mutex/PostgreSQLMutex.php +++ b/src/Mutex/PostgreSQLMutex.php @@ -40,6 +40,12 @@ public function __construct(\PDO $pdo, string $name, float $acquireTimeout = \IN ]; } + private function lockBlocking(): void + { + $statement = $this->pdo->prepare('SELECT pg_advisory_lock(?, ?)'); + $statement->execute($this->key); + } + private function lockBusy(): void { $loop = new Loop(); @@ -58,8 +64,7 @@ private function lockBusy(): void protected function lock(): void { if ($this->acquireTimeout === \INF) { - $statement = $this->pdo->prepare('SELECT pg_advisory_lock(?, ?)'); - $statement->execute($this->key); + $this->lockBlocking(); } else { $this->lockBusy(); } From b9378844247d3fe5c8c9f4960f63a42fd1ddfbb3 Mon Sep 17 00:00:00 2001 From: Colemanator Date: Mon, 28 Jul 2025 10:10:10 +1000 Subject: [PATCH 09/10] improve test coverage --- tests/Mutex/MutexConcurrencyTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Mutex/MutexConcurrencyTest.php b/tests/Mutex/MutexConcurrencyTest.php index 76a39e9d..aaae4ce6 100644 --- a/tests/Mutex/MutexConcurrencyTest.php +++ b/tests/Mutex/MutexConcurrencyTest.php @@ -268,6 +268,13 @@ static function (string $uri): \Redis { return new PostgreSQLMutex($pdo, 'test'); }]; + + yield 'PostgreSQLMutexWithTimeout' => [static function ($timeout) { + $pdo = new \PDO(getenv('PGSQL_DSN'), getenv('PGSQL_USER'), getenv('PGSQL_PASSWORD')); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + return new PostgreSQLMutex($pdo, 'test', $timeout); + }]; } } } From ff3d3d248afdb077377e254683b70e36dc542e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Mon, 28 Jul 2025 11:19:59 +0200 Subject: [PATCH 10/10] MutexTest instead, concurrency test coverage is broken (when using fork) --- tests/Mutex/MutexConcurrencyTest.php | 7 ------- tests/Mutex/MutexTest.php | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Mutex/MutexConcurrencyTest.php b/tests/Mutex/MutexConcurrencyTest.php index aaae4ce6..76a39e9d 100644 --- a/tests/Mutex/MutexConcurrencyTest.php +++ b/tests/Mutex/MutexConcurrencyTest.php @@ -268,13 +268,6 @@ static function (string $uri): \Redis { return new PostgreSQLMutex($pdo, 'test'); }]; - - yield 'PostgreSQLMutexWithTimeout' => [static function ($timeout) { - $pdo = new \PDO(getenv('PGSQL_DSN'), getenv('PGSQL_USER'), getenv('PGSQL_PASSWORD')); - $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - - return new PostgreSQLMutex($pdo, 'test', $timeout); - }]; } } } diff --git a/tests/Mutex/MutexTest.php b/tests/Mutex/MutexTest.php index 4cad7268..626c9624 100644 --- a/tests/Mutex/MutexTest.php +++ b/tests/Mutex/MutexTest.php @@ -240,6 +240,13 @@ static function ($uri) { return new PostgreSQLMutex($pdo, 'test'); }]; + + yield 'PostgreSQLMutexWithTimoutLoop' => [static function () { + $pdo = new \PDO(getenv('PGSQL_DSN'), getenv('PGSQL_USER'), getenv('PGSQL_PASSWORD')); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + + return new PostgreSQLMutex($pdo, 'test', 3); + }]; } } }