Skip to content

Commit 0e9cad7

Browse files
authored
Add support for fractional seconds timeout (#55)
2 parents 82d0d93 + 5a07fc7 commit 0e9cad7

16 files changed

+122
-87
lines changed

src/mutex/CASMutex.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ class CASMutex extends Mutex
2727
*
2828
* The default is 3 seconds.
2929
*
30-
* @param int $timeout The timeout in seconds.
30+
* @param float $timeout The timeout in seconds.
3131
* @throws \LengthException The timeout must be greater than 0.
3232
*/
33-
public function __construct(int $timeout = 3)
33+
public function __construct(float $timeout = 3)
3434
{
3535
$this->loop = new Loop($timeout);
3636
}

src/mutex/FlockMutex.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717
class FlockMutex extends LockMutex
1818
{
19-
public const INFINITE_TIMEOUT = -1;
19+
public const INFINITE_TIMEOUT = -1.0;
2020

2121
/**
2222
* @internal
@@ -39,22 +39,22 @@ class FlockMutex extends LockMutex
3939
private $fileHandle;
4040

4141
/**
42-
* @var int
42+
* @var float
4343
*/
4444
private $timeout;
4545

4646
/**
47-
* @var int
47+
* @var self::STRATEGY_*
4848
*/
4949
private $strategy;
5050

5151
/**
5252
* Sets the file handle.
5353
*
5454
* @param resource $fileHandle The file handle.
55-
* @param int $timeout
55+
* @param float $timeout
5656
*/
57-
public function __construct($fileHandle, int $timeout = self::INFINITE_TIMEOUT)
57+
public function __construct($fileHandle, float $timeout = self::INFINITE_TIMEOUT)
5858
{
5959
if (!is_resource($fileHandle)) {
6060
throw new \InvalidArgumentException('The file handle is not a valid resource.');
@@ -65,9 +65,12 @@ public function __construct($fileHandle, int $timeout = self::INFINITE_TIMEOUT)
6565
$this->strategy = $this->determineLockingStrategy();
6666
}
6767

68-
private function determineLockingStrategy()
68+
/**
69+
* @return self::STRATEGY_*
70+
*/
71+
private function determineLockingStrategy(): int
6972
{
70-
if ($this->timeout == self::INFINITE_TIMEOUT) {
73+
if ($this->timeout === self::INFINITE_TIMEOUT) {
7174
return self::STRATEGY_BLOCK;
7275
}
7376

@@ -94,7 +97,9 @@ private function lockBlocking(): void
9497
*/
9598
private function lockPcntl(): void
9699
{
97-
$timebox = new PcntlTimeout($this->timeout);
100+
$timeoutInt = (int) ceil($this->timeout);
101+
102+
$timebox = new PcntlTimeout($timeoutInt);
98103

99104
try {
100105
$timebox->timeBoxed(
@@ -103,7 +108,7 @@ function (): void {
103108
}
104109
);
105110
} catch (DeadlineException $e) {
106-
throw TimeoutException::create($this->timeout);
111+
throw TimeoutException::create($timeoutInt);
107112
}
108113
}
109114

src/mutex/MemcachedMutex.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,24 @@ class MemcachedMutex extends SpinlockMutex
2424
*
2525
* @param string $name The lock name.
2626
* @param Memcached $memcache The connected Memcached API.
27-
* @param int $timeout The time in seconds a lock expires, default is 3.
27+
* @param float $timeout The time in seconds a lock expires, default is 3.
2828
*
2929
* @throws \LengthException The timeout must be greater than 0.
3030
*/
31-
public function __construct(string $name, Memcached $memcache, int $timeout = 3)
31+
public function __construct(string $name, Memcached $memcache, float $timeout = 3)
3232
{
3333
parent::__construct($name, $timeout);
3434

3535
$this->memcache = $memcache;
3636
}
3737

38-
protected function acquire(string $key, int $expire): bool
38+
protected function acquire(string $key, float $expire): bool
3939
{
40-
return $this->memcache->add($key, true, $expire);
40+
// memcached supports only integer expire
41+
// https://github.com/memcached/memcached/wiki/Commands#standard-protocol
42+
$expireInt = (int) ceil($expire);
43+
44+
return $this->memcache->add($key, true, $expireInt);
4145
}
4246

4347
protected function release(string $key): bool

src/mutex/MySQLMutex.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ class MySQLMutex extends LockMutex
2020
*/
2121
private $name;
2222
/**
23-
* @var int
23+
* @var float
2424
*/
2525
private $timeout;
2626

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

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

46+
// MySQL rounds the value to whole seconds, sadly rounds, not ceils
47+
// TODO MariaDB supports microseconds precision since 10.1.2 version,
48+
// but we need to detect the support reliably first
49+
// https://github.com/MariaDB/server/commit/3e792e6cbccb5d7bf5b84b38336f8a40ad232020
50+
$timeoutInt = (int) ceil($this->timeout);
51+
4652
$statement->execute([
4753
$this->name,
48-
$this->timeout,
54+
$timeoutInt,
4955
]);
5056

5157
$statement->setFetchMode(\PDO::FETCH_NUM);

src/mutex/PHPRedisMutex.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ class PHPRedisMutex extends RedisMutex
2929
* called already.
3030
*
3131
* @param array<\Redis|\RedisCluster> $redisAPIs The Redis connections.
32-
* @param string $name The lock name.
33-
* @param int $timeout The time in seconds a lock expires after. Default is
34-
* 3 seconds.
32+
* @param string $name The lock name.
33+
* @param float $timeout The time in seconds a lock expires after. Default is
34+
* 3 seconds.
3535
* @throws \LengthException The timeout must be greater than 0.
3636
*/
37-
public function __construct(array $redisAPIs, string $name, int $timeout = 3)
37+
public function __construct(array $redisAPIs, string $name, float $timeout = 3)
3838
{
3939
parent::__construct($redisAPIs, $name, $timeout);
4040
}
@@ -43,12 +43,14 @@ public function __construct(array $redisAPIs, string $name, int $timeout = 3)
4343
* @param \Redis|\RedisCluster $redisAPI The Redis or RedisCluster connection.
4444
* @throws LockAcquireException
4545
*/
46-
protected function add($redisAPI, string $key, string $value, int $expire): bool
46+
protected function add($redisAPI, string $key, string $value, float $expire): bool
4747
{
48+
$expireMillis = (int) ceil($expire * 1000);
49+
4850
/** @var \Redis $redisAPI */
4951
try {
5052
// Will set the key, if it doesn't exist, with a ttl of $expire seconds
51-
return $redisAPI->set($key, $value, ['nx', 'ex' => $expire]);
53+
return $redisAPI->set($key, $value, ['nx', 'px' => $expireMillis]);
5254
} catch (RedisException $e) {
5355
$message = sprintf(
5456
"Failed to acquire lock for key '%s'",

src/mutex/PredisMutex.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,26 @@ class PredisMutex extends RedisMutex
2020
* Sets the Redis connections.
2121
*
2222
* @param ClientInterface[] $clients The Redis clients.
23-
* @param string $name The lock name.
24-
* @param int $timeout The time in seconds a lock expires, default is 3.
23+
* @param string $name The lock name.
24+
* @param float $timeout The time in seconds a lock expires, default is 3.
2525
*
2626
* @throws \LengthException The timeout must be greater than 0.
2727
*/
28-
public function __construct(array $clients, string $name, int $timeout = 3)
28+
public function __construct(array $clients, string $name, float $timeout = 3)
2929
{
3030
parent::__construct($clients, $name, $timeout);
3131
}
3232

3333
/**
3434
* @throws LockAcquireException
3535
*/
36-
protected function add($redisAPI, string $key, string $value, int $expire): bool
36+
protected function add($redisAPI, string $key, string $value, float $expire): bool
3737
{
38+
$expireMillis = (int) ceil($expire * 1000);
39+
3840
/** @var ClientInterface $redisAPI */
3941
try {
40-
return $redisAPI->set($key, $value, 'EX', $expire, 'NX') !== null;
42+
return $redisAPI->set($key, $value, 'PX', $expireMillis, 'NX') !== null;
4143
} catch (PredisException $e) {
4244
$message = sprintf(
4345
"Failed to acquire lock for key '%s'",

src/mutex/RedisMutex.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,19 @@ abstract class RedisMutex extends SpinlockMutex implements LoggerAwareInterface
3434
*
3535
* @param array $redisAPIs The Redis APIs.
3636
* @param string $name The lock name.
37-
* @param int $timeout The time in seconds a lock expires, default is 3.
37+
* @param float $timeout The time in seconds a lock expires, default is 3.
3838
*
3939
* @throws \LengthException The timeout must be greater than 0.
4040
*/
41-
public function __construct(array $redisAPIs, string $name, int $timeout = 3)
41+
public function __construct(array $redisAPIs, string $name, float $timeout = 3)
4242
{
4343
parent::__construct($name, $timeout);
4444

4545
$this->redisAPIs = $redisAPIs;
4646
$this->logger = new NullLogger();
4747
}
4848

49-
protected function acquire(string $key, int $expire): bool
49+
protected function acquire(string $key, float $expire): bool
5050
{
5151
// 1. This differs from the specification to avoid an overflow on 32-Bit systems.
5252
$time = microtime(true);
@@ -149,14 +149,14 @@ private function isMajority(int $count): bool
149149
/**
150150
* Sets the key only if such key doesn't exist at the server yet.
151151
*
152-
* @param mixed $redisAPI The connected Redis API.
153-
* @param string $key The key.
154-
* @param string $value The value.
155-
* @param int $expire The TTL seconds.
152+
* @param mixed $redisAPI The connected Redis API.
153+
* @param string $key The key.
154+
* @param string $value The value.
155+
* @param float $expire The TTL seconds.
156156
*
157157
* @return bool True, if the key was set.
158158
*/
159-
abstract protected function add($redisAPI, string $key, string $value, int $expire): bool;
159+
abstract protected function add($redisAPI, string $key, string $value, float $expire): bool;
160160

161161
/**
162162
* @param mixed $redisAPI The connected Redis API.

src/mutex/SpinlockMutex.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ abstract class SpinlockMutex extends LockMutex
2222
private const PREFIX = 'lock_';
2323

2424
/**
25-
* @var int The timeout in seconds a lock may live.
25+
* @var float The timeout in seconds a lock may live.
2626
*/
2727
private $timeout;
2828

@@ -44,11 +44,11 @@ abstract class SpinlockMutex extends LockMutex
4444
/**
4545
* Sets the timeout.
4646
*
47-
* @param int $timeout The time in seconds a lock expires, default is 3.
47+
* @param float $timeout The time in seconds a lock expires, default is 3.
4848
*
4949
* @throws \LengthException The timeout must be greater than 0.
5050
*/
51-
public function __construct(string $name, int $timeout = 3)
51+
public function __construct(string $name, float $timeout = 3)
5252
{
5353
$this->timeout = $timeout;
5454
$this->loop = new Loop($this->timeout);
@@ -92,13 +92,13 @@ protected function unlock(): void
9292
/**
9393
* Tries to acquire a lock.
9494
*
95-
* @param string $key The lock key.
96-
* @param int $expire The timeout in seconds when a lock expires.
95+
* @param string $key The lock key.
96+
* @param float $expire The timeout in seconds when a lock expires.
9797
*
9898
* @throws LockAcquireException An unexpected error happened.
9999
* @return bool True, if the lock could be acquired.
100100
*/
101-
abstract protected function acquire(string $key, int $expire): bool;
101+
abstract protected function acquire(string $key, float $expire): bool;
102102

103103
/**
104104
* Tries to release a lock.

src/mutex/TransactionalMutex.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ class TransactionalMutex extends Mutex
4040
* As this implementation spans a transaction over a unit of work,
4141
* PDO::ATTR_AUTOCOMMIT SHOULD not be enabled.
4242
*
43-
* @param \PDO $pdo The PDO.
44-
* @param int $timeout The timeout in seconds, default is 3.
43+
* @param \PDO $pdo The PDO.
44+
* @param float $timeout The timeout in seconds, default is 3.
4545
*
4646
* @throws \LengthException The timeout must be greater than 0.
4747
*/
48-
public function __construct(\PDO $pdo, int $timeout = 3)
48+
public function __construct(\PDO $pdo, float $timeout = 3)
4949
{
5050
if ($pdo->getAttribute(\PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) {
5151
throw new InvalidArgumentException('The pdo must have PDO::ERRMODE_EXCEPTION set.');

src/util/Loop.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class Loop
2929
private const MAXIMUM_WAIT_US = 5e5; // 0.50 seconds
3030

3131
/**
32-
* @var int The timeout in seconds.
32+
* @var float The timeout in seconds.
3333
*/
3434
private $timeout;
3535

@@ -41,10 +41,10 @@ class Loop
4141
/**
4242
* Sets the timeout. The default is 3 seconds.
4343
*
44-
* @param int $timeout The timeout in seconds. The default is 3 seconds.
44+
* @param float $timeout The timeout in seconds. The default is 3 seconds.
4545
* @throws \LengthException The timeout must be greater than 0.
4646
*/
47-
public function __construct(int $timeout = 3)
47+
public function __construct(float $timeout = 3)
4848
{
4949
if ($timeout <= 0) {
5050
throw new LengthException(\sprintf(

0 commit comments

Comments
 (0)