From 5e03f5a4f547bd71bc0240101a591f94c9db5ee9 Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 00:58:19 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Change=20MySQL=20func?= =?UTF-8?q?tion=20`LENGTH()`=20into=20`CHAR=5FLENGTH()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MySqlSessionLock.php | 6 +++--- src/MySqlSessionLocker.php | 6 +++--- tests/ReconnectionToleranceTest.php | 6 +++--- tests/SessionLockerTest.php | 25 ++++++++++++++----------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/MySqlSessionLock.php b/src/MySqlSessionLock.php index 8a1720f..cc1fb31 100644 --- a/src/MySqlSessionLock.php +++ b/src/MySqlSessionLock.php @@ -30,9 +30,9 @@ public function __construct( public function release(): bool { if (!$this->released) { - // When key strings exceed 64 bytes limit, - // it takes first 24 bytes from them and appends 40 bytes `sha1()` hashes. - $sql = 'SELECT RELEASE_LOCK(CASE WHEN LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END)'; + // When key strings exceed 64 chars limit, + // 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)); diff --git a/src/MySqlSessionLocker.php b/src/MySqlSessionLocker.php index 97f8402..7f2d5f0 100644 --- a/src/MySqlSessionLocker.php +++ b/src/MySqlSessionLocker.php @@ -30,9 +30,9 @@ public function __construct( public function lockOrFail(string $key, int $timeout = 0): SessionLock { - // When key strings exceed 64 bytes limit, - // it takes first 24 bytes from them and appends 40 bytes `sha1()` hashes. - $sql = "SELECT GET_LOCK(CASE WHEN LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END, {$timeout})"; + // When key strings exceed 64 chars limit, + // it takes first 24 chars from them and appends 40 chars `sha1()` hashes. + $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)) diff --git a/tests/ReconnectionToleranceTest.php b/tests/ReconnectionToleranceTest.php index 6c14bfe..cb4702d 100644 --- a/tests/ReconnectionToleranceTest.php +++ b/tests/ReconnectionToleranceTest.php @@ -89,8 +89,8 @@ public function testReconnectionWithoutActiveLocks(): void // Retries $this->assertSame([ - 'SELECT GET_LOCK(CASE WHEN LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END, 0)', - 'SELECT GET_LOCK(CASE WHEN LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END, 0)', + 'SELECT GET_LOCK(CASE WHEN CHAR_LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END, 0)', + 'SELECT GET_LOCK(CASE WHEN CHAR_LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END, 0)', ], $this->queries); } @@ -115,7 +115,7 @@ public function testReconnectionWithActiveLocks(): void // No retries $this->assertSame([ - 'SELECT GET_LOCK(CASE WHEN LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END, 0)', + 'SELECT GET_LOCK(CASE WHEN CHAR_LENGTH(?) > 64 THEN CONCAT(SUBSTR(?, 1, 24), SHA1(?)) ELSE ? END, 0)', ], $this->queries); } } diff --git a/tests/SessionLockerTest.php b/tests/SessionLockerTest.php index 825949f..c03a6d0 100644 --- a/tests/SessionLockerTest.php +++ b/tests/SessionLockerTest.php @@ -105,28 +105,31 @@ public function testSameKeysOnSameConnections(string $name): void $this->assertTrue($passed); } - public function testMysqlTimeout(): void + public function testMysqlHashing(): void { + $key = str_repeat('a', 65); $passed = false; DB::connection('mysql') ->advisoryLocker() ->forSession() - ->withLocking('foo', function (ConnectionInterface $conn) use (&$passed): void { - $conn - ->advisoryLocker() - ->forSession() - ->withLocking('foo', function () use (&$passed): void { - $passed = true; - }); + ->withLocking($key, function (ConnectionInterface $conn) use ($key, &$passed): void { + $this->assertTrue( + (new Selector($conn)) + ->selectBool( + 'SELECT IS_USED_LOCK(?)', + [substr($key, 0, 64 - 40) . sha1($key)], + ), + ); + $passed = true; }); $this->assertTrue($passed); } - public function testMysqlHashing(): void + public function testMysqlHashingMultibyte(): void { - $key = str_repeat('a', 65); + $key = str_repeat('あ', 65); $passed = false; DB::connection('mysql') @@ -137,7 +140,7 @@ public function testMysqlHashing(): void (new Selector($conn)) ->selectBool( 'SELECT IS_USED_LOCK(?)', - [substr($key, 0, 64 - 40) . sha1($key)], + [mb_substr($key, 0, 64 - 40) . sha1($key)], ), ); $passed = true; From 8631c3f2783d2e8f0e99884c49dd227c4fd4440d Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 01:01:37 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Allow=20`-1`=20(infi?= =?UTF-8?q?nite=20wait)=20for=20Postgres=20timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/PostgresSessionLocker.php | 13 ++- src/PostgresTransactionLocker.php | 13 ++- tests/AcquiresLockInSeparateProcesses.php | 48 +++++++++++ tests/SessionLockerTest.php | 100 ++++++++++++++++++++++ tests/TransactionLockerTest.php | 36 ++++++++ 5 files changed, 196 insertions(+), 14 deletions(-) create mode 100644 tests/AcquiresLockInSeparateProcesses.php diff --git a/src/PostgresSessionLocker.php b/src/PostgresSessionLocker.php index 3730bc6..c039721 100644 --- a/src/PostgresSessionLocker.php +++ b/src/PostgresSessionLocker.php @@ -34,13 +34,12 @@ public function __construct( */ public function lockOrFail(string $key, int $timeout = 0): SessionLock { - if ($timeout !== 0) { - // @codeCoverageIgnoreStart - throw new UnsupportedDriverException('Timeout feature is not supported'); - // @codeCoverageIgnoreEnd - } - - $sql = 'SELECT pg_try_advisory_lock(hashtext(?))'; + // 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'), + }; $result = (new Selector($this->connection)) ->selectBool($sql, [$key]); diff --git a/src/PostgresTransactionLocker.php b/src/PostgresTransactionLocker.php index f240c8d..9b6ad71 100644 --- a/src/PostgresTransactionLocker.php +++ b/src/PostgresTransactionLocker.php @@ -26,13 +26,12 @@ public function lockOrFail(string $key, int $timeout = 0): void throw new InvalidTransactionLevelException('There are no transactions'); } - if ($timeout !== 0) { - // @codeCoverageIgnoreStart - throw new UnsupportedDriverException('Timeout feature is not supported'); - // @codeCoverageIgnoreEnd - } - - $sql = 'SELECT pg_try_advisory_xact_lock(hashtext(?))'; + // 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 = (new Selector($this->connection)) ->selectBool($sql, [$key]); diff --git a/tests/AcquiresLockInSeparateProcesses.php b/tests/AcquiresLockInSeparateProcesses.php new file mode 100644 index 0000000..01c7e84 --- /dev/null +++ b/tests/AcquiresLockInSeparateProcesses.php @@ -0,0 +1,48 @@ +query("SELECT GET_LOCK('{$key}', 0)")->fetchColumn(); + sleep({$sleep}); + exit(\$result == 1 ? 0 : 1); + EOD, + ]); + $proc->start(); + + return $proc; + } + + private static function lockPostgresAsync(string $key, int $sleep): Process + { + $host = config('database.connections.pgsql.host'); + assert(is_string($host)); + + $proc = new Process([PHP_BINARY, '-r', + <<query("SELECT pg_try_advisory_lock(hashtext('{$key}'))")->fetchColumn(); + sleep({$sleep}); + exit(\$result ? 0 : 1); + EOD, + ]); + $proc->start(); + + return $proc; + } +} diff --git a/tests/SessionLockerTest.php b/tests/SessionLockerTest.php index c03a6d0..b229f34 100644 --- a/tests/SessionLockerTest.php +++ b/tests/SessionLockerTest.php @@ -7,10 +7,13 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Support\Facades\DB; use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\LockFailedException; +use Mpyw\LaravelDatabaseAdvisoryLock\Contracts\UnsupportedDriverException; use Mpyw\LaravelDatabaseAdvisoryLock\Selector; class SessionLockerTest extends TestCase { + use AcquiresLockInSeparateProcesses; + public function connections(): array { return ['postgres' => ['pgsql'], 'mysql' => ['mysql']]; @@ -148,4 +151,101 @@ public function testMysqlHashingMultibyte(): void $this->assertTrue($passed); } + + public function testFiniteMysqlTimeoutSuccess(): void + { + $passed = false; + + $proc = self::lockMysqlAsync('foo', 2); + sleep(1); + + try { + DB::connection('mysql') + ->advisoryLocker() + ->forSession() + ->withLocking('foo', function () use (&$passed): void { + $passed = true; + }, 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); + } finally { + $proc->wait(); + } + } + + public function testInfiniteMysqlTimeoutSuccess(): void + { + $passed = false; + + $proc = self::lockMysqlAsync('foo', 2); + sleep(1); + + try { + DB::connection('mysql') + ->advisoryLocker() + ->forSession() + ->withLocking('foo', function () use (&$passed): void { + $passed = true; + }, -1); + + $this->assertSame(0, $proc->wait()); + $this->assertTrue($passed); + } 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 + { + $passed = false; + + $proc = self::lockPostgresAsync('foo', 2); + sleep(1); + + try { + DB::connection('pgsql') + ->advisoryLocker() + ->forSession() + ->withLocking('foo', function () use (&$passed): void { + $passed = true; + }, -1); + + $this->assertSame(0, $proc->wait()); + $this->assertTrue($passed); + } finally { + $proc->wait(); + } + } } diff --git a/tests/TransactionLockerTest.php b/tests/TransactionLockerTest.php index f453633..89c6d95 100644 --- a/tests/TransactionLockerTest.php +++ b/tests/TransactionLockerTest.php @@ -8,10 +8,13 @@ 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 { + use AcquiresLockInSeparateProcesses; + public function connections(): array { return ['postgres' => ['pgsql']]; @@ -134,4 +137,37 @@ public function testWithoutTransactions(string $name): void ->forTransaction() ->lockOrFail('foo'); } + + /** + * @throws Throwable + */ + public function testFinitePostgresTimeoutInvalid(): void + { + $this->expectException(UnsupportedDriverException::class); + $this->expectExceptionMessage('Positive timeout is not supported'); + + DB::connection('pgsql')->transaction(function (ConnectionInterface $conn): void { + $conn->advisoryLocker()->forTransaction()->lockOrFail('foo', 1); + }); + } + + /** + * @throws Throwable + */ + public function testInfinitePostgresTimeoutSuccess(): void + { + $proc = self::lockPostgresAsync('foo', 2); + sleep(1); + + try { + $result = DB::connection('pgsql')->transaction(function (ConnectionInterface $conn) { + return $conn->advisoryLocker()->forTransaction()->tryLock('foo', -1); + }); + + $this->assertSame(0, $proc->wait()); + $this->assertTrue($result); + } finally { + $proc->wait(); + } + } } From 7c221bda543eb81cb81b9bae18d9c9e853fe98e1 Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 01:09:38 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Follow=20Laravel=2010?= =?UTF-8?q?=20breaking=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++++------ src/Contracts/LockFailedException.php | 14 ++++++++++++-- src/MySqlSessionLocker.php | 7 ++++++- src/PostgresSessionLocker.php | 7 ++++++- src/PostgresTransactionLocker.php | 7 ++++++- tests/TransactionErrorRecoveryTest.php | 8 ++++++-- ...ansactionErrorRefreshDatabaseRecoveryTest.php | 16 ++++++++++++---- 7 files changed, 54 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0747e8a..81af3f7 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ Advisory Locking Features of Postgres/MySQL on Laravel ## Requirements -| Package | Version | Mandatory | -|:---|:------------------------------------|:---:| -| PHP | ^8.0.2 | ✅ | -| Laravel | ^8.0 || ^9.0 || ^10.0 | ✅ | -| PHPStan | >=1.1 | | +| Package | Version | Mandatory | +|:--------|:-------------------------------------------------------|:---------:| +| PHP | ^8.0.2 | ✅ | +| Laravel | ^8.0 || ^9.0 || ^10.0 | ✅ | +| PHPStan | >=1.1 | | ## Installing ``` -composer require mpyw/laravel-database-advisory-lock:^4.0 +composer require mpyw/laravel-database-advisory-lock:^4.1 ``` ## Basic usage diff --git a/src/Contracts/LockFailedException.php b/src/Contracts/LockFailedException.php index 72ce056..81a0848 100644 --- a/src/Contracts/LockFailedException.php +++ b/src/Contracts/LockFailedException.php @@ -5,6 +5,7 @@ namespace Mpyw\LaravelDatabaseAdvisoryLock\Contracts; use Illuminate\Database\QueryException; +use ReflectionMethod; use RuntimeException; /** @@ -14,8 +15,17 @@ */ class LockFailedException extends QueryException { - public function __construct(string $message, string $sql, array $bindings) + public function __construct(string $connectionName, string $message, string $sql, array $bindings) { - parent::__construct($sql, $bindings, new RuntimeException($message)); + $previous = new RuntimeException($message); + + // Laravel 10 newly introduces $connectionName parameter + // https://github.com/laravel/framework/pull/43190 + $args = (new ReflectionMethod(parent::class, __FUNCTION__))->getNumberOfParameters() > 3 + ? [$connectionName, $sql, $bindings, $previous] + : [$sql, $bindings, $previous]; + + // @phpstan-ignore-next-line + parent::__construct(...$args); } } diff --git a/src/MySqlSessionLocker.php b/src/MySqlSessionLocker.php index 7f2d5f0..ca4b091 100644 --- a/src/MySqlSessionLocker.php +++ b/src/MySqlSessionLocker.php @@ -39,7 +39,12 @@ public function lockOrFail(string $key, int $timeout = 0): SessionLock ->selectBool($sql, $bindings); if (!$result) { - throw new LockFailedException("Failed to acquire lock: {$key}", $sql, $bindings); + throw new LockFailedException( + (string)$this->connection->getName(), + "Failed to acquire lock: {$key}", + $sql, + $bindings, + ); } // Register the lock when it succeeds. diff --git a/src/PostgresSessionLocker.php b/src/PostgresSessionLocker.php index c039721..dee34fd 100644 --- a/src/PostgresSessionLocker.php +++ b/src/PostgresSessionLocker.php @@ -45,7 +45,12 @@ public function lockOrFail(string $key, int $timeout = 0): SessionLock ->selectBool($sql, [$key]); if (!$result) { - throw new LockFailedException("Failed to acquire lock: {$key}", $sql, [$key]); + throw new LockFailedException( + (string)$this->connection->getName(), + "Failed to acquire lock: {$key}", + $sql, + [$key], + ); } // Register the lock when it succeeds. diff --git a/src/PostgresTransactionLocker.php b/src/PostgresTransactionLocker.php index 9b6ad71..19b78f8 100644 --- a/src/PostgresTransactionLocker.php +++ b/src/PostgresTransactionLocker.php @@ -37,7 +37,12 @@ public function lockOrFail(string $key, int $timeout = 0): void ->selectBool($sql, [$key]); if (!$result) { - throw new LockFailedException("Failed to acquire lock: {$key}", $sql, [$key]); + throw new LockFailedException( + (string)$this->connection->getName(), + "Failed to acquire lock: {$key}", + $sql, + [$key], + ); } } } diff --git a/tests/TransactionErrorRecoveryTest.php b/tests/TransactionErrorRecoveryTest.php index 1d4a734..ed91d1d 100644 --- a/tests/TransactionErrorRecoveryTest.php +++ b/tests/TransactionErrorRecoveryTest.php @@ -142,10 +142,14 @@ public function testDestructorReleasesLocksAfterTransactionTerminated(): void } catch (QueryException $e) { // Thrown from [*] $this->assertSame( - $e->getMessage(), 'SQLSTATE[25P02]: In failed sql transaction: 7 ERROR: ' . 'current transaction is aborted, commands ignored until end of transaction block ' - . '(SQL: insert into users(id) values(2))', + . ( + version_compare($this->app->version(), '10.x-dev', '>=') + ? '(Connection: pgsql, SQL: insert into users(id) values(2))' + : '(SQL: insert into users(id) values(2))' + ), + $e->getMessage(), ); } diff --git a/tests/TransactionErrorRefreshDatabaseRecoveryTest.php b/tests/TransactionErrorRefreshDatabaseRecoveryTest.php index 24092a9..44f2cf9 100644 --- a/tests/TransactionErrorRefreshDatabaseRecoveryTest.php +++ b/tests/TransactionErrorRefreshDatabaseRecoveryTest.php @@ -46,10 +46,14 @@ public function testImplicitTransactionRollbacksToSavepoint(): void } catch (QueryException $e) { // Thrown from [*] $this->assertSame( - $e->getMessage(), 'SQLSTATE[25P02]: In failed sql transaction: 7 ERROR: ' . 'current transaction is aborted, commands ignored until end of transaction block ' - . '(SQL: insert into users(id) values(2))', + . ( + version_compare($this->app->version(), '10.x-dev', '>=') + ? '(Connection: pgsql, SQL: insert into users(id) values(2))' + : '(SQL: insert into users(id) values(2))' + ), + $e->getMessage(), ); } @@ -155,10 +159,14 @@ public function testDestructorReleasesLocksAfterRollingBackToSavepoint(): void } catch (QueryException $e) { // Thrown from [*] $this->assertSame( - $e->getMessage(), 'SQLSTATE[25P02]: In failed sql transaction: 7 ERROR: ' . 'current transaction is aborted, commands ignored until end of transaction block ' - . '(SQL: insert into users(id) values(2))', + . ( + version_compare($this->app->version(), '10.x-dev', '>=') + ? '(Connection: pgsql, SQL: insert into users(id) values(2))' + : '(SQL: insert into users(id) values(2))' + ), + $e->getMessage(), ); } From 7c6d823f1cababae717e2b04582daf27ffd04303 Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 21 Jul 2022 01:01:51 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20Update=20READM?= =?UTF-8?q?E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 81af3f7..fa5cf28 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ return [ use Illuminate\Support\Facades\DB; use Illuminate\Database\ConnectionInterface; -// Postgres/MySQL +// Postgres/MySQL: Session-Level Locking (no wait) $result = DB::advisoryLocker() ->forSession() ->withLocking('', function (ConnectionInterface $conn) { @@ -53,7 +53,7 @@ $result = DB::advisoryLocker() return ...; }); -// Postgres only feature +// Postgres only feature: Transaction-Level Locking (no wait) $result = DB::transaction(function (ConnectionInterface $conn) { $conn->advisoryLocker()->forTransaction()->lockOrFail(''); @@ -61,13 +61,21 @@ $result = DB::transaction(function (ConnectionInterface $conn) { return ...; }); -// MySQL only feature +// MySQL only feature: Session-Level Locking with timeout (waits for 5 seconds or fails) $result = DB::advisoryLocker() ->forSession() ->withLocking('', function (ConnectionInterface $conn) { // critical section here return ...; }, timeout: 5); + +// Postgres/MySQL: Session-Level Locking with infinite wait +$result = DB::advisoryLocker() + ->forSession() + ->withLocking('', function (ConnectionInterface $conn) { + // critical section here + return ...; + }, timeout: -1); ``` ## Advanced Usage @@ -112,20 +120,40 @@ class PostgresConnection extends BasePostgresConnection ### Key Hashing Algorithm +```sql +-- Postgres: int8 +hashtext('') +``` + +```sql +-- MySQL: varchar(64) +CASE WHEN CHAR_LENGTH('') > 64 +THEN CONCAT(SUBSTR('', 1, 24), SHA1('')) +ELSE '' +END +``` + - Postgres advisory locking functions only accepts integer keys. So the driver converts key strings into 64-bit integers through `hashtext()` function. -- MySQL advisory locking functions accepts string keys but their length are limited within 64 bytes. When key strings exceed 64 bytes limit, the driver takes first 24 bytes from them and appends 40 bytes `sha1()` hashes. +- MySQL advisory locking functions accepts string keys but their length are limited within 64 chars. When key strings exceed 64 chars limit, the driver takes first 24 chars from them and appends 40 chars `sha1()` hashes. - With either hashing algorithm, collisions can theoretically occur with very low probability. -### Transaction-Level Locks +### Locking Methods + +| | Postgres | MySQL | +|:--------------------------|:---------|:------| +| Session-Level Locking | ✅ | ✅ | +| Transaction-Level Locking | ✅ | ❌ | -- MySQL does not support native transaction-level advisory locking. -- Postgres supports native transaction-level advisory locking. - - Locks can be acquired at any transaction scope. +- Session-Level locks can be acquired anywhere. + - They can be released manually or automatically through a destructor. + - For Postgres, there was a problem where the automatic lock release algorithm did not work properly, but this has been fixed in version 4.0.0. See [#2](https://github.com/mpyw/laravel-database-advisory-lock/pull/2) for details. +- Transaction-Level locks can be acquired within a transaction. - You do not need to and cannot manually release locks that have been acquired. -### Session-Level Locks +### Timeout Values -- MySQL supports session-level advisory locking. - - An optional wait timeout can be set. -- Postgres supports session-level advisory locking. - - There was a problem where the automatic lock release algorithm did not work properly, but this has been fixed in version 4.0.0. See [#2](https://github.com/mpyw/laravel-database-advisory-lock/pull/2) for details. +| | Postgres | MySQL | +|:-------------------------------------------|:---------|:------| +| Timeout: `0` (default; immediate, no wait) | ✅ | ✅ | +| Timeout: `positive-int` | ❌ | ✅ | +| Timeout: `negative-int` (infinite wait) | ✅ | ✅ |