Skip to content

Commit

Permalink
Add LockMiddleware (#2)
Browse files Browse the repository at this point in the history
* Add LockMiddleware

* Fix phpstan and code coverage

* Fix php-cs

* Fix rector and php-cs-fixer

* Remove readonly

* Fix PHP 8.0 compatibility
  • Loading branch information
alexander-schranz authored Aug 11, 2022
1 parent 73da6f6 commit b4450d1
Show file tree
Hide file tree
Showing 17 changed files with 320 additions and 64 deletions.
4 changes: 4 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,9 @@
'multiline_whitespace_before_semicolons' => true,
'single_line_throw' => false,
'visibility_required' => ['elements' => ['property', 'method', 'const']],
'phpdoc_to_comment' => [
'ignored_tags' => ['todo', 'var', 'see'],
],
'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']],
])
->setFinder($finder);
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ This way we make sure that the real exception is thrown out by this message
bus, and a controller can catch or convert it to a specific http status code.
This middleware is always activated in the sulu message bus.

### LockMiddleware

The `LockMiddleware` will allow to lock specific resources by a given key. This is commonly
used to prevent concurrent access to the same resource and avoid race conditions.
The locking can be activated and controlled via the `LockStamp` which supports the same parameters
as the Symfony `LockFactory` to create the Lock.

```php
use Sulu\Messenger\Infrastructure\Symfony\Messenger\LockMiddleware\LockStamp;

$this->handle(new Envelope(new YourMessage(), [new LockStamp('lock-key')]));

# set ttl and autorelease
$this->handle(new Envelope(new YourMessage(), [new LockStamp('lock-key', 300.0, true)]));

# multiple locks possible all locks need to be acquired before processing the message
$this->handle(new Envelope(new YourMessage(), [new LockStamp('lock-key-1'), new LockStamp('lock-key-2')]));
```

### DoctrineFlushMiddleware

The `DoctrineFlushMiddleware` is a Middleware which let us flush the Doctrine
Expand Down
23 changes: 16 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"symfony/doctrine-bridge": "^5.4 || ^6.0",
"symfony/framework-bundle": "^5.4 || ^6.0",
"symfony/http-kernel": "^5.4 || ^6.0",
"symfony/lock": "^5.4 || ^6.0",
"symfony/messenger": "^5.4 || ^6.0"
},
"require-dev": {
Expand All @@ -22,23 +23,24 @@
"jangregor/phpstan-prophecy": "^1.0",
"nunomaduro/collision": "^5.0 || ^6.0",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-doctrine": "^1.2",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-symfony": "^1.1",
"phpstan/phpstan-webmozart-assert": "^1.0",
"phpunit/phpunit": "^9.5",
"qossmic/deptrac-shim": "^0.19.3",
"rector/rector": "^0.12.16",
"schranz/test-generator": "^0.3",
"qossmic/deptrac-shim": "^0.23.0",
"rector/rector": "^0.13.10",
"schranz/test-generator": "^0.4",
"symfony/browser-kit": "^5.4 || ^6.0",
"symfony/css-selector": "^5.4 || ^6.0",
"symfony/debug-bundle": "^5.4 || ^6.0",
"symfony/dotenv": "^5.4 || ^6.0",
"symfony/error-handler": "^5.4 || ^6.0",
"symfony/phpunit-bridge": "^5.4 || ^6.0",
"thecodingmachine/phpstan-strict-rules": "^1.0",
"symfony/dotenv": "^5.4 || ^6.0",
"symfony/yaml": "^5.4 || ^6.0"
"symfony/yaml": "^5.4 || ^6.0",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"autoload": {
"psr-4": {
Expand Down Expand Up @@ -85,6 +87,10 @@
"rector": [
"@php vendor/bin/rector process"
],
"fix": [
"@rector",
"@php-cs-fix"
],
"php-cs": "@php vendor/bin/php-cs-fixer fix --verbose --diff --dry-run",
"php-cs-fix": "@php vendor/bin/php-cs-fixer fix",
"lint-composer": "@composer validate --no-check-publish --strict",
Expand All @@ -100,6 +106,9 @@
]
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"phpstan/extension-installer": true
}
}
}
5 changes: 5 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Sulu\Messenger\Infrastructure\Symfony\Messenger\FlushMiddleware\DoctrineFlushMiddleware;
use Sulu\Messenger\Infrastructure\Symfony\Messenger\LockMiddleware\LockMiddleware;
use Sulu\Messenger\Infrastructure\Symfony\Messenger\UnpackExceptionMiddleware\UnpackExceptionMiddleware;

