Skip to content

Commit

Permalink
Merge pull request #29 from sdoatput/return-value-on-unlock-error
Browse files Browse the repository at this point in the history
Add ability to retrieve executed code result or exception on unlock error
  • Loading branch information
Willem Stuursma-Ruwen authored Dec 12, 2018
2 parents 6925de8 + c526260 commit 093f389
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 8 deletions.
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
41 changes: 41 additions & 0 deletions classes/exception/LockReleaseException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
26 changes: 22 additions & 4 deletions classes/mutex/LockMutex.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
}
42 changes: 39 additions & 3 deletions tests/mutex/LockMutexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,54 @@ 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());

$this->mutex->synchronized(function () {
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());
}
}
}

0 comments on commit 093f389

Please sign in to comment.