From 34c261fafa895bfd99793338b493580745046fbb Mon Sep 17 00:00:00 2001 From: mpyw Date: Tue, 26 Sep 2023 10:53:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Support=20float=20precis?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +++--- src/Concerns/SessionLocks.php | 4 +-- src/Concerns/TransactionalLocks.php | 4 +-- src/Contracts/SessionLocker.php | 8 ++--- src/Contracts/TransactionLocker.php | 4 +-- src/MySqlSessionLocker.php | 4 +-- src/PostgresSessionLocker.php | 4 +-- src/PostgresTransactionLocker.php | 2 +- src/Utilities/PostgresTimeoutEmulator.php | 8 ++--- tests/SessionLockerTest.php | 42 +++++++++++++++++++++++ 10 files changed, 66 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 3f1da65..b4be566 100644 --- a/README.md +++ b/README.md @@ -156,11 +156,11 @@ END ### Timeout Values -| | Postgres | MySQL | MariaDB | -|:-------------------------------------------|:---------------:|:-----:|:-------:| -| Timeout: `0` (default; immediate, no wait) | ✅ | ✅ | ✅ | -| Timeout: `positive-int` | ✅
(Emulated) | ✅ | ✅ | -| Timeout: `negative-int` (infinite wait) | ✅ | ✅ | ❌ | +| | Postgres | MySQL | MariaDB | +|:-----------------------------------------------|:---------------:|:-----:|:-------:| +| Timeout: `0` (default; immediate, no wait) | ✅ | ✅ | ✅ | +| Timeout: positive `int\|float` | ✅
(Emulated) | ✅ | ✅ | +| Timeout: negative `int\|float` (infinite wait) | ✅ | ✅ | ❌ | - Postgres does not natively support waiting for a finite specific amount of time, but this is emulated by looping through a temporary function. - MariaDB does not accept infinite timeouts. very large numbers can be used instead. diff --git a/src/Concerns/SessionLocks.php b/src/Concerns/SessionLocks.php index d5f6048..ce5dc0e 100644 --- a/src/Concerns/SessionLocks.php +++ b/src/Concerns/SessionLocks.php @@ -9,9 +9,9 @@ trait SessionLocks { - abstract public function lockOrFail(string $key, int $timeout = 0): SessionLock; + abstract public function lockOrFail(string $key, int|float $timeout = 0): SessionLock; - public function tryLock(string $key, int $timeout = 0): ?SessionLock + public function tryLock(string $key, int|float $timeout = 0): ?SessionLock { try { return $this->lockOrFail($key, $timeout); diff --git a/src/Concerns/TransactionalLocks.php b/src/Concerns/TransactionalLocks.php index a3da396..6d1a206 100644 --- a/src/Concerns/TransactionalLocks.php +++ b/src/Concerns/TransactionalLocks.php @@ -8,7 +8,7 @@ trait TransactionalLocks { - public function tryLock(string $key, int $timeout = 0): bool + public function tryLock(string $key, int|float $timeout = 0): bool { try { $this->lockOrFail($key, $timeout); @@ -19,5 +19,5 @@ public function tryLock(string $key, int $timeout = 0): bool } } - abstract public function lockOrFail(string $key, int $timeout = 0): void; + abstract public function lockOrFail(string $key, int|float $timeout = 0): void; } diff --git a/src/Contracts/SessionLocker.php b/src/Contracts/SessionLocker.php index 5a086cc..af41c67 100644 --- a/src/Contracts/SessionLocker.php +++ b/src/Contracts/SessionLocker.php @@ -27,12 +27,12 @@ interface SessionLocker * @psalm-param callable(ConnectionInterface): T $callback * @psalm-return T * - * @param int $timeout Time to wait before acquiring a lock. This is NOT the expiry of the lock. + * @param int|float $timeout Time to wait before acquiring a lock. This is NOT the expiry of the lock. * * @throws LockFailedException * @throws QueryException */ - public function withLocking(string $key, callable $callback, int $timeout = 0): mixed; + public function withLocking(string $key, callable $callback, int|float $timeout = 0): mixed; /** * Attempts to acquire a lock or returns NULL if failed. @@ -40,7 +40,7 @@ public function withLocking(string $key, callable $callback, int $timeout = 0): * * @throws QueryException */ - public function tryLock(string $key, int $timeout = 0): ?SessionLock; + public function tryLock(string $key, int|float $timeout = 0): ?SessionLock; /** * Attempts to acquire a lock or throw LockFailedException if failed. @@ -49,7 +49,7 @@ public function tryLock(string $key, int $timeout = 0): ?SessionLock; * @throws LockFailedException * @throws QueryException */ - public function lockOrFail(string $key, int $timeout = 0): SessionLock; + public function lockOrFail(string $key, int|float $timeout = 0): SessionLock; /** * Indicates whether any session-level lock remains. diff --git a/src/Contracts/TransactionLocker.php b/src/Contracts/TransactionLocker.php index 19e5992..8eb026f 100644 --- a/src/Contracts/TransactionLocker.php +++ b/src/Contracts/TransactionLocker.php @@ -20,7 +20,7 @@ interface TransactionLocker * * @throws QueryException */ - public function tryLock(string $key, int $timeout = 0): bool; + public function tryLock(string $key, int|float $timeout = 0): bool; /** * Attempts to acquire a lock or throw LockFailedException if failed. @@ -29,5 +29,5 @@ public function tryLock(string $key, int $timeout = 0): bool; * @throws LockFailedException * @throws QueryException */ - public function lockOrFail(string $key, int $timeout = 0): void; + public function lockOrFail(string $key, int|float $timeout = 0): void; } diff --git a/src/MySqlSessionLocker.php b/src/MySqlSessionLocker.php index 68503a9..20553b4 100644 --- a/src/MySqlSessionLocker.php +++ b/src/MySqlSessionLocker.php @@ -29,7 +29,7 @@ public function __construct( $this->locks = new WeakMap(); } - public function lockOrFail(string $key, int $timeout = 0): SessionLock + public function lockOrFail(string $key, int|float $timeout = 0): SessionLock { // When key strings exceed 64 chars limit, // it takes first 24 chars from them and appends 40 chars `sha1()` hashes. @@ -55,7 +55,7 @@ public function lockOrFail(string $key, int $timeout = 0): SessionLock return $lock; } - public function withLocking(string $key, callable $callback, int $timeout = 0): mixed + public function withLocking(string $key, callable $callback, int|float $timeout = 0): mixed { $lock = $this->lockOrFail($key, $timeout); diff --git a/src/PostgresSessionLocker.php b/src/PostgresSessionLocker.php index 8c68c79..1d519cb 100644 --- a/src/PostgresSessionLocker.php +++ b/src/PostgresSessionLocker.php @@ -33,7 +33,7 @@ public function __construct( * * Use of this method is strongly discouraged in Postgres. Use withLocking() instead. */ - public function lockOrFail(string $key, int $timeout = 0): SessionLock + public function lockOrFail(string $key, int|float $timeout = 0): SessionLock { if ($timeout > 0) { // Positive timeout can be performed through temporary function @@ -67,7 +67,7 @@ public function lockOrFail(string $key, int $timeout = 0): SessionLock return $lock; } - public function withLocking(string $key, callable $callback, int $timeout = 0): mixed + public function withLocking(string $key, callable $callback, int|float $timeout = 0): mixed { $lock = $this->lockOrFail($key, $timeout); diff --git a/src/PostgresTransactionLocker.php b/src/PostgresTransactionLocker.php index 53f8b16..66cf96b 100644 --- a/src/PostgresTransactionLocker.php +++ b/src/PostgresTransactionLocker.php @@ -20,7 +20,7 @@ public function __construct( protected PostgresConnection $connection, ) {} - public function lockOrFail(string $key, int $timeout = 0): void + public function lockOrFail(string $key, int|float $timeout = 0): void { if ($this->connection->transactionLevel() < 1) { throw new InvalidTransactionLevelException('There are no transactions'); diff --git a/src/Utilities/PostgresTimeoutEmulator.php b/src/Utilities/PostgresTimeoutEmulator.php index 06dd573..7a21aac 100644 --- a/src/Utilities/PostgresTimeoutEmulator.php +++ b/src/Utilities/PostgresTimeoutEmulator.php @@ -23,10 +23,10 @@ public function __construct( /** * Perform a time-limited lock acquisition. * - * @phpstan-param positive-int $timeout + * @phpstan-param positive-int|float $timeout * @throws QueryException */ - public function performWithTimeout(string $key, int $timeout, bool $forTransaction = false): bool + public function performWithTimeout(string $key, int|float $timeout, bool $forTransaction = false): bool { // Binding parameters to procedures is only allowed when PDOStatement emulation is enabled. return PDOStatementEmulator::emulated( @@ -39,9 +39,9 @@ public function performWithTimeout(string $key, int $timeout, bool $forTransacti /** * Generates SQL to emulate time-limited lock acquisition. * - * @phpstan-param positive-int $timeout + * @phpstan-param positive-int|float $timeout */ - public function sql(int $timeout, bool $forTransaction): string + public function sql(int|float $timeout, bool $forTransaction): string { $suffix = $forTransaction ? '_xact' : ''; $modifier = $forTransaction ? 'LOCAL' : 'SESSION'; diff --git a/tests/SessionLockerTest.php b/tests/SessionLockerTest.php index dad5e31..653a6a1 100644 --- a/tests/SessionLockerTest.php +++ b/tests/SessionLockerTest.php @@ -243,4 +243,46 @@ public function testInfiniteTimeoutSuccess(string $name): void $proc->wait(); } } + + /** + * @dataProvider connectionsAll + */ + public function testFloatTimeoutSuccess(string $name): void + { + $proc = self::lockAsync($name, 'foo', 2); + usleep(1_800_000); + + try { + $result = DB::connection($name) + ->advisoryLocker() + ->forSession() + ->tryLock('foo', 0.4); + + $this->assertSame(0, $proc->wait()); + $this->assertNotNull($result); + } finally { + $proc->wait(); + } + } + + /** + * @dataProvider connectionsAll + */ + public function testFloatTimeoutExceeded(string $name): void + { + $proc = self::lockAsync($name, 'foo', 2); + usleep(1_700_000); + + try { + $result = DB::connection($name) + ->advisoryLocker() + ->forSession() + ->tryLock('foo', 0.1); + + $this->assertSame(0, $proc->wait()); + $this->assertNull($result); + } finally { + $proc->wait(); + } + } }