return function (ContainerConfigurator $configurator) {
Expand All @@ -16,4 +17,8 @@

$services->set('sulu_messenger.unpack_exception_middleware')
->class(UnpackExceptionMiddleware::class);

$services->set('sulu_messenger.lock_middleware')
->class(LockMiddleware::class)
->args([service('lock.factory')]);
};
10 changes: 0 additions & 10 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
includes:
- vendor/jangregor/phpstan-prophecy/extension.neon
- vendor/phpstan/phpstan-symfony/extension.neon
- vendor/phpstan/phpstan-doctrine/extension.neon
- vendor/phpstan/phpstan-doctrine/rules.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-webmozart-assert/extension.neon
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon

parameters:
paths:
- src
Expand Down
58 changes: 35 additions & 23 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,54 @@

declare(strict_types=1);

use Rector\Core\Configuration\Option;
use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\PHPUnit\Set\PHPUnitLevelSetList;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\Symfony\Set\SymfonyLevelSetList;
use Rector\Symfony\Set\SymfonySetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$services = $containerConfigurator->services();
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
__DIR__ . '/src',
__DIR__ . '/tests',
]);

$parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/tests']);
$parameters->set(Option::SKIP, [
$rectorConfig->skip([
__DIR__ . '/tests/Application/var',
__DIR__ . '/tests/Application/config',
]);
$parameters->set(Option::PHPSTAN_FOR_RECTOR_PATH, __DIR__ . '/phpstan.neon');

$rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');

// basic rules
$parameters->set(Option::AUTO_IMPORT_NAMES, true);
$parameters->set(Option::IMPORT_DOC_BLOCKS, true);
$parameters->set(Option::IMPORT_SHORT_CLASSES, false);
$rectorConfig->importNames();
$rectorConfig->importShortClasses();

$containerConfigurator->import(SetList::CODE_QUALITY);
$containerConfigurator->import(LevelSetList::UP_TO_PHP_80);
$rectorConfig->sets([
SetList::CODE_QUALITY,
LevelSetList::UP_TO_PHP_80,
]);

// symfony rules
$parameters = $containerConfigurator->parameters();
$parameters->set(
Option::SYMFONY_CONTAINER_XML_PATH_PARAMETER,
__DIR__ . '/tests/Application/var/cache/dev/Sulu_Messenger_Tests_Application_KernelDevDebugContainer.xml'
);
$containerConfigurator->import(SymfonySetList::SYMFONY_CODE_QUALITY);
$containerConfigurator->import(SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION);
$containerConfigurator->import(SymfonyLevelSetList::UP_TO_SYMFONY_54);

$containerConfigurator->import(DoctrineSetList::DOCTRINE_CODE_QUALITY);
$rectorConfig->symfonyContainerPhp(__DIR__ . '/tests/Application/var/cache/dev/Sulu_Messenger_Tests_Application_KernelDevDebugContainer.xml');

$rectorConfig->sets([
SymfonySetList::SYMFONY_CODE_QUALITY,
SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION,
SymfonyLevelSetList::UP_TO_SYMFONY_54,
]);

// doctrine rules
$rectorConfig->sets([
DoctrineSetList::DOCTRINE_CODE_QUALITY,
]);

// phpunit rules
$rectorConfig->sets([
PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
PHPUnitSetList::PHPUNIT_91,
]);
};
6 changes: 4 additions & 2 deletions src/Infrastructure/Symfony/HttpKernel/SuluMessengerBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Sulu\Messenger\Infrastructure\Symfony\HttpKernel;

use RuntimeException;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
Expand Down Expand Up @@ -52,7 +53,7 @@ public function getConfiguration(array $config, ContainerBuilder $container): Co
public function prepend(ContainerBuilder $container): void
{
if (!$container->hasExtension('framework')) {
throw new \RuntimeException(\sprintf('The "%s" bundle requires "framework" bundle.', self::ALIAS));
throw new RuntimeException(\sprintf('The "%s" bundle requires "framework" bundle.', self::ALIAS));
}

$container->prependExtensionConfig(
Expand All @@ -64,12 +65,13 @@ public function prepend(ContainerBuilder $container): void
'sulu_message_bus' => [
'middleware' => [
'sulu_messenger.unpack_exception_middleware',
'sulu_messenger.lock_middleware',
'sulu_messenger.doctrine_flush_middleware',
],
],
],
],
]
],
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
class DoctrineFlushMiddleware implements MiddlewareInterface
{
public function __construct(
private EntityManagerInterface $entityManager
private EntityManagerInterface $entityManager,
) {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Sulu\Messenger\Infrastructure\Symfony\Messenger\LockMiddleware;

use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;

class LockMiddleware implements MiddlewareInterface
{
public function __construct(
private LockFactory $lockFactory,
) {
}

public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$locks = [];

try {
/** @var LockStamp[] $lockStamps */
$lockStamps = $envelope->all(LockStamp::class);

foreach ($lockStamps as $lockStamp) {
$lock = $this->lockFactory->createLock(
$lockStamp->getResource(),
$lockStamp->getTtl(),
$lockStamp->getAutoRelease(),
);

$lock->acquire(true);
$locks[] = $lock;
}

$envelope = $stack->next()->handle($envelope, $stack);
} finally {
foreach ($locks as $lock) {
$lock->release();
}
}

return $envelope;
}
}
35 changes: 35 additions & 0 deletions src/Infrastructure/Symfony/Messenger/LockMiddleware/LockStamp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Sulu\Messenger\Infrastructure\Symfony\Messenger\LockMiddleware;

use Symfony\Component\Messenger\Stamp\StampInterface;

/**
* Stamp to lock a process handling to avoid race conditions.
*/
class LockStamp implements StampInterface
{
public function __construct(
private string $resource,
private ?float $ttl = 300.0,
private bool $autoRelease = true,
) {
}

public function getResource(): string
{
return $this->resource;
}

public function getTtl(): ?float
{
return $this->ttl;
}

public function getAutoRelease(): bool
{
return $this->autoRelease;
}
}
17 changes: 9 additions & 8 deletions tests/Traits/PrivatePropertyTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,31 @@

namespace Sulu\Messenger\Tests\Traits;

use ReflectionClass;
use ReflectionException;
use ReflectionProperty;

trait PrivatePropertyTrait
{
/**
* @return mixed
*/
protected static function getPrivateProperty(object $object, string $propertyName)
{
$reflection = new \ReflectionClass($object);
$reflection = new ReflectionClass($object);
$propertyReflection = $reflection->getProperty($propertyName);
$propertyReflection->setAccessible(true);

return $propertyReflection->getValue($object);
}

/**
* @param mixed $value
*/
protected static function setPrivateProperty(object $object, string $propertyName, $value): void
protected static function setPrivateProperty(object $object, string $propertyName, mixed $value): void
{
$reflection = new \ReflectionClass($object);
$reflection = new ReflectionClass($object);
try {
$propertyReflection = $reflection->getProperty($propertyName);
self::setValue($propertyReflection, $object, $value);
} catch (\ReflectionException) {
} catch (ReflectionException) {
$parent = $reflection->getParentClass();
if ($parent) {
$propertyReflection = $parent->getProperty($propertyName);
Expand All @@ -36,7 +37,7 @@ protected static function setPrivateProperty(object $object, string $propertyNam
}
}

private static function setValue(\ReflectionProperty $propertyReflection, object $object, mixed $value): void
private static function setValue(ReflectionProperty $propertyReflection, object $object, mixed $value): void
{
$propertyReflection->setAccessible(true);

Expand Down
Loading

0 comments on commit b4450d1

Please sign in to comment.