diff --git a/README.md b/README.md index aa53d5e7..dfbe72f4 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ method guarantees that the code is only executed by one process at once. Other processes have to wait until the mutex is available. The critical code may throw an exception, which would release the lock as well. -This method returns what ever is returned to the given callable. The return +This method returns whatever is returned to the given callable. The return value is not checked, thus it is up to the user to decide if for example the return value `false` or `null` should be seen as a failed action. @@ -119,6 +119,39 @@ if (false === $newBalance) { } ``` +### Extracting code result after lock release exception + +Mutex implementations based on [`malkush\lock\mutex\LockMutex`][12] will throw +[`malkusch\lock\exception\LockReleaseException`][13] in case of lock release +problem, but the synchronized code block will be already executed at this point. +In order to read the code result (or an exception thrown there), +`LockReleaseException` provides methods to extract it. + +Example: +```php +try { + // or $mutex->check(...) + $mutex->synchronized(function () { + if (someCondition()) { + throw new \DomainException(); + } + + return "result"; + }); +} catch (LockReleaseException $unlock_exception) { + if ($unlock_exception->getCodeException() !== null) { + $code_exception = $unlock_exception->getCodeException() + // do something with the code exception + } else { + $code_result = $unlock_exception->getCodeResult(); + // do something with the code result + } + + // deal with LockReleaseException or propagate it + throw $unlock_exception; +} +``` + ### Implementations Because the [`malkusch\lock\mutex\Mutex`](#mutex) class is an abstract class, @@ -374,3 +407,5 @@ If you like this project and feel generous donate a few Bitcoins here: [9]: https://en.wikipedia.org/wiki/Double-checked_locking [10]: https://en.wikipedia.org/wiki/Compare-and-swap [11]: https://github.com/php-lock/lock/blob/master/classes/mutex/CASMutex.php#L44 +[12]: https://github.com/php-lock/lock/blob/master/classes/mutex/LockMutex.php +[13]: https://github.com/php-lock/lock/blob/master/classes/exception/LockReleaseException.php \ No newline at end of file diff --git a/classes/exception/LockReleaseException.php b/classes/exception/LockReleaseException.php index 5b14a44f..b7ec4362 100644 --- a/classes/exception/LockReleaseException.php +++ b/classes/exception/LockReleaseException.php @@ -16,4 +16,45 @@ class LockReleaseException extends MutexException { + /** + * @var mixed + */ + private $code_result; + + /** + * @var \Throwable|null + */ + private $code_exception; + + /** + * @return mixed The return value of the executed code block. + */ + public function getCodeResult() + { + return $this->code_result; + } + + /** + * @param mixed $code_result The return value of the executed code block. + */ + public function setCodeResult($code_result): void + { + $this->code_result = $code_result; + } + + /** + * @return \Throwable|null The exception thrown by the code block or null when there was no exception. + */ + public function getCodeException(): ?\Throwable + { + return $this->code_exception; + } + + /** + * @param \Throwable $code_exception The exception thrown by the code block. + */ + public function setCodeException(\Throwable $code_exception): void + { + $this->code_exception = $code_exception; + } } diff --git a/classes/mutex/LockMutex.php b/classes/mutex/LockMutex.php index d94bd614..b26aed1b 100644 --- a/classes/mutex/LockMutex.php +++ b/classes/mutex/LockMutex.php @@ -2,8 +2,8 @@ namespace malkusch\lock\mutex; -use malkusch\lock\exception\LockReleaseException; use malkusch\lock\exception\LockAcquireException; +use malkusch\lock\exception\LockReleaseException; /** * Locking mutex. @@ -31,14 +31,32 @@ abstract protected function lock(): void; * @throws LockReleaseException The lock could not be released. */ abstract protected function unlock(): void; - + public function synchronized(callable $code) { $this->lock(); + + $code_result = null; + $code_exception = null; try { - return $code(); + $code_result = $code(); + } catch (\Throwable $exception) { + $code_exception = $exception; + + throw $exception; } finally { - $this->unlock(); + try { + $this->unlock(); + } catch (LockReleaseException $lock_exception) { + $lock_exception->setCodeResult($code_result); + if ($code_exception !== null) { + $lock_exception->setCodeException($code_exception); + } + + throw $lock_exception; + } } + + return $code_result; } } diff --git a/tests/mutex/LockMutexTest.php b/tests/mutex/LockMutexTest.php index 18f42b7a..49aba766 100644 --- a/tests/mutex/LockMutexTest.php +++ b/tests/mutex/LockMutexTest.php @@ -92,13 +92,11 @@ public function testUnlockFailsAfterCode() /** * Tests unlock() fails after the code threw an exception. * - * The previous exception should be the code's exception. - * * @expectedException malkusch\lock\exception\LockReleaseException */ public function testUnlockFailsAfterException() { - $this->mutex->expects($this->any()) + $this->mutex->expects($this->once()) ->method("unlock") ->willThrowException(new LockReleaseException()); @@ -106,4 +104,42 @@ public function testUnlockFailsAfterException() throw new \DomainException(); }); } + + /** + * Tests the code result is available in LockReleaseException. + */ + public function testCodeResultAvailableAfterFailedUnlock() + { + $this->mutex->expects($this->once()) + ->method("unlock") + ->willThrowException(new LockReleaseException()); + + try { + $this->mutex->synchronized(function () { + return "result"; + }); + } catch (LockReleaseException $exception) { + $this->assertEquals("result", $exception->getCodeResult()); + $this->assertNull($exception->getCodeException()); + } + } + + /** + * Tests the code exception is available in LockReleaseException. + */ + public function testCodeExceptionAvailableAfterFailedUnlock() + { + $this->mutex->expects($this->once()) + ->method("unlock") + ->willThrowException(new LockReleaseException()); + + try { + $this->mutex->synchronized(function () { + throw new \DomainException("Domain exception"); + }); + } catch (LockReleaseException $exception) { + $this->assertInstanceOf(\DomainException::class, $exception->getCodeException()); + $this->assertEquals("Domain exception", $exception->getCodeException()->getMessage()); + } + } }