Skip to content

Commit 80728ca

Browse files
authored
Add fail callback support in DoubleCheckedLocking::then() method (#76)
1 parent af4f378 commit 80728ca

File tree

6 files changed

+85
-42
lines changed

6 files changed

+85
-42
lines changed

README.md

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,9 @@ $newBalance = $mutex->check(static function () use ($bankAccount, $amount): bool
115115

116116
return $balance;
117117
});
118-
119-
if (!$newBalance) {
120-
if ($balance < 0) {
121-
throw new \DomainException('You have no credit');
122-
}
123-
}
124118
```
125119

126-
### Extracting code result after lock release exception
120+
### LockReleaseException::getCode{Exception, Result}()
127121

128122
Mutex implementations based on [`Malkush\Lock\Mutex\AbstractLockMutex`][10] will throw
129123
[`Malkusch\Lock\Exception\LockReleaseException`][11] in case of lock release
@@ -134,25 +128,21 @@ In order to read the code result (or an exception thrown there),
134128
Example:
135129
```php
136130
try {
137-
// OR $mutex->check(...)
138131
$result = $mutex->synchronized(static function () {
139132
if (someCondition()) {
140133
throw new \DomainException();
141134
}
142135

143136
return 'result';
144137
});
145-
} catch (LockReleaseException $unlockException) {
146-
if ($unlockException->getCodeException() !== null) {
147-
$codeException = $unlockException->getCodeException();
148-
// do something with the code exception
138+
} catch (LockReleaseException $e) {
139+
if ($e->getCodeException() !== null) {
140+
// do something with the $e->getCodeException() exception
149141
} else {
150-
$codeResult = $unlockException->getCodeResult();
151-
// do something with the code result
142+
// do something with the $e->getCodeResult() result
152143
}
153144

154-
// deal with LockReleaseException or propagate it
155-
throw $unlockException;
145+
throw $e;
156146
}
157147
```
158148

src/Exception/LockReleaseException.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
*
1010
* Take this exception very serious.
1111
*
12-
* This exception implies that the critical code was executed, i.e. side effects may have happened.
12+
* This exception implies that the synchronized code was executed, i.e. side effects may have happened.
1313
*
14-
* Failing to release a lock might have the potential to introduce deadlocks.
14+
* Failing to release a lock might also introduce deadlock.
1515
*/
1616
class LockReleaseException extends MutexException
1717
{
@@ -21,8 +21,6 @@ class LockReleaseException extends MutexException
2121
private ?\Throwable $codeException = null;
2222

2323
/**
24-
* The return value of the executed critical code.
25-
*
2624
* @return mixed
2725
*/
2826
public function getCodeResult()
@@ -42,9 +40,6 @@ public function setCodeResult($codeResult): self
4240
return $this;
4341
}
4442

45-
/**
46-
* The exception that has happened during the critical code execution.
47-
*/
4843
public function getCodeException(): ?\Throwable
4944
{
5045
return $this->codeException;

src/Mutex/DistributedMutex.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class DistributedMutex extends AbstractSpinlockWithTokenMutex implements LoggerA
3434
public function __construct(array $mutexes, float $acquireTimeout = 3, float $expireTimeout = \INF)
3535
{
3636
parent::__construct('', $acquireTimeout, $expireTimeout);
37+
\Closure::bind(function () {
38+
$this->key = 'distributed';
39+
}, $this, AbstractSpinlockMutex::class)();
3740

3841
$this->mutexes = $mutexes;
3942
$this->logger = new NullLogger();

src/Util/DoubleCheckedLocking.php

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,44 +21,53 @@ class DoubleCheckedLocking
2121
private Mutex $mutex;
2222

2323
/** @var callable(): bool */
24-
private $check;
24+
private $checkFx;
2525

2626
/**
27-
* @param callable(): bool $check Callback that decides if the lock should be acquired and is rechecked
28-
* after a lock has been acquired
27+
* @param callable(): bool $checkFx Decides if a lock should be acquired and is rechecked after the lock has been acquired
2928
*/
30-
public function __construct(Mutex $mutex, callable $check)
29+
public function __construct(Mutex $mutex, callable $checkFx)
3130
{
3231
$this->mutex = $mutex;
33-
$this->check = $check;
32+
$this->checkFx = $checkFx;
33+
}
34+
35+
private function invokeCheckFx(): bool
36+
{
37+
return ($this->checkFx)();
3438
}
3539

3640
/**
37-
* Executes a block of code only after the check callback passes
38-
* before and after acquiring a lock.
41+
* Execute a block of code only after the check callback passes before and after acquiring a lock.
3942
*
40-
* @template T
43+
* @template TSuccess
44+
* @template TFail = never
4145
*
42-
* @param callable(): T $code
46+
* @param callable(): TSuccess $successFx
47+
* @param callable(): TFail $failFx
4348
*
44-
* @return T|false False if check did not pass
49+
* @return TSuccess|($failFx is null ? false : TFail)
4550
*
4651
* @throws \Throwable
4752
* @throws LockAcquireException
4853
* @throws LockReleaseException
4954
*/
50-
public function then(callable $code)
55+
public function then(callable $successFx, ?callable $failFx = null)
5156
{
52-
if (!($this->check)()) {
53-
return false;
57+
if (!$this->invokeCheckFx()) {
58+
return $failFx !== null
59+
? $failFx()
60+
: false;
5461
}
5562

56-
return $this->mutex->synchronized(function () use ($code) {
57-
if (!($this->check)()) {
58-
return false;
63+
return $this->mutex->synchronized(function () use ($successFx, $failFx) {
64+
if (!$this->invokeCheckFx()) {
65+
return $failFx !== null
66+
? $failFx()
67+
: false;
5968
}
6069

61-
return $code();
70+
return $successFx();
6271
});
6372
}
6473
}

tests/Mutex/DistributedMutexTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ public function testAcquireMutexLogger(): void
331331

332332
$mutex->expects(self::exactly(3))
333333
->method('acquireMutex')
334-
->with(self::isInstanceOf(AbstractSpinlockMutex::class), 'php-malkusch-lock:', 1.0, \INF)
334+
->with(self::isInstanceOf(AbstractSpinlockMutex::class), 'distributed', 1.0, \INF)
335335
->willThrowException($this->createMock(/* PredisException::class */ LockAcquireException::class));
336336

337337
$logger->expects(self::exactly(3))
@@ -357,7 +357,7 @@ public function testReleaseMutexLogger(): void
357357

358358
$mutex->expects(self::exactly(3))
359359
->method('releaseMutex')
360-
->with(self::isInstanceOf(AbstractSpinlockMutex::class), 'php-malkusch-lock:', \INF)
360+
->with(self::isInstanceOf(AbstractSpinlockMutex::class), 'distributed', \INF)
361361
->willThrowException($this->createMock(/* PredisException::class */ LockReleaseException::class));
362362

363363
$logger->expects(self::exactly(3))

tests/Util/DoubleCheckedLockingTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,50 @@ public function testCodeExecuted(): void
129129
self::assertSame(1, $executedCount);
130130
self::assertSame('foo', $result);
131131
}
132+
133+
public function testFailCodeExecutedBeforeLock(): void
134+
{
135+
$this->mutex->expects(self::never())
136+
->method('synchronized');
137+
138+
$checkedLocking = new DoubleCheckedLocking($this->mutex, static function () {
139+
return false;
140+
});
141+
142+
$executedCount = 0;
143+
$result = $checkedLocking->then(static function () {
144+
self::fail();
145+
}, static function () use (&$executedCount) {
146+
++$executedCount;
147+
148+
return 'foo';
149+
});
150+
151+
self::assertSame(1, $executedCount);
152+
self::assertSame('foo', $result);
153+
}
154+
155+
public function testFailCodeExecutedAfterLock(): void
156+
{
157+
$this->mutex->expects(self::once())
158+
->method('synchronized')
159+
->willReturnCallback(static fn (\Closure $block) => $block());
160+
161+
$i = 0;
162+
$checkedLocking = new DoubleCheckedLocking($this->mutex, static function () use (&$i) {
163+
return $i++ === 0;
164+
});
165+
166+
$executedCount = 0;
167+
$result = $checkedLocking->then(static function () {
168+
self::fail();
169+
}, static function () use (&$executedCount) {
170+
++$executedCount;
171+
172+
return 'foo';
173+
});
174+
175+
self::assertSame(1, $executedCount);
176+
self::assertSame('foo', $result);
177+
}
132178
}

0 commit comments

Comments
 (0)