Skip to content

Add Timeout Support to PostgreSQLMutex #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
27 changes: 25 additions & 2 deletions src/Mutex/PostgreSQLMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Malkusch\Lock\Mutex;

use Malkusch\Lock\Util\LockUtil;
use Malkusch\Lock\Util\Loop;

class PostgreSQLMutex extends AbstractLockMutex
{
Expand All @@ -13,9 +14,12 @@ class PostgreSQLMutex extends AbstractLockMutex
/** @var array{int, int} */
private array $key;

public function __construct(\PDO $PDO, string $name)
private float $acquireTimeout;

public function __construct(\PDO $PDO, string $name, float $acquireTimeout = \INF)
{
$this->pdo = $PDO;
$this->acquireTimeout = $acquireTimeout;

[$keyBytes1, $keyBytes2] = str_split(md5(LockUtil::getInstance()->getKeyPrefix() . ':' . $name, true), 4);

Expand All @@ -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 ($this->acquireTimeout !== \INF) {
$this->tryLock();

return;
}

$statement = $this->pdo->prepare('SELECT pg_advisory_lock(?, ?)');
$statement->execute($this->key);
}

Expand All @@ -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->acquireTimeout);
}
}
46 changes: 45 additions & 1 deletion tests/Mutex/PostgreSQLMutexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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->acquireTimeout = 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);
}
}