Skip to content

Commit 03f890a

Browse files
Add acquire timeout support to PostgreSQLMutex (#80)
Co-authored-by: Michael Voříšek <mvorisek@mvorisek.cz>
1 parent f1eb0bd commit 03f890a

File tree

6 files changed

+90
-9
lines changed

6 files changed

+90
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ functions.
248248
Named locks are offered. PostgreSQL locking functions require integers but the
249249
conversion is handled automatically.
250250

251-
No timeouts are supported. If the connection to the database server is lost or
251+
It supports timeouts. If the connection to the database server is lost or
252252
interrupted, the lock is automatically released.
253253

254254
```php

src/Mutex/FlockMutex.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class FlockMutex extends AbstractLockMutex
2424
/** @var resource */
2525
private $fileHandle;
2626

27+
/** In seconds */
2728
private float $acquireTimeout;
2829

2930
/** @var self::STRATEGY_* */

src/Mutex/MySQLMutex.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ class MySQLMutex extends AbstractLockMutex
2020
/**
2121
* @param float $acquireTimeout In seconds
2222
*/
23-
public function __construct(\PDO $PDO, string $name, float $acquireTimeout = 0)
23+
public function __construct(\PDO $pdo, string $name, float $acquireTimeout = 0)
2424
{
25-
$this->pdo = $PDO;
25+
$this->pdo = $pdo;
2626

2727
$namePrefix = LockUtil::getInstance()->getKeyPrefix() . ':';
2828

src/Mutex/PostgreSQLMutex.php

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Malkusch\Lock\Mutex;
66

77
use Malkusch\Lock\Util\LockUtil;
8+
use Malkusch\Lock\Util\Loop;
89

910
class PostgreSQLMutex extends AbstractLockMutex
1011
{
@@ -13,9 +14,16 @@ class PostgreSQLMutex extends AbstractLockMutex
1314
/** @var array{int, int} */
1415
private array $key;
1516

16-
public function __construct(\PDO $PDO, string $name)
17+
/** In seconds */
18+
private float $acquireTimeout;
19+
20+
/**
21+
* @param float $acquireTimeout In seconds
22+
*/
23+
public function __construct(\PDO $pdo, string $name, float $acquireTimeout = \INF)
1724
{
18-
$this->pdo = $PDO;
25+
$this->pdo = $pdo;
26+
$this->acquireTimeout = $acquireTimeout;
1927

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

@@ -32,14 +40,36 @@ public function __construct(\PDO $PDO, string $name)
3240
];
3341
}
3442

35-
#[\Override]
36-
protected function lock(): void
43+
private function lockBlocking(): void
3744
{
3845
$statement = $this->pdo->prepare('SELECT pg_advisory_lock(?, ?)');
39-
4046
$statement->execute($this->key);
4147
}
4248

49+
private function lockBusy(): void
50+
{
51+
$loop = new Loop();
52+
53+
$loop->execute(function () use ($loop): void {
54+
$statement = $this->pdo->prepare('SELECT pg_try_advisory_lock(?, ?)');
55+
$statement->execute($this->key);
56+
57+
if ($statement->fetchColumn()) {
58+
$loop->end();
59+
}
60+
}, $this->acquireTimeout);
61+
}
62+
63+
#[\Override]
64+
protected function lock(): void
65+
{
66+
if ($this->acquireTimeout === \INF) {
67+
$this->lockBlocking();
68+
} else {
69+
$this->lockBusy();
70+
}
71+
}
72+
4373
#[\Override]
4474
protected function unlock(): void
4575
{

tests/Mutex/MutexTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ static function ($uri) {
240240

241241
return new PostgreSQLMutex($pdo, 'test');
242242
}];
243+
244+
yield 'PostgreSQLMutexWithTimoutLoop' => [static function () {
245+
$pdo = new \PDO(getenv('PGSQL_DSN'), getenv('PGSQL_USER'), getenv('PGSQL_PASSWORD'));
246+
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
247+
248+
return new PostgreSQLMutex($pdo, 'test', 3);
249+
}];
243250
}
244251
}
245252
}

tests/Mutex/PostgreSQLMutexTest.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Malkusch\Lock\Tests\Mutex;
66

7+
use Eloquent\Liberator\Liberator;
8+
use Malkusch\Lock\Exception\LockAcquireTimeoutException;
79
use Malkusch\Lock\Mutex\PostgreSQLMutex;
810
use PHPUnit\Framework\Constraint\IsType;
911
use PHPUnit\Framework\MockObject\MockObject;
@@ -24,7 +26,7 @@ protected function setUp(): void
2426

2527
$this->pdo = $this->createMock(\PDO::class);
2628

27-
$this->mutex = new PostgreSQLMutex($this->pdo, 'test-one-negative-key');
29+
$this->mutex = Liberator::liberate(new PostgreSQLMutex($this->pdo, 'test-one-negative-key')); // @phpstan-ignore assign.propertyType
2830
}
2931

3032
private function isPhpunit9x(): bool
@@ -97,4 +99,45 @@ public function testReleaseLock(): void
9799

98100
\Closure::bind(static fn ($mutex) => $mutex->unlock(), null, PostgreSQLMutex::class)($this->mutex);
99101
}
102+
103+
public function testAcquireTimeoutOccurs(): void
104+
{
105+
$statement = $this->createMock(\PDOStatement::class);
106+
107+
$this->pdo->expects(self::atLeastOnce())
108+
->method('prepare')
109+
->with('SELECT pg_try_advisory_lock(?, ?)')
110+
->willReturn($statement);
111+
112+
$statement->expects(self::atLeastOnce())
113+
->method('execute')
114+
->with(self::logicalAnd(
115+
new IsType(IsType::TYPE_ARRAY),
116+
self::countOf(2),
117+
self::callback(function (...$arguments) {
118+
if ($this->isPhpunit9x()) { // https://github.com/sebastianbergmann/phpunit/issues/5891
119+
$arguments = $arguments[0];
120+
}
121+
122+
foreach ($arguments as $v) {
123+
self::assertLessThan(1 << 32, $v);
124+
self::assertGreaterThanOrEqual(-(1 << 32), $v);
125+
self::assertIsInt($v);
126+
}
127+
128+
return true;
129+
}),
130+
[533558444, -1716795572]
131+
));
132+
133+
$statement->expects(self::atLeastOnce())
134+
->method('fetchColumn')
135+
->willReturn(false);
136+
137+
$this->mutex->acquireTimeout = 1.0; // @phpstan-ignore property.private
138+
139+
$this->expectException(LockAcquireTimeoutException::class);
140+
$this->expectExceptionMessage('Lock acquire timeout of 1.0 seconds has been exceeded');
141+
\Closure::bind(static fn ($mutex) => $mutex->lock(), null, PostgreSQLMutex::class)($this->mutex);
142+
}
100143
}

0 commit comments

Comments
 (0)