Skip to content

Commit

Permalink
Merge pull request #2 from petrknap/multilock-support
Browse files Browse the repository at this point in the history
Implemented wrapping of critical sections
  • Loading branch information
petrknap authored Nov 12, 2023
2 parents 0d064d5 + 16f60e3 commit b8bf7d3
Show file tree
Hide file tree
Showing 20 changed files with 292 additions and 56 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.bat text eol=crlf
2 changes: 1 addition & 1 deletion .molireali
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
authors
composer PetrKnap\\CriticalSection
dockerfile php 8.0-cli
docker-scripts petrknap/php-criticalsection
docker-scripts petrknap/php-critical-section
donation
git-crlf "*.bat"
github-workflow docker "composer ci-script"
Expand Down
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
# Critical section based on `symfony/lock`

The [CriticalSection](./src/CriticalSection.php) is a simple object that handles the lock manipulation for you
The [CriticalSection](./src/CriticalSection.php) is a simple object that handles the critical section overhead for you
and lets you focus on the actual code.

```php
use PetrKnap\CriticalSection\CriticalSection;
use Symfony\Component\Lock\NoLock;

$lock = new NoLock();
var_dump(
CriticalSection::withLock($lock)(function() {
return 'This was critical!';
})
);

$criticalOutput = CriticalSection::withLock($lock)(fn () => 'This was critical!');

var_dump($criticalOutput);
```

You can wrap critical sections one inside the other thanks to the [WrappingCriticalSection](./src/WrappingCriticalSection.php).
This makes it easy to combine multiple locks, for example.

```php
use PetrKnap\CriticalSection\CriticalSection;
use Symfony\Component\Lock\NoLock;

$lockA = new NoLock();
$lockB = new NoLock();

$criticalOutput = CriticalSection::withLock($lockA)->withLock($lockB)(fn () => 'This was even more critical!');

var_dump($criticalOutput);
```

