Skip to content

Commit

Permalink
feat: 🎸 Support float precision
Browse files Browse the repository at this point in the history
  • Loading branch information
mpyw committed Sep 26, 2023
1 parent f8d7918 commit 34c261f
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 24 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,11 @@ END

### Timeout Values

| | Postgres | MySQL | MariaDB |
|:-------------------------------------------|:---------------:|:-----:|:-------:|
| Timeout: `0` (default; immediate, no wait) ||||
| Timeout: `positive-int` | ✅<br>(Emulated) |||
| Timeout: `negative-int` (infinite wait) ||||
| | Postgres | MySQL | MariaDB |
|:-----------------------------------------------|:---------------:|:-----:|:-------:|
| Timeout: `0` (default; immediate, no wait) ||||
| Timeout: positive `int\|float` | ✅<br>(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.
4 changes: 2 additions & 2 deletions src/Concerns/SessionLocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/Concerns/TransactionalLocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
8 changes: 4 additions & 4 deletions src/Contracts/SessionLocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,20 @@ 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.
* QueryException may be thrown on connection-level errors.
*
* @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.
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/Contracts/TransactionLocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
4 changes: 2 additions & 2 deletions src/MySqlSessionLocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions src/PostgresSessionLocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion src/PostgresTransactionLocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
8 changes: 4 additions & 4 deletions src/Utilities/PostgresTimeoutEmulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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';
Expand Down
42 changes: 42 additions & 0 deletions tests/SessionLockerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}

0 comments on commit 34c261f

Please sign in to comment.