diff --git a/README.md b/README.md index 6f73b48..d8f0d08 100644 --- a/README.md +++ b/README.md @@ -115,15 +115,9 @@ $newBalance = $mutex->check(static function () use ($bankAccount, $amount): bool return $balance; }); - -if (!$newBalance) { - if ($balance < 0) { - throw new \DomainException('You have no credit'); - } -} ``` -### Extracting code result after lock release exception +### LockReleaseException::getCode{Exception, Result}() Mutex implementations based on [`Malkush\Lock\Mutex\AbstractLockMutex`][10] will throw [`Malkusch\Lock\Exception\LockReleaseException`][11] in case of lock release @@ -134,7 +128,6 @@ In order to read the code result (or an exception thrown there), Example: ```php try { - // OR $mutex->check(...) $result = $mutex->synchronized(static function () { if (someCondition()) { throw new \DomainException(); @@ -142,17 +135,14 @@ try { return 'result'; }); -} catch (LockReleaseException $unlockException) { - if ($unlockException->getCodeException() !== null) { - $codeException = $unlockException->getCodeException(); - // do something with the code exception +} catch (LockReleaseException $e) { + if ($e->getCodeException() !== null) { + // do something with the $e->getCodeException() exception } else { - $codeResult = $unlockException->getCodeResult(); - // do something with the code result + // do something with the $e->getCodeResult() result } - // deal with LockReleaseException or propagate it - throw $unlockException; + throw $e; } ``` diff --git a/src/Exception/LockReleaseException.php b/src/Exception/LockReleaseException.php index e9d1a27..008e9b0 100644 --- a/src/Exception/LockReleaseException.php +++ b/src/Exception/LockReleaseException.php @@ -9,9 +9,9 @@ * * Take this exception very serious. * - * This exception implies that the critical code was executed, i.e. side effects may have happened. + * This exception implies that the synchronized code was executed, i.e. side effects may have happened. * - * Failing to release a lock might have the potential to introduce deadlocks. + * Failing to release a lock might also introduce deadlock. */ class LockReleaseException extends MutexException { @@ -21,8 +21,6 @@ class LockReleaseException extends MutexException private ?\Throwable $codeException = null; /** - * The return value of the executed critical code. - * * @return mixed */ public function getCodeResult() @@ -42,9 +40,6 @@ public function setCodeResult($codeResult): self return $this; } - /** - * The exception that has happened during the critical code execution. - */ public function getCodeException(): ?\Throwable { return $this->codeException; diff --git a/src/Mutex/DistributedMutex.php b/src/Mutex/DistributedMutex.php index 4da4b08..991e81f 100644 --- a/src/Mutex/DistributedMutex.php +++ b/src/Mutex/DistributedMutex.php @@ -34,6 +34,9 @@ class DistributedMutex extends AbstractSpinlockWithTokenMutex implements LoggerA public function __construct(array $mutexes, float $acquireTimeout = 3, float $expireTimeout = \INF) { parent::__construct('', $acquireTimeout, $expireTimeout); + \Closure::bind(function () { + $this->key = 'distributed'; + }, $this, AbstractSpinlockMutex::class)(); $this->mutexes = $mutexes; $this->logger = new NullLogger(); diff --git a/src/Util/DoubleCheckedLocking.php b/src/Util/DoubleCheckedLocking.php index d45f179..9f50729 100644 --- a/src/Util/DoubleCheckedLocking.php +++ b/src/Util/DoubleCheckedLocking.php @@ -21,44 +21,53 @@ class DoubleCheckedLocking private Mutex $mutex; /** @var callable(): bool */ - private $check; + private $checkFx; /** - * @param callable(): bool $check Callback that decides if the lock should be acquired and is rechecked - * after a lock has been acquired + * @param callable(): bool $checkFx Decides if a lock should be acquired and is rechecked after the lock has been acquired */ - public function __construct(Mutex $mutex, callable $check) + public function __construct(Mutex $mutex, callable $checkFx) { $this->mutex = $mutex; - $this->check = $check; + $this->checkFx = $checkFx; + } + + private function invokeCheckFx(): bool + { + return ($this->checkFx)(); } /** - * Executes a block of code only after the check callback passes - * before and after acquiring a lock. + * Execute a block of code only after the check callback passes before and after acquiring a lock. * - * @template T + * @template TSuccess + * @template TFail = never * - * @param callable(): T $code + * @param callable(): TSuccess $successFx + * @param callable(): TFail $failFx * - * @return T|false False if check did not pass + * @return TSuccess|($failFx is null ? false : TFail) * * @throws \Throwable * @throws LockAcquireException * @throws LockReleaseException */ - public function then(callable $code) + public function then(callable $successFx, ?callable $failFx = null) { - if (!($this->check)()) { - return false; + if (!$this->invokeCheckFx()) { + return $failFx !== null + ? $failFx() + : false; } - return $this->mutex->synchronized(function () use ($code) { - if (!($this->check)()) { - return false; + return $this->mutex->synchronized(function () use ($successFx, $failFx) { + if (!$this->invokeCheckFx()) { + return $failFx !== null + ? $failFx() + : false; } - return $code(); + return $successFx(); }); } } diff --git a/tests/Mutex/DistributedMutexTest.php b/tests/Mutex/DistributedMutexTest.php index 97f83a3..be96d90 100644 --- a/tests/Mutex/DistributedMutexTest.php +++ b/tests/Mutex/DistributedMutexTest.php @@ -331,7 +331,7 @@ public function testAcquireMutexLogger(): void $mutex->expects(self::exactly(3)) ->method('acquireMutex') - ->with(self::isInstanceOf(AbstractSpinlockMutex::class), 'php-malkusch-lock:', 1.0, \INF) + ->with(self::isInstanceOf(AbstractSpinlockMutex::class), 'distributed', 1.0, \INF) ->willThrowException($this->createMock(/* PredisException::class */ LockAcquireException::class)); $logger->expects(self::exactly(3)) @@ -357,7 +357,7 @@ public function testReleaseMutexLogger(): void $mutex->expects(self::exactly(3)) ->method('releaseMutex') - ->with(self::isInstanceOf(AbstractSpinlockMutex::class), 'php-malkusch-lock:', \INF) + ->with(self::isInstanceOf(AbstractSpinlockMutex::class), 'distributed', \INF) ->willThrowException($this->createMock(/* PredisException::class */ LockReleaseException::class)); $logger->expects(self::exactly(3)) diff --git a/tests/Util/DoubleCheckedLockingTest.php b/tests/Util/DoubleCheckedLockingTest.php index 491e9ff..0fa22cd 100644 --- a/tests/Util/DoubleCheckedLockingTest.php +++ b/tests/Util/DoubleCheckedLockingTest.php @@ -129,4 +129,50 @@ public function testCodeExecuted(): void self::assertSame(1, $executedCount); self::assertSame('foo', $result); } + + public function testFailCodeExecutedBeforeLock(): void + { + $this->mutex->expects(self::never()) + ->method('synchronized'); + + $checkedLocking = new DoubleCheckedLocking($this->mutex, static function () { + return false; + }); + + $executedCount = 0; + $result = $checkedLocking->then(static function () { + self::fail(); + }, static function () use (&$executedCount) { + ++$executedCount; + + return 'foo'; + }); + + self::assertSame(1, $executedCount); + self::assertSame('foo', $result); + } + + public function testFailCodeExecutedAfterLock(): void + { + $this->mutex->expects(self::once()) + ->method('synchronized') + ->willReturnCallback(static fn (\Closure $block) => $block()); + + $i = 0; + $checkedLocking = new DoubleCheckedLocking($this->mutex, static function () use (&$i) { + return $i++ === 0; + }); + + $executedCount = 0; + $result = $checkedLocking->then(static function () { + self::fail(); + }, static function () use (&$executedCount) { + ++$executedCount; + + return 'foo'; + }); + + self::assertSame(1, $executedCount); + self::assertSame('foo', $result); + } }