Skip to content

Add support for fractional seconds timeout #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/mutex/CASMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ class CASMutex extends Mutex
*
* The default is 3 seconds.
*
* @param int $timeout The timeout in seconds.
* @param float $timeout The timeout in seconds.
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(int $timeout = 3)
public function __construct(float $timeout = 3)
{
$this->loop = new Loop($timeout);
}
Expand Down
23 changes: 14 additions & 9 deletions src/mutex/FlockMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
class FlockMutex extends LockMutex
{
public const INFINITE_TIMEOUT = -1;
public const INFINITE_TIMEOUT = -1.0;

/**
* @internal
Expand All @@ -39,22 +39,22 @@ class FlockMutex extends LockMutex
private $fileHandle;

/**
* @var int
* @var float
*/
private $timeout;

/**
* @var int
* @var self::STRATEGY_*
*/
private $strategy;

/**
* Sets the file handle.
*
* @param resource $fileHandle The file handle.
* @param int $timeout
* @param float $timeout
*/
public function __construct($fileHandle, int $timeout = self::INFINITE_TIMEOUT)
public function __construct($fileHandle, float $timeout = self::INFINITE_TIMEOUT)
{
if (!is_resource($fileHandle)) {
throw new \InvalidArgumentException('The file handle is not a valid resource.');
Expand All @@ -65,9 +65,12 @@ public function __construct($fileHandle, int $timeout = self::INFINITE_TIMEOUT)
$this->strategy = $this->determineLockingStrategy();
}

private function determineLockingStrategy()
/**
* @return self::STRATEGY_*
*/
private function determineLockingStrategy(): int
{
if ($this->timeout == self::INFINITE_TIMEOUT) {
if ($this->timeout === self::INFINITE_TIMEOUT) {
return self::STRATEGY_BLOCK;
}

Expand All @@ -94,7 +97,9 @@ private function lockBlocking(): void
*/
private function lockPcntl(): void
{
$timebox = new PcntlTimeout($this->timeout);
$timeoutInt = (int) ceil($this->timeout);

$timebox = new PcntlTimeout($timeoutInt);

try {
$timebox->timeBoxed(
Expand All @@ -103,7 +108,7 @@ function (): void {
}
);
} catch (DeadlineException $e) {
throw TimeoutException::create($this->timeout);
throw TimeoutException::create($timeoutInt);
}
}

Expand Down
12 changes: 8 additions & 4 deletions src/mutex/MemcachedMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,24 @@ class MemcachedMutex extends SpinlockMutex
*
* @param string $name The lock name.
* @param Memcached $memcache The connected Memcached API.
* @param int $timeout The time in seconds a lock expires, default is 3.
* @param float $timeout The time in seconds a lock expires, default is 3.
*
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(string $name, Memcached $memcache, int $timeout = 3)
public function __construct(string $name, Memcached $memcache, float $timeout = 3)
{
parent::__construct($name, $timeout);

$this->memcache = $memcache;
}

protected function acquire(string $key, int $expire): bool
protected function acquire(string $key, float $expire): bool
{
return $this->memcache->add($key, true, $expire);
// memcached supports only integer expire
// https://github.com/memcached/memcached/wiki/Commands#standard-protocol
$expireInt = (int) ceil($expire);

return $this->memcache->add($key, true, $expireInt);
}

protected function release(string $key): bool
Expand Down
12 changes: 9 additions & 3 deletions src/mutex/MySQLMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ class MySQLMutex extends LockMutex
*/
private $name;
/**
* @var int
* @var float
*/
private $timeout;

public function __construct(\PDO $PDO, string $name, int $timeout = 0)
public function __construct(\PDO $PDO, string $name, float $timeout = 0)
{
$this->pdo = $PDO;

Expand All @@ -43,9 +43,15 @@ public function lock(): void
{
$statement = $this->pdo->prepare('SELECT GET_LOCK(?,?)');

// MySQL rounds the value to whole seconds, sadly rounds, not ceils
// TODO MariaDB supports microseconds precision since 10.1.2 version,
// but we need to detect the support reliably first
// https://github.com/MariaDB/server/commit/3e792e6cbccb5d7bf5b84b38336f8a40ad232020
$timeoutInt = (int) ceil($this->timeout);

$statement->execute([
$this->name,
$this->timeout,
$timeoutInt,
]);

$statement->setFetchMode(\PDO::FETCH_NUM);
Expand Down
14 changes: 8 additions & 6 deletions src/mutex/PHPRedisMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ class PHPRedisMutex extends RedisMutex
* called already.
*
* @param array<\Redis|\RedisCluster> $redisAPIs The Redis connections.
* @param string $name The lock name.
* @param int $timeout The time in seconds a lock expires after. Default is
* 3 seconds.
* @param string $name The lock name.
* @param float $timeout The time in seconds a lock expires after. Default is
* 3 seconds.
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(array $redisAPIs, string $name, int $timeout = 3)
public function __construct(array $redisAPIs, string $name, float $timeout = 3)
{
parent::__construct($redisAPIs, $name, $timeout);
}
Expand All @@ -43,12 +43,14 @@ public function __construct(array $redisAPIs, string $name, int $timeout = 3)
* @param \Redis|\RedisCluster $redisAPI The Redis or RedisCluster connection.
* @throws LockAcquireException
*/
protected function add($redisAPI, string $key, string $value, int $expire): bool
protected function add($redisAPI, string $key, string $value, float $expire): bool
{
$expireMillis = (int) ceil($expire * 1000);

/** @var \Redis $redisAPI */
try {
// Will set the key, if it doesn't exist, with a ttl of $expire seconds
return $redisAPI->set($key, $value, ['nx', 'ex' => $expire]);
return $redisAPI->set($key, $value, ['nx', 'px' => $expireMillis]);
} catch (RedisException $e) {
$message = sprintf(
"Failed to acquire lock for key '%s'",
Expand Down
12 changes: 7 additions & 5 deletions src/mutex/PredisMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,26 @@ class PredisMutex extends RedisMutex
* Sets the Redis connections.
*
* @param ClientInterface[] $clients The Redis clients.
* @param string $name The lock name.
* @param int $timeout The time in seconds a lock expires, default is 3.
* @param string $name The lock name.
* @param float $timeout The time in seconds a lock expires, default is 3.
*
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(array $clients, string $name, int $timeout = 3)
public function __construct(array $clients, string $name, float $timeout = 3)
{
parent::__construct($clients, $name, $timeout);
}

/**
* @throws LockAcquireException
*/
protected function add($redisAPI, string $key, string $value, int $expire): bool
protected function add($redisAPI, string $key, string $value, float $expire): bool
{
$expireMillis = (int) ceil($expire * 1000);

/** @var ClientInterface $redisAPI */
try {
return $redisAPI->set($key, $value, 'EX', $expire, 'NX') !== null;
return $redisAPI->set($key, $value, 'PX', $expireMillis, 'NX') !== null;
} catch (PredisException $e) {
$message = sprintf(
"Failed to acquire lock for key '%s'",
Expand Down
16 changes: 8 additions & 8 deletions src/mutex/RedisMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,19 @@ abstract class RedisMutex extends SpinlockMutex implements LoggerAwareInterface
*
* @param array $redisAPIs The Redis APIs.
* @param string $name The lock name.
* @param int $timeout The time in seconds a lock expires, default is 3.
* @param float $timeout The time in seconds a lock expires, default is 3.
*
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(array $redisAPIs, string $name, int $timeout = 3)
public function __construct(array $redisAPIs, string $name, float $timeout = 3)
{
parent::__construct($name, $timeout);

$this->redisAPIs = $redisAPIs;
$this->logger = new NullLogger();
}

protected function acquire(string $key, int $expire): bool
protected function acquire(string $key, float $expire): bool
{
// 1. This differs from the specification to avoid an overflow on 32-Bit systems.
$time = microtime(true);
Expand Down Expand Up @@ -149,14 +149,14 @@ private function isMajority(int $count): bool
/**
* Sets the key only if such key doesn't exist at the server yet.
*
* @param mixed $redisAPI The connected Redis API.
* @param string $key The key.
* @param string $value The value.
* @param int $expire The TTL seconds.
* @param mixed $redisAPI The connected Redis API.
* @param string $key The key.
* @param string $value The value.
* @param float $expire The TTL seconds.
*
* @return bool True, if the key was set.
*/
abstract protected function add($redisAPI, string $key, string $value, int $expire): bool;
abstract protected function add($redisAPI, string $key, string $value, float $expire): bool;

/**
* @param mixed $redisAPI The connected Redis API.
Expand Down
12 changes: 6 additions & 6 deletions src/mutex/SpinlockMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ abstract class SpinlockMutex extends LockMutex
private const PREFIX = 'lock_';

/**
* @var int The timeout in seconds a lock may live.
* @var float The timeout in seconds a lock may live.
*/
private $timeout;

Expand All @@ -44,11 +44,11 @@ abstract class SpinlockMutex extends LockMutex
/**
* Sets the timeout.
*
* @param int $timeout The time in seconds a lock expires, default is 3.
* @param float $timeout The time in seconds a lock expires, default is 3.
*
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(string $name, int $timeout = 3)
public function __construct(string $name, float $timeout = 3)
{
$this->timeout = $timeout;
$this->loop = new Loop($this->timeout);
Expand Down Expand Up @@ -92,13 +92,13 @@ protected function unlock(): void
/**
* Tries to acquire a lock.
*
* @param string $key The lock key.
* @param int $expire The timeout in seconds when a lock expires.
* @param string $key The lock key.
* @param float $expire The timeout in seconds when a lock expires.
*
* @throws LockAcquireException An unexpected error happened.
* @return bool True, if the lock could be acquired.
*/
abstract protected function acquire(string $key, int $expire): bool;
abstract protected function acquire(string $key, float $expire): bool;

/**
* Tries to release a lock.
Expand Down
6 changes: 3 additions & 3 deletions src/mutex/TransactionalMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ class TransactionalMutex extends Mutex
* As this implementation spans a transaction over a unit of work,
* PDO::ATTR_AUTOCOMMIT SHOULD not be enabled.
*
* @param \PDO $pdo The PDO.
* @param int $timeout The timeout in seconds, default is 3.
* @param \PDO $pdo The PDO.
* @param float $timeout The timeout in seconds, default is 3.
*
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(\PDO $pdo, int $timeout = 3)
public function __construct(\PDO $pdo, float $timeout = 3)
{
if ($pdo->getAttribute(\PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) {
throw new InvalidArgumentException('The pdo must have PDO::ERRMODE_EXCEPTION set.');
Expand Down
6 changes: 3 additions & 3 deletions src/util/Loop.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Loop
private const MAXIMUM_WAIT_US = 5e5; // 0.50 seconds

/**
* @var int The timeout in seconds.
* @var float The timeout in seconds.
*/
private $timeout;

Expand All @@ -41,10 +41,10 @@ class Loop
/**
* Sets the timeout. The default is 3 seconds.
*
* @param int $timeout The timeout in seconds. The default is 3 seconds.
* @param float $timeout The timeout in seconds. The default is 3 seconds.
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(int $timeout = 3)
public function __construct(float $timeout = 3)
{
if ($timeout <= 0) {
throw new LengthException(\sprintf(
Expand Down
2 changes: 2 additions & 0 deletions src/util/PcntlTimeout.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
*
* This class requires the pcntl module and supports the cli sapi only.
*
* Only integer timeout is supported - https://github.com/php/php-src/issues/11828.
*
* @internal
*/
final class PcntlTimeout
Expand Down
10 changes: 5 additions & 5 deletions tests/mutex/MutexConcurrencyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,18 +197,18 @@ function ($timeout = 3) use ($dsn, $user, $password) {
*/
public function testExecutionIsSerializedWhenLocked(callable $mutexFactory)
{
$timestamp = hrtime(true);
$time = \microtime(true);

$this->fork(5, function () use ($mutexFactory): void {
$this->fork(6, function () use ($mutexFactory): void {
/** @var Mutex $mutex */
$mutex = $mutexFactory();
$mutex->synchronized(function (): void {
\usleep(200000);
\usleep(200 * 1000);
});
});

$delta = \hrtime(true) - $timestamp;
$this->assertGreaterThan(1e9, $delta);
$delta = \microtime(true) - $time;
$this->assertGreaterThan(1.201, $delta);
}

/**
Expand Down
Loading
Loading