Skip to content
Closed
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
10 changes: 10 additions & 0 deletions classes/exception/LockedTimeoutException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace malkusch\lock\exception;

/**
* A locked timeout was exceeded (time to wait while locked)
*/
class LockedTimeoutException extends TimeoutException
{
}
5 changes: 3 additions & 2 deletions classes/mutex/PHPRedisMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ class PHPRedisMutex extends RedisMutex
* @param string $name The lock name.
* @param int $timeout The time in seconds a lock expires after. Default is
* 3 seconds.
* @param int $lockedTimeout The maximum time in seconds we are spinning while locked.
* @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, int $timeout = 3, int $lockedTimeout = null)
{
parent::__construct($redisAPIs, $name, $timeout);
parent::__construct($redisAPIs, $name, $timeout, $lockedTimeout);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions classes/mutex/PredisMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ class PredisMutex extends RedisMutex
* @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 int $lockedTimeout The maximum time in seconds we are spinning while locked.
*
* @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, int $timeout = 3, int $lockedTimeout = null)
{
parent::__construct($clients, $name, $timeout);
parent::__construct($clients, $name, $timeout, $lockedTimeout);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions classes/mutex/RedisMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ 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 int $lockedTimeout The maximum time in seconds we are spinning while locked.
*
* @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, int $timeout = 3, int $lockedTimeout = null)
{
parent::__construct($name, $timeout);
parent::__construct($name, $timeout, $lockedTimeout);

$this->redisAPIs = $redisAPIs;
$this->logger = new NullLogger();
Expand Down
20 changes: 19 additions & 1 deletion classes/mutex/SpinlockMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace malkusch\lock\mutex;

use malkusch\lock\exception\ExecutionOutsideLockException;
use malkusch\lock\exception\LockedTimeoutException;
use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\exception\LockReleaseException;
use malkusch\lock\util\Loop;
Expand All @@ -29,6 +30,11 @@ abstract class SpinlockMutex extends LockMutex
*/
private $timeout;

/**
* @var int The timeout in seconds a process will wait while mutex is locked
*/
private $lockedTimeout;

/**
* @var \malkusch\lock\util\Loop The loop.
*/
Expand All @@ -44,22 +50,30 @@ abstract class SpinlockMutex extends LockMutex
*/
private $acquired;

/**
* @var double The timestamp before the first lock has been attempted.
*/
private $start;

/**
* Sets the timeout.
*
* @param int $timeout The time in seconds a lock expires, default is 3.
* @param int $lockedTimeout The time in seconds we are spinning while locked.
*
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(string $name, int $timeout = 3)
public function __construct(string $name, int $timeout = 3, int $lockedTimeout = null)
{
$this->timeout = $timeout;
$this->lockedTimeout = $lockedTimeout;
$this->loop = new Loop($this->timeout);
$this->key = self::PREFIX . $name;
}

protected function lock(): void
{
$this->start = microtime(true);
$this->loop->execute(function (): void {
$this->acquired = microtime(true);

Expand All @@ -72,6 +86,10 @@ protected function lock(): void
*/
if ($this->acquire($this->key, $this->timeout + 1)) {
$this->loop->end();
} elseif ($this->lockedTimeout !== null) {
if (microtime(true) - $this->start > $this->lockedTimeout) {
throw new LockedTimeoutException("Timeout while locked of $this->lockedTimeout seconds exceeded.");
}
}
});
}
Expand Down
22 changes: 22 additions & 0 deletions tests/mutex/SpinlockMutexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace malkusch\lock\mutex;

use malkusch\lock\exception\ExecutionOutsideLockException;
use malkusch\lock\exception\LockedTimeoutException;
use malkusch\lock\exception\LockAcquireException;
use phpmock\environment\SleepEnvironmentBuilder;
use phpmock\phpunit\PHPMock;
Expand Down Expand Up @@ -48,6 +49,27 @@ public function testFailAcquireLock()
});
}

/**
* Tests failing to acquire the lock due to a timeout (while lock is already taken).
*
* @expectedException \malkusch\lock\exception\LockedTimeoutException
* @expectedExceptionMessage Timeout while locked of 3 seconds exceeded.
*/
public function testLockedTimeoutExeption()
{
$timeout = 5;
$lockedTimeout = 3;
$mutex = $this->getMockForAbstractClass(SpinlockMutex::class, ['test', $timeout, $lockedTimeout]);

$mutex->expects($this->atLeastOnce())
->method('acquire')
->willReturn(false);

$mutex->synchronized(function () {
$this->fail('execution is not expected');
});
}

/**
* Tests failing to acquire the lock due to a timeout.
*
Expand Down