Skip to content

Commit

Permalink
refactor: refactored CriticalSection
Browse files Browse the repository at this point in the history
feat: added support for nonblocking mode
  • Loading branch information
petrknap committed Nov 11, 2023
1 parent 1b336dd commit 44c8ce7
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 31 deletions.
21 changes: 13 additions & 8 deletions src/CriticalSection.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,42 @@

namespace PetrKnap\CriticalSection;

use PetrKnap\CriticalSection\Exception\CouldNotEnterCriticalSection;
use PetrKnap\CriticalSection\Exception\CouldNotLeaveCriticalSection;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockReleasingException;
use Symfony\Component\Lock\LockInterface;

final class CriticalSection
final class CriticalSection implements CriticalSectionInterface
{
protected function __construct(
private LockInterface $lock,
private bool $isLocking,
) {
}

public static function withLock(LockInterface $lock): self
public static function withLock(LockInterface $lock, bool $isBlocking = true): CriticalSectionInterface
{
return new self($lock);
return new self($lock, $isBlocking);
}

public function __invoke(callable $callable, mixed ...$args): mixed
public function __invoke(callable $criticalSection)
{
try {
$this->lock->acquire(true);
if($this->lock->acquire(blocking: $this->isLocking) === false) {
return null;
}
} catch (LockConflictedException | LockAcquiringException $acquiringException) {
throw new CriticalSectionCouldNotAcquireLock(previous: $acquiringException);
throw new CouldNotEnterCriticalSection(previous: $acquiringException);
}
try {
return $callable(...$args);
return $criticalSection();
} finally {
try {
$this->lock->release();
} catch (LockReleasingException $releasingException) {
throw new CriticalSectionCouldNotReleaseLock(previous: $releasingException);
throw new CouldNotLeaveCriticalSection(previous: $releasingException);
}
}
}
Expand Down
7 changes: 0 additions & 7 deletions src/CriticalSectionCouldNotAcquireLock.php

This file was deleted.

7 changes: 0 additions & 7 deletions src/CriticalSectionCouldNotReleaseLock.php

This file was deleted.

22 changes: 22 additions & 0 deletions src/CriticalSectionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);

namespace PetrKnap\CriticalSection;

use PetrKnap\CriticalSection\Exception\CouldNotEnterCriticalSection;
use PetrKnap\CriticalSection\Exception\CouldNotLeaveCriticalSection;
use Throwable;

/**
* @template T
*/
interface CriticalSectionInterface
{
/**
* @param callable(): T $criticalSection
* @return ?T from the critical section
* @throws CouldNotEnterCriticalSection
* @throws CouldNotLeaveCriticalSection
* @throws Throwable from the critical section
*/
public function __invoke(callable $criticalSection);
}
9 changes: 9 additions & 0 deletions src/Exception/CouldNotEnterCriticalSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

namespace PetrKnap\CriticalSection\Exception;

use RuntimeException;

final class CouldNotEnterCriticalSection extends RuntimeException implements CriticalSectionException
{
}
9 changes: 9 additions & 0 deletions src/Exception/CouldNotLeaveCriticalSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

namespace PetrKnap\CriticalSection\Exception;

use RuntimeException;

final class CouldNotLeaveCriticalSection extends RuntimeException implements CriticalSectionException
{
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types=1);

namespace PetrKnap\CriticalSection;
namespace PetrKnap\CriticalSection\Exception;

interface CriticalSectionException
{
Expand Down
36 changes: 28 additions & 8 deletions tests/CriticalSectionTest.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
<?php declare(strict_types=1);
<?php /** @noinspection PhpUnhandledExceptionInspection */

declare(strict_types=1);

namespace PetrKnap\CriticalSection;

use Exception;
use PetrKnap\CriticalSection\Exception\CouldNotEnterCriticalSection;
use PetrKnap\CriticalSection\Exception\CouldNotLeaveCriticalSection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockReleasingException;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\NoLock;

class CriticalSectionTest extends TestCase
{
public function testAcquiresLockBeforeCallableIsExecuted(): void
public function testAcquiresLockBeforeCriticalSectionIsExecuted(): void
{
$shared = new \stdClass();
$lock = self::createMock(LockInterface::class);
Expand All @@ -27,21 +32,22 @@ public function testAcquiresLockBeforeCallableIsExecuted(): void
});
}

public function testReturnsValueReturnedByExecutedCallable(): void
public function testReturnsValueReturnedByExecutedCriticalSection(): void
{
$lock = self::createMock(LockInterface::class);
$lock = new NoLock();
$expected = new \stdClass();

self::assertSame($expected, CriticalSection::withLock($lock)(function () use ($expected) {
return $expected;
}));
}

public function testReleasesLockAfterCallableWasExecuted(): void
public function testReleasesLockAfterCriticalSectionWasExecuted(): void
{
$shared = new \stdClass();
$shared->isLocked = true;
$lock = self::createMock(LockInterface::class);
$lock->method('acquire')->willReturn(true);
$lock->expects(self::once())
->method('release')
->willReturnCallback(function () use ($shared) {
Expand All @@ -55,11 +61,12 @@ public function testReleasesLockAfterCallableWasExecuted(): void
self::assertFalse($shared->isLocked);
}

public function testReleasesLockAndThrowsWhenCallableThrows(): void
public function testReleasesLockAndThrowsWhenCriticalSectionThrows(): void
{
$shared = new \stdClass();
$shared->isLocked = true;
$lock = self::createMock(LockInterface::class);
$lock->method('acquire')->willReturn(true);
$lock->expects(self::once())
->method('release')
->willReturnCallback(function () use ($shared) {
Expand All @@ -79,6 +86,18 @@ public function testReleasesLockAndThrowsWhenCallableThrows(): void
}
}

public function testSkipsCriticalSectionWhenItIsOccupiedInNonBlockingMode(): void
{
$lock = self::createMock(LockInterface::class);
$lock->expects(self::once())
->method('acquire')
->willReturn(false);

self::assertNull(CriticalSection::withLock($lock)(function (): bool {
return true;
}));
}

/** @dataProvider dataThrowsWhenLockThrowsOnAcquire */
public function testThrowsWhenLockThrowsOnAcquire(Exception $lockException): void
{
Expand All @@ -87,7 +106,7 @@ public function testThrowsWhenLockThrowsOnAcquire(Exception $lockException): voi
->method('acquire')
->willThrowException($lockException);

self::expectException(CriticalSectionCouldNotAcquireLock::class);
self::expectException(CouldNotEnterCriticalSection::class);
CriticalSection::withLock($lock)(function () {
});
}
Expand All @@ -106,11 +125,12 @@ public static function dataThrowsWhenLockThrowsOnAcquire(): iterable
public function testThrowsWhenLockThrowsOnRelease(): void
{
$lock = self::createMock(LockInterface::class);
$lock->method('acquire')->willReturn(true);
$lock->expects(self::once())
->method('release')
->willThrowException(new LockReleasingException());

self::expectException(CriticalSectionCouldNotReleaseLock::class);
self::expectException(CouldNotLeaveCriticalSection::class);
CriticalSection::withLock($lock)(function () {
});
}
Expand Down

0 comments on commit 44c8ce7

Please sign in to comment.