---
Expand Down
4 changes: 4 additions & 0 deletions bin/build.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
DIR="$(realpath "${BASH_SOURCE%/*}")"

docker build "${DIR}/.." -t petrknap/php-critical-section:latest
1 change: 1 addition & 0 deletions bin/build.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker build %~dp0/.. -t petrknap/php-critical-section:latest
7 changes: 7 additions & 0 deletions bin/run.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
DIR="$(realpath "${BASH_SOURCE%/*}")"

docker run --rm -ti \
-v "${DIR}/..:/app" \
petrknap/php-critical-section:latest \
$@
4 changes: 4 additions & 0 deletions bin/run.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
docker run --rm -ti ^
-v %~dp0/..:/app ^
petrknap/php-critical-section:latest ^
%*
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"test": "phpunit --colors=always --testdox tests",
"validate": [
"phpcs --colors --standard=PSR12 --exclude=PSR12.Files.OpenTag,PSR12.Files.FileHeader,Generic.Files.LineLength src tests",
"phpstan analyse --level 5 src",
"phpstan analyse --level max src",
"phpstan analyse --level 5 tests",
"phpinsights analyse src"
],
Expand Down
16 changes: 13 additions & 3 deletions src/CriticalSection.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection;

use Symfony\Component\Lock\LockInterface;

/** @template T */
final class CriticalSection
{
public static function withLock(LockInterface $lock, bool $isBlocking = true): CriticalSectionInterface
/** @return NonCriticalSection<T> */
public static function create(): NonCriticalSection
{
return new NonCriticalSection();
}

/** @return SymfonyLockCriticalSection<T> */
public static function withLock(LockInterface $lock, bool $isBlocking = true): SymfonyLockCriticalSection
{
return new SymfonyLockCriticalSection($lock, $isBlocking);
return self::create()->withLock($lock, $isBlocking);
}
}
6 changes: 4 additions & 2 deletions src/CriticalSectionInterface.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection;

Expand All @@ -14,7 +16,7 @@ interface CriticalSectionInterface
/**
* @param callable(): T $criticalSection
*
* @return ?T returned by {@link $criticalSection} or null when it is occupied in non-blocking mode
* @return ?T returned by {@link $criticalSection} or null when it is occupied (non-blocking mode only)
*
* @throws CouldNotEnterCriticalSection
* @throws CouldNotLeaveCriticalSection
Expand Down
4 changes: 3 additions & 1 deletion src/Exception/CouldNotEnterCriticalSection.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection\Exception;

Expand Down
4 changes: 3 additions & 1 deletion src/Exception/CouldNotLeaveCriticalSection.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection\Exception;

Expand Down
4 changes: 3 additions & 1 deletion src/Exception/CriticalSectionException.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection\Exception;

Expand Down
40 changes: 40 additions & 0 deletions src/NonCriticalSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection;

/**
* @template T
*
* @extends WrappingCriticalSection<T>
*/
final class NonCriticalSection extends WrappingCriticalSection
{
/**
* @internal Use {@link CriticalSection::create()}
*
* @param CriticalSectionInterface<T>|null $wrappedCriticalSection
*/
public function __construct(
private ?CriticalSectionInterface $wrappedCriticalSection = null,
private bool $canEnter = true,
) {
parent::__construct($wrappedCriticalSection);
}

protected function enter(): bool
{
return $this->canEnter;
}

protected function leave(): void
{
}

/** @return CriticalSectionInterface<T>|null */
protected function getWrappingReferenceOrNull(): ?CriticalSectionInterface
{
return $this->wrappedCriticalSection;
}
}
34 changes: 18 additions & 16 deletions src/SymfonyLockCriticalSection.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php declare(strict_types=1);
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection;

Expand All @@ -12,34 +14,34 @@
/**
* @template T
*
* @implements CriticalSectionInterface<T>
* @extends WrappingCriticalSection<T>
*/
final class SymfonyLockCriticalSection implements CriticalSectionInterface
final class SymfonyLockCriticalSection extends WrappingCriticalSection
{
public function __construct(
/** @param CriticalSectionInterface<T>|null $wrappedCriticalSection */
protected function __construct(
?CriticalSectionInterface $wrappedCriticalSection,
private LockInterface $lock,
private bool $isBlocking,
) {
parent::__construct($wrappedCriticalSection);
}

/** @inheritDoc */
public function __invoke(callable $criticalSection)
protected function enter(): bool
{
try {
if ($this->lock->acquire(blocking: $this->isBlocking) === false) {
return null;
}
return $this->lock->acquire(blocking: $this->isBlocking);
} catch (LockConflictedException | LockAcquiringException $reason) {
throw new CouldNotEnterCriticalSection(previous: $reason);
}
}

protected function leave(): void
{
try {
return $criticalSection();
} finally {
try {
$this->lock->release();
} catch (LockReleasingException $reason) {
throw new CouldNotLeaveCriticalSection(previous: $reason);
}
$this->lock->release();
} catch (LockReleasingException $reason) {
throw new CouldNotLeaveCriticalSection(previous: $reason);
}
}
}
61 changes: 61 additions & 0 deletions src/WrappingCriticalSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection;

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

/**
* @template T
*
* @implements CriticalSectionInterface<T>
*/
abstract class WrappingCriticalSection implements CriticalSectionInterface
{
/** @param CriticalSectionInterface<T>|null $wrappedCriticalSection */
protected function __construct(
private ?CriticalSectionInterface $wrappedCriticalSection,
) {
}

/** @inheritDoc */
public function __invoke(callable $criticalSection)
{
if ($this->enter() === false) {
return null;
}
try {
if ($this->wrappedCriticalSection) {
return ($this->wrappedCriticalSection)(static fn () => $criticalSection());
}
return $criticalSection();
} finally {
$this->leave();
}
}

/** @return SymfonyLockCriticalSection<T> */
public function withLock(LockInterface $lock, bool $isBlocking = true): SymfonyLockCriticalSection
{
return new SymfonyLockCriticalSection($this->getWrappingReferenceOrNull(), $lock, $isBlocking);
}

/**
* @return bool false if it is occupied (non-blocking mode only)
*
* @throws CouldNotEnterCriticalSection
*/
abstract protected function enter(): bool;

/** @throws CouldNotLeaveCriticalSection */
abstract protected function leave(): void;

/** @return CriticalSectionInterface<T>|null */
protected function getWrappingReferenceOrNull(): ?CriticalSectionInterface
{
return $this;
}
}
38 changes: 38 additions & 0 deletions tests/NonCriticalSectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php /** @noinspection PhpUnhandledExceptionInspection */

declare(strict_types=1);

namespace PetrKnap\CriticalSection;

use PHPUnit\Framework\TestCase;

class NonCriticalSectionTest extends TestCase
{
/** @dataProvider dataEntersCriticalSectionWhenCanEnter */
public function testEntersCriticalSectionWhenCanEnter(bool $canEnter): void
{
$section = new NonCriticalSection(canEnter: $canEnter);

if (
$section(function () use ($canEnter) {
self::assertTrue($canEnter);
return true;
}) === null
) {
self::assertFalse($canEnter);
}
}

public static function dataEntersCriticalSectionWhenCanEnter(): array
{
return [
'yes' => [true],
'no' => [false],
];
}

public function testDoesNotUseSelfAsWrappingReference(): void
{
self::markTestIncomplete();
}
}
3 changes: 2 additions & 1 deletion tests/ReadmeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public static function getPathToMarkdownFile(): string
public static function getExpectedOutputsOfPhpExamples(): iterable
{
return [
'string(18) "This was critical!"' . PHP_EOL,
'single-lock' => 'string(18) "This was critical!"' . PHP_EOL,
'double-lock' => 'string(28) "This was even more critical!"' . PHP_EOL,
];
}
}
Loading

0 comments on commit b8bf7d3

Please sign in to comment.