From 863b6b487e190d561c2e51f4e5f166c5eba682de Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 10:52:14 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Increase=20PHPStan?= =?UTF-8?q?=20memory=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 372206e..59b0bf0 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ }, "scripts": { "test": "vendor/bin/phpunit", - "phpstan": "vendor/bin/phpstan analyse --level=9 src tests phpstan", + "phpstan": "vendor/bin/phpstan analyse --level=9 --memory-limit=2G src tests phpstan", "cs": "vendor/bin/php-cs-fixer fix --dry-run", "cs:fix": "vendor/bin/php-cs-fixer fix" }, From 3bf40d4b7463e605621d52c9def9b3b245bdadcb Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 10:54:09 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20selectBool()=20-?= =?UTF-8?q?>=20select()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MySqlSessionLock.php | 4 ++-- src/MySqlSessionLocker.php | 4 ++-- src/PostgresSessionLock.php | 4 ++-- src/PostgresSessionLocker.php | 4 ++-- src/PostgresTransactionLocker.php | 4 ++-- src/Selector.php | 17 +++++++++-------- tests/SessionLockerTest.php | 8 ++++---- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/MySqlSessionLock.php b/src/MySqlSessionLock.php index cc1fb31..28f6f97 100644 --- a/src/MySqlSessionLock.php +++ b/src/MySqlSessionLock.php @@ -34,8 +34,8 @@ public function release(): bool // it takes first 24 chars from them and appends 40 chars `sha1()` hashes. $sql = 'SELECT RELEASE_LOCK(CASE WHEN CHAR_LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END)'; - $this->released = (new Selector($this->connection)) - ->selectBool($sql, array_fill(0, 4, $this->key)); + $this->released = (bool)(new Selector($this->connection)) + ->select($sql, array_fill(0, 4, $this->key)); // Clean up the lock when it succeeds. $this->released && $this->locks->offsetUnset($this); diff --git a/src/MySqlSessionLocker.php b/src/MySqlSessionLocker.php index ca4b091..ef43b6e 100644 --- a/src/MySqlSessionLocker.php +++ b/src/MySqlSessionLocker.php @@ -35,8 +35,8 @@ public function lockOrFail(string $key, int $timeout = 0): SessionLock $sql = "SELECT GET_LOCK(CASE WHEN CHAR_LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END, {$timeout})"; $bindings = array_fill(0, 4, $key); - $result = (new Selector($this->connection)) - ->selectBool($sql, $bindings); + $result = (bool)(new Selector($this->connection)) + ->select($sql, $bindings); if (!$result) { throw new LockFailedException( diff --git a/src/PostgresSessionLock.php b/src/PostgresSessionLock.php index eee5746..d9b9616 100644 --- a/src/PostgresSessionLock.php +++ b/src/PostgresSessionLock.php @@ -36,8 +36,8 @@ public function release(): bool { if (!$this->released) { try { - $this->released = (new Selector($this->connection)) - ->selectBool('SELECT pg_advisory_unlock(hashtext(?))', [$this->key]); + $this->released = (bool)(new Selector($this->connection)) + ->select('SELECT pg_advisory_unlock(hashtext(?))', [$this->key]); } catch (PDOException $e) { // Postgres can't release session-level locks immediately // when an error occurs within a transaction. diff --git a/src/PostgresSessionLocker.php b/src/PostgresSessionLocker.php index dee34fd..2b7e0bc 100644 --- a/src/PostgresSessionLocker.php +++ b/src/PostgresSessionLocker.php @@ -41,8 +41,8 @@ public function lockOrFail(string $key, int $timeout = 0): SessionLock 1 => throw new UnsupportedDriverException('Positive timeout is not supported'), }; - $result = (new Selector($this->connection)) - ->selectBool($sql, [$key]); + $result = (bool)(new Selector($this->connection)) + ->select($sql, [$key]); if (!$result) { throw new LockFailedException( diff --git a/src/PostgresTransactionLocker.php b/src/PostgresTransactionLocker.php index 19b78f8..eb3e0b3 100644 --- a/src/PostgresTransactionLocker.php +++ b/src/PostgresTransactionLocker.php @@ -33,8 +33,8 @@ public function lockOrFail(string $key, int $timeout = 0): void 1 => throw new UnsupportedDriverException('Positive timeout is not supported'), }; - $result = (new Selector($this->connection)) - ->selectBool($sql, [$key]); + $result = (bool)(new Selector($this->connection)) + ->select($sql, [$key]); if (!$result) { throw new LockFailedException( diff --git a/src/Selector.php b/src/Selector.php index 965b807..59611ab 100644 --- a/src/Selector.php +++ b/src/Selector.php @@ -7,6 +7,8 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Database\QueryException; +use function array_shift; + /** * class Selector * @@ -22,19 +24,18 @@ public function __construct( } /** - * Run query to get a boolean from the result. - * Illegal values are regarded as false. + * Run query to get a single value from the result. * QueryException may be thrown on connection-level errors. * * @throws QueryException */ - public function selectBool(string $sql, array $bindings): bool + public function select(string $sql, array $bindings): mixed { // Always pass false to $useReadPdo - return (bool)current( - (array)$this - ->connection - ->selectOne($sql, $bindings, false), - ); + $row = (array)$this + ->connection + ->selectOne($sql, $bindings, false); + + return array_shift($row); } } diff --git a/tests/SessionLockerTest.php b/tests/SessionLockerTest.php index b229f34..38d7203 100644 --- a/tests/SessionLockerTest.php +++ b/tests/SessionLockerTest.php @@ -118,8 +118,8 @@ public function testMysqlHashing(): void ->forSession() ->withLocking($key, function (ConnectionInterface $conn) use ($key, &$passed): void { $this->assertTrue( - (new Selector($conn)) - ->selectBool( + (bool)(new Selector($conn)) + ->select( 'SELECT IS_USED_LOCK(?)', [substr($key, 0, 64 - 40) . sha1($key)], ), @@ -140,8 +140,8 @@ public function testMysqlHashingMultibyte(): void ->forSession() ->withLocking($key, function (ConnectionInterface $conn) use ($key, &$passed): void { $this->assertTrue( - (new Selector($conn)) - ->selectBool( + (bool)(new Selector($conn)) + ->select( 'SELECT IS_USED_LOCK(?)', [mb_substr($key, 0, 64 - 40) . sha1($key)], ), From ce0939afd9234c0325726e73fa2864389454b80a Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 13:26:45 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Finite=20timeout=20e?= =?UTF-8?q?mulation=20for=20Postgres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MySqlSessionLock.php | 1 + src/MySqlSessionLocker.php | 1 + src/PostgresSessionLock.php | 1 + src/PostgresSessionLocker.php | 25 ++-- src/PostgresTransactionLocker.php | 27 ++-- src/Utilities/PDOStatementEmulator.php | 32 +++++ src/Utilities/PostgresTryLockLoopEmulator.php | 116 ++++++++++++++++++ src/{ => Utilities}/Selector.php | 2 +- 8 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 src/Utilities/PDOStatementEmulator.php create mode 100644 src/Utilities/PostgresTryLockLoopEmulator.php rename src/{ => Utilities}/Selector.php (93%) diff --git a/src/MySqlSessionLock.php b/src/MySqlSessionLock.php index 28f6f97..2b8f90a 100644 --- a/src/MySqlSessionLock.php +++ b/src/MySqlSessionLock.php @@ -7,6 +7,7 @@ use Illuminate\Database\MySqlConnection; use Mpyw\LaravelDatabaseAdvisoryLock\Concerns\ReleasesWhenDestructed; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\SessionLock; +use Mpyw\LaravelDatabaseAdvisoryLock\Utilities\Selector; use WeakMap; use function array_fill; diff --git a/src/MySqlSessionLocker.php b/src/MySqlSessionLocker.php index ef43b6e..68503a9 100644 --- a/src/MySqlSessionLocker.php +++ b/src/MySqlSessionLocker.php @@ -9,6 +9,7 @@ use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\LockFailedException; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\SessionLock; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\SessionLocker; +use Mpyw\LaravelDatabaseAdvisoryLock\Utilities\Selector; use WeakMap; use function array_fill; diff --git a/src/PostgresSessionLock.php b/src/PostgresSessionLock.php index d9b9616..96c51b1 100644 --- a/src/PostgresSessionLock.php +++ b/src/PostgresSessionLock.php @@ -10,6 +10,7 @@ use Mpyw\LaravelDatabaseAdvisoryLock\Concerns\ReleasesWhenDestructed; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\SessionLock; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\TransactionTerminationListener; +use Mpyw\LaravelDatabaseAdvisoryLock\Utilities\Selector; use PDOException; use WeakMap; diff --git a/src/PostgresSessionLocker.php b/src/PostgresSessionLocker.php index 2b7e0bc..a846fdf 100644 --- a/src/PostgresSessionLocker.php +++ b/src/PostgresSessionLocker.php @@ -9,7 +9,8 @@ use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\LockFailedException; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\SessionLock; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\SessionLocker; -use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\UnsupportedDriverException; +use Mpyw\LaravelDatabaseAdvisoryLock\Utilities\PostgresTryLockLoopEmulator; +use Mpyw\LaravelDatabaseAdvisoryLock\Utilities\Selector; use WeakMap; final class PostgresSessionLocker implements SessionLocker @@ -34,15 +35,21 @@ public function __construct( */ public function lockOrFail(string $key, int $timeout = 0): SessionLock { - // Negative timeout means infinite wait - $sql = match ($timeout <=> 0) { - -1 => "SELECT pg_advisory_lock(hashtext(?))::text = ''", - 0 => 'SELECT pg_try_advisory_lock(hashtext(?))', - 1 => throw new UnsupportedDriverException('Positive timeout is not supported'), - }; + if ($timeout > 0) { + // Positive timeout can be emulated through repeating sleep and retry + $emulator = new PostgresTryLockLoopEmulator($this->connection); + $sql = $emulator->sql($timeout, false); + $result = $emulator->performTryLockLoop($key, $timeout); + } else { + // Negative timeout means infinite wait + // Zero timeout means no wait + $sql = $timeout < 0 + ? "SELECT pg_advisory_lock(hashtext(?))::text = ''" + : 'SELECT pg_try_advisory_lock(hashtext(?))'; - $result = (bool)(new Selector($this->connection)) - ->select($sql, [$key]); + $selector = new Selector($this->connection); + $result = (bool)$selector->select($sql, [$key]); + } if (!$result) { throw new LockFailedException( diff --git a/src/PostgresTransactionLocker.php b/src/PostgresTransactionLocker.php index eb3e0b3..78c2d58 100644 --- a/src/PostgresTransactionLocker.php +++ b/src/PostgresTransactionLocker.php @@ -9,7 +9,8 @@ use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\InvalidTransactionLevelException; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\LockFailedException; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\TransactionLocker; -use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\UnsupportedDriverException; +use Mpyw\LaravelDatabaseAdvisoryLock\Utilities\PostgresTryLockLoopEmulator; +use Mpyw\LaravelDatabaseAdvisoryLock\Utilities\Selector; final class PostgresTransactionLocker implements TransactionLocker { @@ -26,15 +27,21 @@ public function lockOrFail(string $key, int $timeout = 0): void throw new InvalidTransactionLevelException('There are no transactions'); } - // Negative timeout means infinite wait - $sql = match ($timeout <=> 0) { - -1 => "SELECT pg_advisory_xact_lock(hashtext(?))::text = ''", - 0 => 'SELECT pg_try_advisory_xact_lock(hashtext(?))', - 1 => throw new UnsupportedDriverException('Positive timeout is not supported'), - }; - - $result = (bool)(new Selector($this->connection)) - ->select($sql, [$key]); + if ($timeout > 0) { + // Positive timeout can be emulated through repeating sleep and retry + $emulator = new PostgresTryLockLoopEmulator($this->connection); + $sql = $emulator->sql($timeout, false); + $result = $emulator->performTryLockLoop($key, $timeout, true); + } else { + // Negative timeout means infinite wait + // Zero timeout means no wait + $sql = $timeout < 0 + ? "SELECT pg_advisory_xact_lock(hashtext(?))::text = ''" + : 'SELECT pg_try_advisory_xact_lock(hashtext(?))'; + + $selector = new Selector($this->connection); + $result = (bool)$selector->select($sql, [$key]); + } if (!$result) { throw new LockFailedException( diff --git a/src/Utilities/PDOStatementEmulator.php b/src/Utilities/PDOStatementEmulator.php new file mode 100644 index 0000000..1099961 --- /dev/null +++ b/src/Utilities/PDOStatementEmulator.php @@ -0,0 +1,32 @@ +getAttribute(PDO::ATTR_EMULATE_PREPARES); + $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + + try { + return $callback(); + } finally { + $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, $original); + } + } +} diff --git a/src/Utilities/PostgresTryLockLoopEmulator.php b/src/Utilities/PostgresTryLockLoopEmulator.php new file mode 100644 index 0000000..f440bcb --- /dev/null +++ b/src/Utilities/PostgresTryLockLoopEmulator.php @@ -0,0 +1,116 @@ +connection = $connection; + } + + /** + * Perform a time-limited lock acquisition. + * + * @phpstan-param positive-int $timeout + * @throws QueryException + */ + 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]); + } + + /** + * Generates SQL to emulate time-limited lock acquisition. + * + * @phpstan-param positive-int $timeout + */ + public function sql(int $timeout, bool $forTransaction): string + { + $suffix = $forTransaction ? '_xact' : ''; + + $sql = << interval '{$timeout} seconds' THEN + RAISE 'LaravelDatabaseAdvisoryLock: Lock timeout'; + END IF; + PERFORM pg_sleep(0.5); + END LOOP; + END + $$; + EOD; + + return (string)preg_replace('/\s++/', ' ', $sql); + } +} diff --git a/src/Selector.php b/src/Utilities/Selector.php similarity index 93% rename from src/Selector.php rename to src/Utilities/Selector.php index 59611ab..c0cc6d3 100644 --- a/src/Selector.php +++ b/src/Utilities/Selector.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Mpyw\LaravelDatabaseAdvisoryLock; +namespace Mpyw\LaravelDatabaseAdvisoryLock\Utilities; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\QueryException; From 3d8b50745650a10b8211d18a08ea0b14526eda8f Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 13:27:12 +0900 Subject: [PATCH 4/6] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/AcquiresLockInSeparateProcesses.php | 13 ++++ tests/SessionLockerTest.php | 85 +++++++---------------- tests/TransactionLockerTest.php | 40 +++++++++-- 3 files changed, 71 insertions(+), 67 deletions(-) diff --git a/tests/AcquiresLockInSeparateProcesses.php b/tests/AcquiresLockInSeparateProcesses.php index 01c7e84..ef16d10 100644 --- a/tests/AcquiresLockInSeparateProcesses.php +++ b/tests/AcquiresLockInSeparateProcesses.php @@ -4,10 +4,23 @@ namespace Mpyw\LaravelDatabaseAdvisoryLock\Tests; +use LogicException; use Symfony\Component\Process\Process; trait AcquiresLockInSeparateProcesses { + private static function lockAsync(string $driver, string $key, int $sleep): Process + { + if ($driver === 'mysql') { + return self::lockMysqlAsync($key, $sleep); + } + if ($driver === 'pgsql') { + return self::lockPostgresAsync($key, $sleep); + } + + throw new LogicException('Unsupported driver'); + } + private static function lockMysqlAsync(string $key, int $sleep): Process { $host = config('database.connections.mysql.host'); diff --git a/tests/SessionLockerTest.php b/tests/SessionLockerTest.php index 38d7203..a4bf0cf 100644 --- a/tests/SessionLockerTest.php +++ b/tests/SessionLockerTest.php @@ -7,8 +7,7 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Support\Facades\DB; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\LockFailedException; -use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\UnsupportedDriverException; -use Mpyw\LaravelDatabaseAdvisoryLock\Selector; +use Mpyw\LaravelDatabaseAdvisoryLock\Utilities\Selector; class SessionLockerTest extends TestCase { @@ -152,98 +151,64 @@ public function testMysqlHashingMultibyte(): void $this->assertTrue($passed); } - public function testFiniteMysqlTimeoutSuccess(): void + /** + * @dataProvider connections + */ + public function testFiniteTimeoutSuccess(string $name): void { - $passed = false; - - $proc = self::lockMysqlAsync('foo', 2); + $proc = self::lockAsync($name, 'foo', 2); sleep(1); try { - DB::connection('mysql') + $result = DB::connection($name) ->advisoryLocker() ->forSession() - ->withLocking('foo', function () use (&$passed): void { - $passed = true; - }, 3); + ->tryLock('foo', 3); $this->assertSame(0, $proc->wait()); - $this->assertTrue($passed); - } finally { - $proc->wait(); - } - } - - public function testFiniteMysqlTimeoutExceeded(): void - { - $proc = self::lockMysqlAsync('foo', 3); - sleep(1); - - try { - $this->expectException(LockFailedException::class); - $this->expectExceptionMessage('Failed to acquire lock: foo'); - - DB::connection('mysql') - ->advisoryLocker() - ->forSession() - ->withLocking('foo', function (): void { - }, 1); + $this->assertNotNull($result); } finally { $proc->wait(); } } - public function testInfiniteMysqlTimeoutSuccess(): void + /** + * @dataProvider connections + */ + public function testFiniteTimeoutExceeded(string $name): void { - $passed = false; - - $proc = self::lockMysqlAsync('foo', 2); + $proc = self::lockAsync($name, 'foo', 3); sleep(1); try { - DB::connection('mysql') + $result = DB::connection($name) ->advisoryLocker() ->forSession() - ->withLocking('foo', function () use (&$passed): void { - $passed = true; - }, -1); + ->tryLock('foo', 1); $this->assertSame(0, $proc->wait()); - $this->assertTrue($passed); + $this->assertNull($result); } finally { $proc->wait(); } } - public function testFinitePostgresTimeoutInvalid(): void - { - $this->expectException(UnsupportedDriverException::class); - $this->expectExceptionMessage('Positive timeout is not supported'); - - DB::connection('pgsql') - ->advisoryLocker() - ->forSession() - ->withLocking('foo', function (): void { - }, 1); - } - - public function testInfinitePostgresTimeoutSuccess(): void + /** + * @dataProvider connections + */ + public function testInfiniteTimeoutSuccess(string $name): void { - $passed = false; - - $proc = self::lockPostgresAsync('foo', 2); + $proc = self::lockAsync($name, 'foo', 2); sleep(1); try { - DB::connection('pgsql') + $result = DB::connection($name) ->advisoryLocker() ->forSession() - ->withLocking('foo', function () use (&$passed): void { - $passed = true; - }, -1); + ->tryLock('foo', -1); $this->assertSame(0, $proc->wait()); - $this->assertTrue($passed); + $this->assertNotNull($result); } finally { $proc->wait(); } diff --git a/tests/TransactionLockerTest.php b/tests/TransactionLockerTest.php index 89c6d95..0f27171 100644 --- a/tests/TransactionLockerTest.php +++ b/tests/TransactionLockerTest.php @@ -8,7 +8,6 @@ use Illuminate\Support\Facades\DB; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\InvalidTransactionLevelException; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\LockFailedException; -use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\UnsupportedDriverException; use Throwable; class TransactionLockerTest extends TestCase @@ -141,14 +140,41 @@ public function testWithoutTransactions(string $name): void /** * @throws Throwable */ - public function testFinitePostgresTimeoutInvalid(): void + public function testFinitePostgresTimeoutSuccess(): void { - $this->expectException(UnsupportedDriverException::class); - $this->expectExceptionMessage('Positive timeout is not supported'); + $proc = self::lockPostgresAsync('foo', 2); + sleep(1); - DB::connection('pgsql')->transaction(function (ConnectionInterface $conn): void { - $conn->advisoryLocker()->forTransaction()->lockOrFail('foo', 1); - }); + try { + $result = DB::connection('pgsql')->transaction(function (ConnectionInterface $conn) { + return $conn->advisoryLocker()->forTransaction()->tryLock('foo', 3); + }); + + $this->assertSame(0, $proc->wait()); + $this->assertTrue($result); + } finally { + $proc->wait(); + } + } + + /** + * @throws Throwable + */ + public function testFinitePostgresTimeoutExceeded(): void + { + $proc = self::lockPostgresAsync('foo', 3); + sleep(1); + + try { + $result = DB::connection('pgsql')->transaction(function (ConnectionInterface $conn) { + return $conn->advisoryLocker()->forTransaction()->tryLock('foo', 1); + }); + + $this->assertSame(0, $proc->wait()); + $this->assertFalse($result); + } finally { + $proc->wait(); + } } /** From d310305ac9a1ef7486a6a1d8a0cb5cb1c206f2d3 Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 13:27:34 +0900 Subject: [PATCH 5/6] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Update=20PHPStan=20?= =?UTF-8?q?configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 67f3f74..1aee6d0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,5 +8,5 @@ parameters: paths: - src/AdvisoryLocks.php - src/Contracts/LockFailedException.php - - src/Selector.php + - src/Utilities/Selector.php - tests/*.php From fa50419ce00961b0e5aeef26bd73a4f3b1b9f9f8 Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 13:28:41 +0900 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20Update=20REAAD?= =?UTF-8?q?ME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fa5cf28..15cb6b7 100644 --- a/README.md +++ b/README.md @@ -139,10 +139,10 @@ END ### Locking Methods -| | Postgres | MySQL | -|:--------------------------|:---------|:------| -| Session-Level Locking | ✅ | ✅ | -| Transaction-Level Locking | ✅ | ❌ | +| | Postgres | MySQL | +|:--------------------------|:---------:|:------:| +| Session-Level Locking | ✅ | ✅ | +| Transaction-Level Locking | ✅ | ❌ | - Session-Level locks can be acquired anywhere. - They can be released manually or automatically through a destructor. @@ -152,8 +152,10 @@ END ### Timeout Values -| | Postgres | MySQL | -|:-------------------------------------------|:---------|:------| -| Timeout: `0` (default; immediate, no wait) | ✅ | ✅ | -| Timeout: `positive-int` | ❌ | ✅ | -| Timeout: `negative-int` (infinite wait) | ✅ | ✅ | +| | Postgres | MySQL | +|:-------------------------------------------|:----------------:|:------:| +| Timeout: `0` (default; immediate, no wait) | ✅ | ✅ | +| 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.