Skip to content

Commit

Permalink
refactor: refactored critical sections
Browse files Browse the repository at this point in the history
  • Loading branch information
petrknap committed Jun 29, 2024
1 parent 2263f59 commit 1a7969d
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 267 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Critical section based on `symfony/lock`

The [CriticalSection](./src/CriticalSection.php) is a simple object that handles the critical section overhead 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
Expand All @@ -14,7 +14,7 @@ $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).
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
Expand Down Expand Up @@ -45,15 +45,15 @@ var_dump($criticalOutput);

## Do you need to accept only locked resources?

Use [`LockedResource`](./src/LockedResource.php) if you need to be sure that you are not processing resource outside it's critical section.
Use [the `LockedResource`](./src/LockedResource.php) if you need to be sure that you are not processing resource outside it's critical section.

```php
namespace PetrKnap\CriticalSection;

use Symfony\Component\Lock\NoLock;

/** @param Locked<Some\Resource> $resource */
function f(Locked $resource) {
function f(LockedResource $resource) {
echo $resource->value;
}

Expand All @@ -64,7 +64,7 @@ CriticalSection::withLock($lock)(fn () => f($resource));

## Does your critical section work with database?

Use [`doctrine/dbal`](https://packagist.org/packages/doctrine/dbal) and its `transactional` method.
Use [the `doctrine/dbal`](https://packagist.org/packages/doctrine/dbal) and its `transactional` method.

```php
/** @var PetrKnap\CriticalSection\CriticalSectionInterface $criticalSection */
Expand Down
54 changes: 38 additions & 16 deletions src/CriticalSection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,52 @@

namespace PetrKnap\CriticalSection;

use Symfony\Component\Lock\LockInterface;
use Throwable;

/** @template T */
final class CriticalSection
abstract class CriticalSection
{
/** @return NonCriticalSection<T> */
public static function create(): NonCriticalSection
{
return new NonCriticalSection();
}
use CriticalSectionStaticFactory;

/** @return WrappingCriticalSection<T> */
public static function withLock(LockInterface $lock, bool $isBlocking = true): WrappingCriticalSection
{
return self::create()->withLock($lock, $isBlocking);
protected function __construct(
protected readonly bool $isBlocking,
) {
}

/**
* @param array<LockInterface> $locks
* @phpstan-ignore-next-line Template type T ... is not referenced in a parameter.
*
* @template T of mixed
*
* @return WrappingCriticalSection<T>
* @param (callable(mixed ...$args): T)|callable $criticalSection
* @param mixed ...$args will be forwarded to {@link $criticalSection}
*
* @return T|null returned by {@link $criticalSection} or null when it is occupied (non-blocking mode only)
*
* @throws Exception\CouldNotEnterCriticalSection
* @throws Exception\CouldNotLeaveCriticalSection
* @throws Throwable from {@link $criticalSection}
*/
public static function withLocks(array $locks, bool $isBlocking = true): WrappingCriticalSection
public function __invoke(callable $criticalSection, mixed ...$args): mixed
{
return self::create()->withLocks($locks, $isBlocking);
if ($this->enter() === false) {
return null;
}
try {
return $criticalSection(...$args);
} finally {
$this->leave();
}
}

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

/**
* @throws Exception\CouldNotLeaveCriticalSection
*/
abstract protected function leave(): void;
}
27 changes: 0 additions & 27 deletions src/CriticalSectionInterface.php

This file was deleted.

56 changes: 56 additions & 0 deletions src/CriticalSectionStaticFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace PetrKnap\CriticalSection;

use PetrKnap\Shorts\Exception\NotImplemented;
use Symfony\Component\Lock\LockInterface;

/**
* @internal please use {@see CriticalSection}
*
* @method static WrappingCriticalSection withLock(LockInterface $lock, bool $isBlocking = true)
* @method static WrappingCriticalSection withLocks(array $locks, bool $isBlocking = true)
*/
trait CriticalSectionStaticFactory
{
/**
* @todo refactor it when {@see https://bugs.php.net/bug.php?id=40837} will be fixed
*
* @param array<mixed> $arguments
*/
public static function __callStatic(string $name, array $arguments): mixed
{
return match ($name) {
'withLock', 'withLocks' => self::nonCritical(canEnter: true)->$name(...$arguments),
default => NotImplemented::throw("Static method `{$name}`"),
};
}

private static function nonCritical(CriticalSection $wrappedCriticalSection = null, bool $canEnter = null): WrappingCriticalSection
{
return new class ($wrappedCriticalSection, (bool) $canEnter) extends WrappingCriticalSection {
public function __construct(
private readonly CriticalSection|null $wrappedCriticalSection,
private readonly bool $canEnter,
) {
parent::__construct($wrappedCriticalSection, false);
}

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

public function leave(): void
{
}

protected function getWrappingReferenceOrNull(): CriticalSection|null
{
return $this->wrappedCriticalSection;
}
};
}
}
2 changes: 2 additions & 0 deletions src/Exception/CouldNotEnterCriticalSection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace PetrKnap\CriticalSection\Exception;

use PetrKnap\Shorts\ExceptionWrapper;
use RuntimeException;

final class CouldNotEnterCriticalSection extends RuntimeException implements CriticalSectionException
{
use ExceptionWrapper;
}
2 changes: 2 additions & 0 deletions src/Exception/CouldNotLeaveCriticalSection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

namespace PetrKnap\CriticalSection\Exception;

use PetrKnap\Shorts\ExceptionWrapper;
use RuntimeException;

final class CouldNotLeaveCriticalSection extends RuntimeException implements CriticalSectionException
{
use ExceptionWrapper;
}
4 changes: 3 additions & 1 deletion src/Exception/CriticalSectionException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace PetrKnap\CriticalSection\Exception;

interface CriticalSectionException
use Throwable;

interface CriticalSectionException extends Throwable
{
}
40 changes: 0 additions & 40 deletions src/NonCriticalSection.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,32 @@

declare(strict_types=1);

namespace PetrKnap\CriticalSection;
namespace PetrKnap\CriticalSection\Symfony\Lock;

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

/**
* @template T
*
* @extends WrappingCriticalSection<T>
*/
final class SymfonyLockCriticalSection extends WrappingCriticalSection
final class CriticalSection extends WrappingCriticalSection
{
/** @param CriticalSectionInterface<T>|null $wrappedCriticalSection */
protected function __construct(
?CriticalSectionInterface $wrappedCriticalSection,
private LockInterface $lock,
private bool $isBlocking,
private readonly LockInterface $lock,
Base|null $wrappedCriticalSection,
bool $isBlocking,
) {
parent::__construct($wrappedCriticalSection);
parent::__construct($wrappedCriticalSection, $isBlocking);
}

protected function enter(): bool
{
try {
return $this->lock->acquire(blocking: $this->isBlocking);
} catch (LockConflictedException | LockAcquiringException $reason) {
throw new CouldNotEnterCriticalSection(previous: $reason);
throw new Exception\CouldNotEnterCriticalSection($reason);
}
}

Expand All @@ -41,7 +36,7 @@ protected function leave(): void
try {
$this->lock->release();
} catch (LockReleasingException $reason) {
throw new CouldNotLeaveCriticalSection(previous: $reason);
throw new Exception\CouldNotLeaveCriticalSection($reason);
}
}
}
Loading

0 comments on commit 1a7969d

Please sign in to comment.