diff --git a/classes/exception/LockedTimeoutException.php b/classes/exception/LockedTimeoutException.php new file mode 100644 index 00000000..8b2d8dc7 --- /dev/null +++ b/classes/exception/LockedTimeoutException.php @@ -0,0 +1,10 @@ +redisAPIs = $redisAPIs; $this->logger = new NullLogger(); diff --git a/classes/mutex/SpinlockMutex.php b/classes/mutex/SpinlockMutex.php index 90a74816..4b717b6a 100644 --- a/classes/mutex/SpinlockMutex.php +++ b/classes/mutex/SpinlockMutex.php @@ -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; @@ -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. */ @@ -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); @@ -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."); + } } }); } diff --git a/tests/mutex/SpinlockMutexTest.php b/tests/mutex/SpinlockMutexTest.php index bc2a06d2..7a94dc5f 100644 --- a/tests/mutex/SpinlockMutexTest.php +++ b/tests/mutex/SpinlockMutexTest.php @@ -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; @@ -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. *