diff --git a/README.md b/README.md
index 0fb3bb6..26d9260 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Advisory Locking Features of Postgres/MySQL on Laravel
## Installing
```
-composer require mpyw/laravel-database-advisory-lock:^4.2
+composer require mpyw/laravel-database-advisory-lock:^4.2.1
```
## Basic usage
@@ -153,4 +153,4 @@ END
| Timeout: `positive-int` | ✅
(Emulated) | ✅ |
| Timeout: `negative-int` (infinite wait) | ✅ | ✅ |
-- Postgres does not natively support waiting for a finite specific amount of time, but this is emulated by looping through an anonymous procedure.
+- Postgres does not natively support waiting for a finite specific amount of time, but this is emulated by looping through a temporary function.
diff --git a/src/Utilities/PostgresTryLockLoopEmulator.php b/src/Utilities/PostgresTryLockLoopEmulator.php
index f440bcb..16346a5 100644
--- a/src/Utilities/PostgresTryLockLoopEmulator.php
+++ b/src/Utilities/PostgresTryLockLoopEmulator.php
@@ -4,13 +4,10 @@
namespace Mpyw\LaravelDatabaseAdvisoryLock\Utilities;
-use Illuminate\Database\Connection;
-use Illuminate\Database\ConnectionInterface;
+use Illuminate\Database\PostgresConnection;
use Illuminate\Database\QueryException;
-use LogicException;
use function preg_replace;
-use function str_starts_with;
/**
* class PostgresTryLockLoopEmulator
@@ -19,18 +16,9 @@
*/
final class PostgresTryLockLoopEmulator
{
- private Connection $connection;
-
public function __construct(
- ConnectionInterface $connection,
+ private PostgresConnection $connection,
) {
- if (!$connection instanceof Connection) {
- // @codeCoverageIgnoreStart
- throw new LogicException('Procedure features are not available.');
- // @codeCoverageIgnoreEnd
- }
-
- $this->connection = $connection;
}
/**
@@ -41,42 +29,12 @@ public function __construct(
*/
public function performTryLockLoop(string $key, int $timeout, bool $forTransaction = false): bool
{
- try {
- // Binding parameters to procedures is only allowed when PDOStatement emulation is enabled.
- PDOStatementEmulator::emulated(
- $this->connection->getPdo(),
- fn () => $this->performRawTryLockLoop($key, $timeout, $forTransaction),
- );
- // @codeCoverageIgnoreStart
- throw new LogicException('Unreachable here');
- // @codeCoverageIgnoreEnd
- } catch (QueryException $e) {
- // Handle user level exceptions
- if ($e->getCode() === 'P0001') {
- $prefix = 'ERROR: LaravelDatabaseAdvisoryLock';
- $message = (string)($e->errorInfo[2] ?? '');
- if (str_starts_with($message, "{$prefix}: Lock acquired successfully")) {
- return true;
- }
- if (str_starts_with($message, "{$prefix}: Lock timeout")) {
- return false;
- }
- }
-
- throw $e;
- }
- }
-
- /**
- * Generates SQL to emulate time-limited lock acquisition.
- * This query will always throw QueryException.
- *
- * @phpstan-param positive-int $timeout
- * @throws QueryException
- */
- public function performRawTryLockLoop(string $key, int $timeout, bool $forTransaction): void
- {
- $this->connection->select($this->sql($timeout, $forTransaction), [$key]);
+ // Binding parameters to procedures is only allowed when PDOStatement emulation is enabled.
+ return PDOStatementEmulator::emulated(
+ $this->connection->getPdo(),
+ fn () => (bool)(new Selector($this->connection))
+ ->select($this->sql($timeout, $forTransaction), [$key]),
+ );
}
/**
@@ -89,7 +47,10 @@ public function sql(int $timeout, bool $forTransaction): string
$suffix = $forTransaction ? '_xact' : '';
$sql = << interval '{$timeout} seconds' THEN
- RAISE 'LaravelDatabaseAdvisoryLock: Lock timeout';
+ IF now - start > timeout THEN
+ RETURN false;
END IF;
PERFORM pg_sleep(0.5);
END LOOP;
END
- $$;
+ $$
+ LANGUAGE plpgsql;
+ SELECT pg_temp.laravel_pg_try_advisory{$suffix}_lock_timeout(?, interval '{$timeout} seconds');
EOD;
return (string)preg_replace('/\s++/', ' ', $sql);
diff --git a/tests/SessionLockerTest.php b/tests/SessionLockerTest.php
index a4bf0cf..e8cb3c9 100644
--- a/tests/SessionLockerTest.php
+++ b/tests/SessionLockerTest.php
@@ -172,6 +172,30 @@ public function testFiniteTimeoutSuccess(string $name): void
}
}
+ public function testFinitePostgresTimeoutSuccessConsecutive(): void
+ {
+ $proc1 = self::lockPostgresAsync('foo', 5);
+ $proc2 = self::lockPostgresAsync('baz', 5);
+ sleep(1);
+
+ try {
+ $conn = DB::connection('pgsql');
+ $results = [
+ $conn->advisoryLocker()->forSession()->tryLock('foo', 1),
+ $conn->advisoryLocker()->forSession()->tryLock('bar', 1),
+ $conn->advisoryLocker()->forSession()->tryLock('baz', 1),
+ $conn->advisoryLocker()->forSession()->tryLock('qux', 1),
+ ];
+ $result_booleans = array_map(fn ($result) => $result !== null, $results);
+ $this->assertSame(0, $proc1->wait());
+ $this->assertSame(0, $proc2->wait());
+ $this->assertSame([false, true, false, true], $result_booleans);
+ } finally {
+ $proc1->wait();
+ $proc2->wait();
+ }
+ }
+
/**
* @dataProvider connections
*/
diff --git a/tests/TransactionLockerTest.php b/tests/TransactionLockerTest.php
index 0f27171..3b62d0c 100644
--- a/tests/TransactionLockerTest.php
+++ b/tests/TransactionLockerTest.php
@@ -157,6 +157,33 @@ public function testFinitePostgresTimeoutSuccess(): void
}
}
+ /**
+ * @throws Throwable
+ */
+ public function testFinitePostgresTimeoutSuccessConsecutive(): void
+ {
+ $proc1 = self::lockPostgresAsync('foo', 5);
+ $proc2 = self::lockPostgresAsync('baz', 5);
+ sleep(1);
+
+ try {
+ $result = DB::connection('pgsql')->transaction(function (ConnectionInterface $conn) {
+ return [
+ $conn->advisoryLocker()->forTransaction()->tryLock('foo', 1),
+ $conn->advisoryLocker()->forTransaction()->tryLock('bar', 1),
+ $conn->advisoryLocker()->forTransaction()->tryLock('baz', 1),
+ $conn->advisoryLocker()->forTransaction()->tryLock('qux', 1),
+ ];
+ });
+ $this->assertSame(0, $proc1->wait());
+ $this->assertSame(0, $proc2->wait());
+ $this->assertSame([false, true, false, true], $result);
+ } finally {
+ $proc1->wait();
+ $proc2->wait();
+ }
+ }
+
/**
* @throws Throwable
*/