diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index fe90dd4..d98e6bc 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -32,7 +32,7 @@ jobs: - dependencies: lowest stability: stable php-version: 8.1 - symfony: 5.4.* + symfony: 6.3.* steps: - name: Checkout diff --git a/README.md b/README.md index bcfefc7..7a44f18 100644 --- a/README.md +++ b/README.md @@ -51,18 +51,15 @@ return [ ksaveras_circuit_breaker: circuit_breakers: cb_name: - storage: 'apcu' + storage: 'cache' failure_threshold: 3 - retry_policy: - type: 'exponential' - options: + retry_policy: + exponential: reset_timeout: 60 + maximum_timeout: 86400 storage: - apcu: ~ in_memory: ~ - redis: 'client_service_id' - psr_cache: 'pool_service_id' - predis: 'predis_service_id' - my_storage: 'storage_service' + cache: 'pool_service_id' + my_storage: '@storage_service' ``` diff --git a/composer.json b/composer.json index c58a4d5..293d78c 100644 --- a/composer.json +++ b/composer.json @@ -12,13 +12,14 @@ ], "require": { "php": "^8.1", - "ksaveras/circuit-breaker": "^1.0", + "ksaveras/circuit-breaker": "dev-release-v2.0.0", "symfony/framework-bundle": "^5.0|^6.0|^7.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "phpstan/phpstan": "^1.6", "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.5", "phpunit/phpunit": "^10.2", "rector/rector": "^0.17.0" }, @@ -39,7 +40,8 @@ "rector": "./vendor/bin/rector process --dry-run", "rector:fix": "./vendor/bin/rector process", "test": "./vendor/bin/phpunit", - "test:coverage": "@php -dapc.enable_cli=1 ./vendor/bin/phpunit --coverage-clover=coverage/clover.xml" + "test:coverage": "./vendor/bin/phpunit --coverage-clover=coverage/clover.xml", + "static-analysis": ["@phpcs", "@phpstan", "@rector"] }, "config": { "sort-packages": true diff --git a/phpstan.neon b/phpstan.neon index 859eda1..59be05b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,10 +10,15 @@ parameters: count: 1 path: src/DependencyInjection/Configuration.php - - message: "#^Cannot call method integerNode\\(\\) on Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\|null\\.$#" + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:addDefaultsIfNotSet\\(\\)\\.$#" + count: 3 + path: src/DependencyInjection/Configuration.php + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:useAttributeAsKey\\(\\)\\.$#" count: 1 path: src/DependencyInjection/Configuration.php includes: - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/rector.php b/rector.php index 7aed73d..598b642 100644 --- a/rector.php +++ b/rector.php @@ -12,6 +12,9 @@ use Rector\CodingStyle\Rector\ClassMethod\NewlineBeforeNewAssignSetRector; use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector; use Rector\Config\RectorConfig; +use Rector\PHPUnit\Rector\Class_\AddSeeTestAnnotationRector; +use Rector\PHPUnit\Rector\Class_\PreferPHPUnitSelfCallRector; +use Rector\PHPUnit\Rector\Class_\PreferPHPUnitThisCallRector; use Rector\PHPUnit\Set\PHPUnitLevelSetList; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\LevelSetList; @@ -20,9 +23,8 @@ return static function (RectorConfig $rectorConfig): void { $rectorConfig->paths([ - __DIR__.'/DependencyInjection', - __DIR__.'/Resources', - __DIR__.'/Tests', + __DIR__.'/src', + __DIR__.'/tests', ]); // define sets of rules @@ -40,9 +42,15 @@ PHPUnitSetList::PHPUNIT_CODE_QUALITY, ]); + $rectorConfig->rules([ + PreferPHPUnitSelfCallRector::class, + ]); + $rectorConfig->skip([ AddReturnTypeDeclarationFromYieldsRector::class, + AddSeeTestAnnotationRector::class, FlipTypeControlToUseExclusiveTypeRector::class, + PreferPHPUnitThisCallRector::class, NewlineAfterStatementRector::class, NewlineBeforeNewAssignSetRector::class, VarConstantCommentRector::class, diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 4e03db1..1a03bde 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -10,6 +10,7 @@ namespace Ksaveras\CircuitBreakerBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -30,11 +31,12 @@ public function getConfigTreeBuilder() private function addStorageSection(ArrayNodeDefinition $rootNode): void { $rootNode + ->fixXmlConfig('storage') ->children() - ->arrayNode('storage') + ->arrayNode('storages') ->useAttributeAsKey('name') ->beforeNormalization() - ->always(static function ($config) { + ->always(static function ($config): array { if (!\is_array($config)) { return []; } @@ -50,9 +52,8 @@ private function addStorageSection(ArrayNodeDefinition $rootNode): void $config[$name] = ['type' => 'service', 'id' => substr($v, 1)]; } else { $config[$name] = match ($name) { - 'apcu', 'in_memory' => ['type' => $v], - 'redis', 'predis' => ['type' => $name, 'client' => $v], - 'psr_cache' => ['type' => $name, 'pool' => $v], + 'in_memory' => ['type' => $v], + 'cache' => ['type' => $name, 'pool' => $v], default => ['type' => 'service', 'id' => $v], }; } @@ -68,24 +69,15 @@ private function addStorageSection(ArrayNodeDefinition $rootNode): void ->thenInvalid('You must specify service "id" for storage type "service".') ->end() ->validate() - ->ifTrue(static fn($v): bool => 'redis' === $v['type'] && !isset($v['client'])) - ->thenInvalid('You must specify "client" for storage type "redis".') - ->end() - ->validate() - ->ifTrue(static fn($v): bool => 'predis' === $v['type'] && !isset($v['client'])) - ->thenInvalid('You must specify "client" for storage type "predis".') - ->end() - ->validate() - ->ifTrue(static fn($v): bool => 'psr_cache' === $v['type'] && !isset($v['pool'])) - ->thenInvalid('You must specify "pool" for storage type "psr_cache".') + ->ifTrue(static fn($v): bool => 'cache' === $v['type'] && !isset($v['pool'])) + ->thenInvalid('You must specify "pool" for storage type "cache".') ->end() ->children() ->enumNode('type') - ->values(['service', 'apcu', 'in_memory', 'redis', 'psr_cache', 'predis']) + ->values(['service', 'in_memory', 'cache']) ->isRequired() ->end() ->scalarNode('id')->end() - ->scalarNode('client')->end() ->scalarNode('pool')->end() ->end() ->end() @@ -100,6 +92,17 @@ private function addCircuitBreakerSection(ArrayNodeDefinition $rootNode): void ->fixXmlConfig('circuit_breaker') ->children() ->arrayNode('circuit_breakers') + ->beforeNormalization() + ->always(static function (array $values): array { + foreach ($values as $name => $config) { + if (!isset($config['retry_policy'])) { + $values[$name]['retry_policy']['exponential'] = ['enabled' => true]; + } + } + + return $values; + }) + ->end() ->useAttributeAsKey('name') ->arrayPrototype() ->children() @@ -114,26 +117,43 @@ private function addCircuitBreakerSection(ArrayNodeDefinition $rootNode): void ->min(1) ->end() ->arrayNode('retry_policy') + ->beforeNormalization() + ->always(static function (array $values): array { + foreach ($values as $type => $value) { + if (!is_array($value)) { + continue; + } + if (isset($value['enabled'])) { + continue; + } + $values[$type]['enabled'] = true; + } + + return $values; + }) + ->end() ->addDefaultsIfNotSet() ->children() - ->enumNode('type') - ->values(['exponential', 'constant', 'linear']) - ->defaultValue('exponential') - ->cannotBeEmpty() - ->end() - ->arrayNode('options') - ->addDefaultsIfNotSet() - ->children() - ->integerNode('reset_timeout') - ->defaultValue(60) - ->min(1) - ->end() - ->integerNode('maximum_timeout') - ->defaultValue(86400) - ->min(1) - ->end() - ->end() - ->end() + ->append($this->addExponentialRetryPolicyNode()) + ->append($this->addLinearRetryPolicyNode()) + ->append($this->addConstantRetryPolicyNode()) + ->end() + ->validate() + ->ifTrue(static fn($v): bool => 1 !== count(array_filter( + array_values($v), + static fn ($i) => $i['enabled'] ?? false + ))) + ->thenInvalid('Only one retry policy can be configured per circuit breaker.') + ->end() + ->end() + ->arrayNode('header_policy') + ->info('List of header policies') + ->defaultValue(['retry_after', 'rate_limit']) + ->treatNullLike([]) + ->treatFalseLike([]) + ->treatTrueLike(['retry_after', 'rate_limit']) + ->enumPrototype() + ->values(['retry_after', 'rate_limit']) ->end() ->end() ->end() @@ -141,4 +161,81 @@ private function addCircuitBreakerSection(ArrayNodeDefinition $rootNode): void ->end() ; } + + private function addExponentialRetryPolicyNode(): NodeDefinition + { + $treeBuilder = new TreeBuilder('exponential'); + + return $treeBuilder->getRootNode() + ->treatFalseLike(['enabled' => false]) + ->treatTrueLike(['enabled' => true]) + ->treatNullLike(['enabled' => true]) + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled')->defaultFalse()->end() + ->integerNode('reset_timeout') + ->info('Number of seconds the circuit breaker will be in open state') + ->defaultValue(10) + ->min(1) + ->end() + ->floatNode('base') + ->info('Base value for exponential function') + ->defaultValue(2.0) + ->min(1.01) + ->end() + ->integerNode('maximum_timeout') + ->info('Maximum number of seconds for open circuit') + ->defaultValue(86400) + ->min(10) + ->end() + ->end(); + } + + private function addLinearRetryPolicyNode(): NodeDefinition + { + $treeBuilder = new TreeBuilder('linear'); + + return $treeBuilder->getRootNode() + ->treatFalseLike(['enabled' => false]) + ->treatTrueLike(['enabled' => true]) + ->treatNullLike(['enabled' => true]) + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled')->defaultFalse()->end() + ->integerNode('reset_timeout') + ->info('Number of seconds the circuit breaker will be in open state') + ->defaultValue(10) + ->min(1) + ->end() + ->integerNode('step') + ->info('Step size in seconds for increasing the open circuit TTL') + ->defaultValue(60) + ->min(1) + ->end() + ->integerNode('maximum_timeout') + ->info('Maximum number of seconds for open circuit') + ->defaultValue(86400) + ->min(10) + ->end() + ->end(); + } + + private function addConstantRetryPolicyNode(): NodeDefinition + { + $treeBuilder = new TreeBuilder('constant'); + + return $treeBuilder->getRootNode() + ->treatFalseLike(['enabled' => false]) + ->treatTrueLike(['enabled' => true]) + ->treatNullLike(['enabled' => true]) + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('enabled')->defaultFalse()->end() + ->integerNode('reset_timeout') + ->info('Number of seconds the circuit breaker will be in open state') + ->defaultValue(10) + ->min(1) + ->end() + ->end(); + } } diff --git a/src/DependencyInjection/Factory/Storage/ApcuStorageFactory.php b/src/DependencyInjection/Factory/Storage/CacheStorageFactory.php similarity index 65% rename from src/DependencyInjection/Factory/Storage/ApcuStorageFactory.php rename to src/DependencyInjection/Factory/Storage/CacheStorageFactory.php index 40de7a6..2b8d136 100644 --- a/src/DependencyInjection/Factory/Storage/ApcuStorageFactory.php +++ b/src/DependencyInjection/Factory/Storage/CacheStorageFactory.php @@ -9,21 +9,23 @@ */ namespace Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage; -use Symfony\Component\DependencyInjection\ChildDefinition; +use Ksaveras\CircuitBreaker\Storage\CacheStorage; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; -final class ApcuStorageFactory extends AbstractStorageFactory +final class CacheStorageFactory extends AbstractStorageFactory { public function create(ContainerBuilder $container, string $name, array $config = []): string { $id = $this->serviceId($name); - $container->setDefinition($id, new ChildDefinition('ksaveras_circuit_breaker.storage.apcu.abstract')); + $definition = $container->register($id, CacheStorage::class); + $definition->setArguments([new Reference($config['pool'])]); return $id; } public function getType(): string { - return 'apcu'; + return 'cache'; } } diff --git a/src/DependencyInjection/Factory/Storage/InMemoryStorageFactory.php b/src/DependencyInjection/Factory/Storage/InMemoryStorageFactory.php index 2d8ca92..4ad121c 100644 --- a/src/DependencyInjection/Factory/Storage/InMemoryStorageFactory.php +++ b/src/DependencyInjection/Factory/Storage/InMemoryStorageFactory.php @@ -9,7 +9,7 @@ */ namespace Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage; -use Symfony\Component\DependencyInjection\ChildDefinition; +use Ksaveras\CircuitBreaker\Storage\InMemoryStorage; use Symfony\Component\DependencyInjection\ContainerBuilder; final class InMemoryStorageFactory extends AbstractStorageFactory @@ -17,7 +17,7 @@ final class InMemoryStorageFactory extends AbstractStorageFactory public function create(ContainerBuilder $container, string $name, array $config = []): string { $id = $this->serviceId($name); - $container->setDefinition($id, new ChildDefinition('ksaveras_circuit_breaker.storage.in_memory.abstract')); + $container->register($id, InMemoryStorage::class); return $id; } diff --git a/src/DependencyInjection/Factory/Storage/PredisStorageFactory.php b/src/DependencyInjection/Factory/Storage/PredisStorageFactory.php deleted file mode 100644 index bb9d6c8..0000000 --- a/src/DependencyInjection/Factory/Storage/PredisStorageFactory.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage; - -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -final class PredisStorageFactory extends AbstractStorageFactory -{ - public function create(ContainerBuilder $container, string $name, array $config = []): string - { - $id = $this->serviceId($name); - $definition = new ChildDefinition('ksaveras_circuit_breaker.storage.predis.abstract'); - $definition->replaceArgument(0, new Reference($config['client'])); - - $container->setDefinition($id, $definition); - - return $id; - } - - public function getType(): string - { - return 'predis'; - } -} diff --git a/src/DependencyInjection/Factory/Storage/PsrCacheStorageFactory.php b/src/DependencyInjection/Factory/Storage/PsrCacheStorageFactory.php deleted file mode 100644 index 5d2e1f3..0000000 --- a/src/DependencyInjection/Factory/Storage/PsrCacheStorageFactory.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage; - -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -final class PsrCacheStorageFactory extends AbstractStorageFactory -{ - public function create(ContainerBuilder $container, string $name, array $config = []): string - { - $id = $this->serviceId($name); - $definition = new ChildDefinition('ksaveras_circuit_breaker.storage.psr_cache.abstract'); - $definition->replaceArgument(0, new Reference($config['pool'])); - - $container->setDefinition($id, $definition); - - return $id; - } - - public function getType(): string - { - return 'psr_cache'; - } -} diff --git a/src/DependencyInjection/Factory/Storage/RedisStorageFactory.php b/src/DependencyInjection/Factory/Storage/RedisStorageFactory.php deleted file mode 100644 index 14e2b15..0000000 --- a/src/DependencyInjection/Factory/Storage/RedisStorageFactory.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage; - -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -final class RedisStorageFactory extends AbstractStorageFactory -{ - public function create(ContainerBuilder $container, string $name, array $config = []): string - { - $id = $this->serviceId($name); - $definition = new ChildDefinition('ksaveras_circuit_breaker.storage.redis.abstract'); - $definition->replaceArgument(0, new Reference($config['client'])); - - $container->setDefinition($id, $definition); - - return $id; - } - - public function getType(): string - { - return 'redis'; - } -} diff --git a/src/DependencyInjection/KsaverasCircuitBreakerExtension.php b/src/DependencyInjection/KsaverasCircuitBreakerExtension.php index 9395fc7..5862c9f 100644 --- a/src/DependencyInjection/KsaverasCircuitBreakerExtension.php +++ b/src/DependencyInjection/KsaverasCircuitBreakerExtension.php @@ -11,10 +11,16 @@ use Ksaveras\CircuitBreaker\CircuitBreaker; use Ksaveras\CircuitBreaker\CircuitBreakerFactory; +use Ksaveras\CircuitBreaker\CircuitBreakerInterface; +use Ksaveras\CircuitBreaker\HeaderPolicy\PolicyChain; +use Ksaveras\CircuitBreaker\HeaderPolicy\RateLimitPolicy; +use Ksaveras\CircuitBreaker\HeaderPolicy\RetryAfterPolicy; +use Ksaveras\CircuitBreaker\Policy\ConstantRetryPolicy; +use Ksaveras\CircuitBreaker\Policy\ExponentialRetryPolicy; +use Ksaveras\CircuitBreaker\Policy\LinearRetryPolicy; use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\StorageFactoryInterface; -use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; @@ -30,29 +36,35 @@ public function addStorageFactory(StorageFactoryInterface $factory): void $this->storageFactories[$factory->getType()] = $factory; } - protected function loadInternal(array $config, ContainerBuilder $container): void + protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void { - $configPath = implode(\DIRECTORY_SEPARATOR, [__DIR__, '..', 'Resources', 'config']); - $loader = new Loader\XmlFileLoader($container, new FileLocator($configPath)); - $loader->load('storage.xml'); + $this->createStorages($container, $mergedConfig['storages']); + $this->createCircuitBreakers($container, $mergedConfig['circuit_breakers']); + } - $storage = []; - foreach ($config['storage'] as $name => $storageConfig) { + private function createStorages(ContainerBuilder $container, array $storages): void + { + foreach ($storages as $name => $storageConfig) { if (!isset($this->storageFactories[$storageConfig['type']])) { throw new \RuntimeException(sprintf('Storage factory of type "%s" is not registered', $storageConfig['type'])); } - $storage[$name] = $this->storageFactories[$storageConfig['type']]->create( - $container, $name, $storageConfig - ); + $this->storageFactories[$storageConfig['type']]->create($container, $name, $storageConfig); } + } - foreach ($config['circuit_breakers'] as $name => $serviceConfig) { - $storageName = $serviceConfig['storage']; - unset($serviceConfig['storage']); + private function createCircuitBreakers(ContainerBuilder $container, array $circuitBreakers): void + { + foreach ($circuitBreakers as $name => $serviceConfig) { + $policyDefinition = $this->createRetryPolicyDefinition($serviceConfig['retry_policy']); $factory = $container ->register(sprintf('ksaveras_circuit_breaker.factory.%s', $name), CircuitBreakerFactory::class) - ->setArguments([$serviceConfig, new Reference($storage[$storageName])]); + ->setArguments([ + $serviceConfig['failure_threshold'], + new Reference(sprintf('ksaveras_circuit_breaker.storage.%s', $serviceConfig['storage'])), + $policyDefinition, + $this->createHeaderPolicyDefinition($serviceConfig['header_policy']), + ]); $id = sprintf('ksaveras_circuit_breaker.circuit.%s', $name); $container->register($id, CircuitBreaker::class) @@ -60,7 +72,42 @@ protected function loadInternal(array $config, ContainerBuilder $container): voi ->setArguments([$name]) ->setPublic(true); - $container->registerAliasForArgument($id, CircuitBreaker::class, $name); + $container->registerAliasForArgument($id, CircuitBreakerInterface::class, $name)->setPublic(false); + } + } + + private function createRetryPolicyDefinition(array $policyOptions): Definition + { + foreach ($policyOptions as $type => $options) { + if ($options['enabled']) { + return match ($type) { + 'constant' => new Definition(ConstantRetryPolicy::class, [$options['reset_timeout']]), + 'exponential' => new Definition(ExponentialRetryPolicy::class, [$options['reset_timeout'], $options['maximum_timeout'], (float) $options['base']]), + 'linear' => new Definition(LinearRetryPolicy::class, [$options['reset_timeout'], $options['maximum_timeout'], $options['step']]), + default => throw new \InvalidArgumentException(), + }; + } + } + + throw new \InvalidArgumentException(); + } + + private function createHeaderPolicyDefinition(array $policyOptions): Definition + { + $headerPolicies = []; + + foreach ($policyOptions as $policyName) { + $definition = match ($policyName) { + 'retry_after' => new Definition(RetryAfterPolicy::class), + 'rate_limit' => new Definition(RateLimitPolicy::class), + default => null, + }; + + if (null !== $definition) { + $headerPolicies[] = $definition; + } } + + return new Definition(PolicyChain::class, [$headerPolicies]); } } diff --git a/src/KsaverasCircuitBreakerBundle.php b/src/KsaverasCircuitBreakerBundle.php index 8f6ca4a..8d07478 100644 --- a/src/KsaverasCircuitBreakerBundle.php +++ b/src/KsaverasCircuitBreakerBundle.php @@ -9,17 +9,14 @@ */ namespace Ksaveras\CircuitBreakerBundle; -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\ApcuStorageFactory; +use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\CacheStorageFactory; use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\InMemoryStorageFactory; -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\PredisStorageFactory; -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\PsrCacheStorageFactory; -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\RedisStorageFactory; use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\ServiceStorageFactory; use Ksaveras\CircuitBreakerBundle\DependencyInjection\KsaverasCircuitBreakerExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; -class KsaverasCircuitBreakerBundle extends Bundle +final class KsaverasCircuitBreakerBundle extends Bundle { public function build(ContainerBuilder $container): void { @@ -27,11 +24,8 @@ public function build(ContainerBuilder $container): void /** @var KsaverasCircuitBreakerExtension $extension */ $extension = $container->getExtension('ksaveras_circuit_breaker'); - $extension->addStorageFactory(new ApcuStorageFactory()); $extension->addStorageFactory(new InMemoryStorageFactory()); - $extension->addStorageFactory(new PredisStorageFactory()); - $extension->addStorageFactory(new PsrCacheStorageFactory()); - $extension->addStorageFactory(new RedisStorageFactory()); + $extension->addStorageFactory(new CacheStorageFactory()); $extension->addStorageFactory(new ServiceStorageFactory()); } } diff --git a/src/Resources/config/storage.xml b/src/Resources/config/storage.xml deleted file mode 100644 index af511de..0000000 --- a/src/Resources/config/storage.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 4d5e2d2..a3a72f4 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -23,206 +23,145 @@ public function testConfiguration(array $configs, array $expected): void $configuration = new Configuration(); $processor = new Processor(); - $config = $processor->processConfiguration($configuration, $configs); + $config = $processor->processConfiguration( + $configuration, + ['ksaveras_circuit_breaker' => $configs], + ); self::assertEquals($expected, $config); } public static function configsDataProvider(): iterable { - yield [ - [ - 'ksaveras_circuit_breaker' => [ - 'circuit_breakers' => [ - 'web_api' => [ - 'storage' => 'in_memory', - 'retry_policy' => [ - 'type' => 'exponential', - ], - ], - ], - ], - ], + yield 'empty configuration' => [ + [], + ['circuit_breakers' => [], 'storages' => []], + ]; + + yield 'storage configuration' => [ [ - 'storage' => [], - 'circuit_breakers' => [ - 'web_api' => [ - 'storage' => 'in_memory', - 'failure_threshold' => 3, - 'retry_policy' => [ - 'type' => 'exponential', - 'options' => [ - 'reset_timeout' => 60, - 'maximum_timeout' => 86400, - ], - ], - ], + 'storages' => [ + 'in_memory' => null, + 'my_memory' => ['type' => 'in_memory'], + 'cache' => 'cache.pool.array', + 'cache_redis' => ['type' => 'cache', 'pool' => 'cache.pool.redis'], + 'service_one' => '@app_service.one', + 'service_two' => ['type' => 'service', 'id' => 'app_service.two'], ], ], - ]; - - yield [ - [], [ - 'storage' => [], 'circuit_breakers' => [], + 'storages' => [ + 'in_memory' => ['type' => 'in_memory'], + 'my_memory' => ['type' => 'in_memory'], + 'cache' => ['type' => 'cache', 'pool' => 'cache.pool.array'], + 'cache_redis' => ['type' => 'cache', 'pool' => 'cache.pool.redis'], + 'service_one' => ['type' => 'service', 'id' => 'app_service.one'], + 'service_two' => ['type' => 'service', 'id' => 'app_service.two'], + ], ], ]; - yield [ + yield 'exponential retry policy defaults' => [ [ - 'ksaveras_circuit_breaker' => [ - 'circuit_breakers' => [ - 'web_api' => [ - 'storage' => 'in_memory', - ], + 'circuit_breakers' => [ + 'web_api' => [ + 'storage' => 'memory', ], ], ], [ - 'storage' => [], 'circuit_breakers' => [ 'web_api' => [ - 'storage' => 'in_memory', + 'storage' => 'memory', 'failure_threshold' => 3, - 'retry_policy' => [ - 'type' => 'exponential', - 'options' => [ - 'reset_timeout' => 60, - 'maximum_timeout' => 86400, - ], - ], + 'retry_policy' => self::getDefaultPolicyConfiguration(), + 'header_policy' => ['retry_after', 'rate_limit'], ], ], + 'storages' => [], ], ]; - yield [ + $policyConfig = self::getDefaultPolicyConfiguration(); + $policyConfig['exponential']['enabled'] = false; + $policyConfig['linear']['enabled'] = true; + + yield 'linear retry policy defaults' => [ [ - 'ksaveras_circuit_breaker' => [ - 'circuit_breakers' => [ - 'web_api' => [ - 'storage' => 'storage_service', - 'failure_threshold' => 10, - 'retry_policy' => [ - 'type' => 'constant', - 'options' => [ - 'reset_timeout' => 600, - ], - ], + 'circuit_breakers' => [ + 'web_api' => [ + 'storage' => 'memory', + 'retry_policy' => [ + 'linear' => true, ], ], ], ], [ - 'storage' => [], 'circuit_breakers' => [ 'web_api' => [ - 'storage' => 'storage_service', - 'failure_threshold' => 10, - 'retry_policy' => [ - 'type' => 'constant', - 'options' => [ - 'reset_timeout' => 600, - 'maximum_timeout' => 86400, - ], - ], + 'storage' => 'memory', + 'failure_threshold' => 3, + 'retry_policy' => $policyConfig, + 'header_policy' => ['retry_after', 'rate_limit'], ], ], + 'storages' => [], ], ]; - } - - #[DataProvider('storageTypeConfigProvider')] - public function testStorageTypeConfiguration(array $config, array $expected): void - { - $configuration = new Configuration(); - - $configs = [ - 'ksaveras_circuit_breaker' => $config, - ]; - $processor = new Processor(); - $config = $processor->processConfiguration($configuration, $configs); + $policyConfig = self::getDefaultPolicyConfiguration(); + $policyConfig['exponential']['enabled'] = false; + $policyConfig['constant']['enabled'] = true; - self::assertEquals($expected, $config['storage']); - } - - public static function storageTypeConfigProvider(): iterable - { - yield [ - [], - [], - ]; - - yield [ + yield 'constant retry policy defaults' => [ [ - 'storage' => [ - 'apcu' => null, - ], - ], - [ - 'apcu' => [ - 'type' => 'apcu', - ], - ], - ]; - - yield [ - [ - 'storage' => [ - 'my_storage' => [ - 'type' => 'apcu', + 'circuit_breakers' => [ + 'web_api' => [ + 'storage' => 'memory', + 'retry_policy' => [ + 'constant' => true, + ], ], ], ], [ - 'my_storage' => [ - 'type' => 'apcu', + 'circuit_breakers' => [ + 'web_api' => [ + 'storage' => 'memory', + 'failure_threshold' => 3, + 'retry_policy' => $policyConfig, + 'header_policy' => ['retry_after', 'rate_limit'], + ], ], + 'storages' => [], ], ]; - yield [ + yield 'header policies as null' => [ [ - 'storage' => [ - 'my_storage' => 'private_storage', + 'circuit_breakers' => [ + 'web_api' => [ + 'storage' => 'memory', + 'header_policy' => null, + ], ], ], [ - 'my_storage' => [ - 'type' => 'service', - 'id' => 'private_storage', + 'circuit_breakers' => [ + 'web_api' => [ + 'storage' => 'memory', + 'failure_threshold' => 3, + 'retry_policy' => self::getDefaultPolicyConfiguration(), + 'header_policy' => [], + ], ], + 'storages' => [], ], ]; } - public function testStorageTypeService(): void - { - $configuration = new Configuration(); - - $configs = [ - 'ksaveras_circuit_breaker' => [ - 'storage' => [ - 'my_storage' => '@service_id', - ], - ], - ]; - - $expected = [ - 'my_storage' => [ - 'type' => 'service', - 'id' => 'service_id', - ], - ]; - - $processor = new Processor(); - $config = $processor->processConfiguration($configuration, $configs); - - self::assertEquals($expected, $config['storage']); - } - public function testMissingTypeServiceId(): void { $this->expectException(InvalidConfigurationException::class); @@ -230,7 +169,7 @@ public function testMissingTypeServiceId(): void $configs = [ 'ksaveras_circuit_breaker' => [ - 'storage' => [ + 'storages' => [ 'my_storage' => [ 'type' => 'service', ], @@ -242,16 +181,16 @@ public function testMissingTypeServiceId(): void $processor->processConfiguration(new Configuration(), $configs); } - public function testMissingPhpRedisClientConfig(): void + public function testMissingCachePoolConfig(): void { $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('You must specify "client" for storage type "redis".'); + $this->expectExceptionMessage('You must specify "pool" for storage type "cache".'); $configs = [ 'ksaveras_circuit_breaker' => [ - 'storage' => [ + 'storages' => [ 'my_storage' => [ - 'type' => 'redis', + 'type' => 'cache', ], ], ], @@ -261,41 +200,25 @@ public function testMissingPhpRedisClientConfig(): void $processor->processConfiguration(new Configuration(), $configs); } - public function testMissingPredisClientConfig(): void + private static function getDefaultPolicyConfiguration(): array { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('You must specify "client" for storage type "predis".'); - - $configs = [ - 'ksaveras_circuit_breaker' => [ - 'storage' => [ - 'my_storage' => [ - 'type' => 'predis', - ], - ], + return [ + 'exponential' => [ + 'reset_timeout' => 10, + 'base' => 2, + 'maximum_timeout' => 86400, + 'enabled' => true, + ], + 'linear' => [ + 'reset_timeout' => 10, + 'step' => 60, + 'maximum_timeout' => 86400, + 'enabled' => false, + ], + 'constant' => [ + 'reset_timeout' => 10, + 'enabled' => false, ], ]; - - $processor = new Processor(); - $processor->processConfiguration(new Configuration(), $configs); - } - - public function testMissingPsrCachePoolConfig(): void - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('You must specify "pool" for storage type "psr_cache".'); - - $configs = [ - 'ksaveras_circuit_breaker' => [ - 'storage' => [ - 'my_storage' => [ - 'type' => 'psr_cache', - ], - ], - ], - ]; - - $processor = new Processor(); - $processor->processConfiguration(new Configuration(), $configs); } } diff --git a/tests/DependencyInjection/Factory/Storage/ApcuStorageFactoryTest.php b/tests/DependencyInjection/Factory/Storage/ApcuStorageFactoryTest.php deleted file mode 100644 index d0a9c60..0000000 --- a/tests/DependencyInjection/Factory/Storage/ApcuStorageFactoryTest.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Ksaveras\CircuitBreakerBundle\Tests\DependencyInjection\Factory\Storage; - -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\ApcuStorageFactory; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -final class ApcuStorageFactoryTest extends TestCase -{ - public function testCreate(): void - { - $factory = new ApcuStorageFactory(); - $expectedId = 'ksaveras_circuit_breaker.storage.dummy'; - - self::assertEquals('apcu', $factory->getType()); - - $container = $this->createMock(ContainerBuilder::class); - $container - ->expects(self::once()) - ->method('setDefinition') - ->with($expectedId, new ChildDefinition('ksaveras_circuit_breaker.storage.apcu.abstract')); - - $id = $factory->create($container, 'dummy'); - - self::assertEquals($expectedId, $id); - } -} diff --git a/tests/DependencyInjection/Factory/Storage/InMemoryStorageFactoryTest.php b/tests/DependencyInjection/Factory/Storage/InMemoryStorageFactoryTest.php deleted file mode 100644 index a6e3e83..0000000 --- a/tests/DependencyInjection/Factory/Storage/InMemoryStorageFactoryTest.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Ksaveras\CircuitBreakerBundle\Tests\DependencyInjection\Factory\Storage; - -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\InMemoryStorageFactory; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -final class InMemoryStorageFactoryTest extends TestCase -{ - public function testCreate(): void - { - $factory = new InMemoryStorageFactory(); - $expectedId = 'ksaveras_circuit_breaker.storage.dummy'; - - self::assertEquals('in_memory', $factory->getType()); - - $container = $this->createMock(ContainerBuilder::class); - $container - ->expects(self::once()) - ->method('setDefinition') - ->with($expectedId, new ChildDefinition('ksaveras_circuit_breaker.storage.in_memory.abstract')); - - $id = $factory->create($container, 'dummy'); - - self::assertEquals($expectedId, $id); - } -} diff --git a/tests/DependencyInjection/Factory/Storage/PredisStorageFactoryTest.php b/tests/DependencyInjection/Factory/Storage/PredisStorageFactoryTest.php deleted file mode 100644 index 1d13625..0000000 --- a/tests/DependencyInjection/Factory/Storage/PredisStorageFactoryTest.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Ksaveras\CircuitBreakerBundle\Tests\DependencyInjection\Factory\Storage; - -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\PredisStorageFactory; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -final class PredisStorageFactoryTest extends TestCase -{ - public function testCreate(): void - { - $factory = new PredisStorageFactory(); - $expectedId = 'ksaveras_circuit_breaker.storage.dummy'; - - self::assertEquals('predis', $factory->getType()); - - $container = $this->createMock(ContainerBuilder::class); - $container - ->expects(self::once()) - ->method('setDefinition') - ->with($expectedId, self::callback(static function ($definition): bool { - self::assertInstanceOf(ChildDefinition::class, $definition); - self::assertEquals('ksaveras_circuit_breaker.storage.predis.abstract', $definition->getParent()); - self::assertEquals(new Reference('predis_service_id'), $definition->getArgument(0)); - - return true; - })); - - $id = $factory->create($container, 'dummy', ['client' => 'predis_service_id']); - - self::assertEquals($expectedId, $id); - } -} diff --git a/tests/DependencyInjection/Factory/Storage/PsrCacheStorageFactoryTest.php b/tests/DependencyInjection/Factory/Storage/PsrCacheStorageFactoryTest.php deleted file mode 100644 index c8f12a5..0000000 --- a/tests/DependencyInjection/Factory/Storage/PsrCacheStorageFactoryTest.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Ksaveras\CircuitBreakerBundle\Tests\DependencyInjection\Factory\Storage; - -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\PsrCacheStorageFactory; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -final class PsrCacheStorageFactoryTest extends TestCase -{ - public function testCreate(): void - { - $factory = new PsrCacheStorageFactory(); - $expectedId = 'ksaveras_circuit_breaker.storage.dummy'; - - self::assertEquals('psr_cache', $factory->getType()); - - $container = $this->createMock(ContainerBuilder::class); - $container - ->expects(self::once()) - ->method('setDefinition') - ->with($expectedId, self::callback(static function ($definition): bool { - self::assertInstanceOf(ChildDefinition::class, $definition); - self::assertEquals('ksaveras_circuit_breaker.storage.psr_cache.abstract', $definition->getParent()); - self::assertEquals(new Reference('cache_pool'), $definition->getArgument(0)); - - return true; - })); - - $id = $factory->create($container, 'dummy', ['pool' => 'cache_pool']); - - self::assertEquals($expectedId, $id); - } -} diff --git a/tests/DependencyInjection/Factory/Storage/RedisStorageFactoryTest.php b/tests/DependencyInjection/Factory/Storage/RedisStorageFactoryTest.php deleted file mode 100644 index 5898296..0000000 --- a/tests/DependencyInjection/Factory/Storage/RedisStorageFactoryTest.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Ksaveras\CircuitBreakerBundle\Tests\DependencyInjection\Factory\Storage; - -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\RedisStorageFactory; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -final class RedisStorageFactoryTest extends TestCase -{ - public function testCreate(): void - { - $factory = new RedisStorageFactory(); - $expectedId = 'ksaveras_circuit_breaker.storage.dummy'; - - self::assertEquals('redis', $factory->getType()); - - $container = $this->createMock(ContainerBuilder::class); - $container - ->expects(self::once()) - ->method('setDefinition') - ->with($expectedId, self::callback(static function ($definition): bool { - self::assertInstanceOf(ChildDefinition::class, $definition); - self::assertEquals('ksaveras_circuit_breaker.storage.redis.abstract', $definition->getParent()); - self::assertEquals(new Reference('redis_service_id'), $definition->getArgument(0)); - - return true; - })); - - $id = $factory->create($container, 'dummy', ['client' => 'redis_service_id']); - - self::assertEquals($expectedId, $id); - } -} diff --git a/tests/DependencyInjection/KsaverasCircuitBreakerExtensionTest.php b/tests/DependencyInjection/KsaverasCircuitBreakerExtensionTest.php index 2311b04..c4e5f47 100644 --- a/tests/DependencyInjection/KsaverasCircuitBreakerExtensionTest.php +++ b/tests/DependencyInjection/KsaverasCircuitBreakerExtensionTest.php @@ -11,72 +11,265 @@ use Ksaveras\CircuitBreaker\CircuitBreaker; use Ksaveras\CircuitBreaker\CircuitBreakerFactory; +use Ksaveras\CircuitBreaker\CircuitBreakerInterface; +use Ksaveras\CircuitBreaker\Policy\ExponentialRetryPolicy; +use Ksaveras\CircuitBreaker\Storage\CacheStorage; use Ksaveras\CircuitBreaker\Storage\InMemoryStorage; -use Ksaveras\CircuitBreakerBundle\DependencyInjection\Factory\Storage\ServiceStorageFactory; use Ksaveras\CircuitBreakerBundle\DependencyInjection\KsaverasCircuitBreakerExtension; +use Ksaveras\CircuitBreakerBundle\KsaverasCircuitBreakerBundle; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; final class KsaverasCircuitBreakerExtensionTest extends TestCase { - public function testLoadConfig(): void + public function testUnknownStorage(): void { - $config = [ - [ - 'circuit_breakers' => [ - 'demo' => [ - 'storage' => 'private', + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage('You have requested a non-existent service "ksaveras_circuit_breaker.storage.foo_bar".'); + + $container = $this->createContainer(); + + $container->loadFromExtension('ksaveras_circuit_breaker', [ + 'circuit_breakers' => [ + 'web_api' => [ + 'storage' => 'foo_bar', + 'failure_threshold' => 3, + 'retry_policy' => [ + 'exponential' => [ + 'reset_timeout' => 200, + ], + ], + ], + ], + ]); + + $this->compileContainer($container); + $container->get('ksaveras_circuit_breaker.circuit.web_api'); + } + + public function testFullConfig(): void + { + $container = $this->createContainer(); + + $container->loadFromExtension('ksaveras_circuit_breaker', [ + 'circuit_breakers' => [ + 'web-api' => [ + 'storage' => 'memory', + 'failure_threshold' => 2, + 'retry_policy' => [ + 'exponential' => [ + 'reset_timeout' => 2, + 'maximum_timeout' => 600, + ], ], + 'header_policy' => ['retry_after', 'rate_limit'], ], - 'storage' => [ - 'private' => [ - 'type' => 'service', - 'id' => 'storage_service', + 'intraApi' => [ + 'storage' => 'cache', + 'failure_threshold' => 2, + 'retry_policy' => [ + 'linear' => [ + 'reset_timeout' => 2, + 'maximum_timeout' => 600, + ], ], + 'header_policy' => ['retry_after', 'rate_limit'], + ], + ], + 'storages' => [ + 'memory' => [ + 'type' => 'in_memory', + ], + 'cache' => 'cache.adapter.array', + 'my_service' => [ + 'type' => 'service', + 'id' => 'custom_storage_service', ], ], - ]; + ]); + + $this->compileContainer($container); + + self::assertTrue($container->has('ksaveras_circuit_breaker.circuit.web_api')); + $definition = $container->getDefinition('ksaveras_circuit_breaker.circuit.web_api'); + + $this->runAssertCircuitBreakerDefinition($definition, 'web_api'); + $this->runAssertArgumentAlias($container, '$webApi', 'ksaveras_circuit_breaker.circuit.web_api'); + + self::assertTrue($container->has('ksaveras_circuit_breaker.circuit.intraApi')); + $definition = $container->getDefinition('ksaveras_circuit_breaker.circuit.intraApi'); + + $this->runAssertCircuitBreakerDefinition($definition, 'intraApi'); + $this->runAssertArgumentAlias($container, '$intraApi', 'ksaveras_circuit_breaker.circuit.intraApi'); + } + + public function testInMemoryStorage(): void + { + $container = $this->createContainer(); + + $container->loadFromExtension('ksaveras_circuit_breaker', [ + 'storages' => [ + 'memory' => ['type' => 'in_memory'], + ], + ]); + + $this->compileContainer($container); + + self::assertTrue($container->has('ksaveras_circuit_breaker.storage.memory')); + $storageDefinition = $container->getDefinition('ksaveras_circuit_breaker.storage.memory'); + self::assertSame(InMemoryStorage::class, $storageDefinition->getClass()); + } + + public function testCacheStorage(): void + { + $container = $this->createContainer(); + + $container->loadFromExtension('ksaveras_circuit_breaker', [ + 'storages' => [ + 'cache' => 'cache.adapter.array', + 'cache_one' => ['type' => 'cache', 'pool' => 'cache.adapter.redis'], + ], + ]); + + $this->compileContainer($container); - $container = new ContainerBuilder(); - $container->register('storage_service', InMemoryStorage::class); + self::assertTrue($container->has('ksaveras_circuit_breaker.storage.cache')); + $storageDefinition = $container->getDefinition('ksaveras_circuit_breaker.storage.cache'); + self::assertSame(CacheStorage::class, $storageDefinition->getClass()); + self::assertInstanceOf(Reference::class, $storageDefinition->getArgument(0)); + self::assertSame('cache.adapter.array', (string) $storageDefinition->getArgument(0)); - $extension = new KsaverasCircuitBreakerExtension(); - $extension->addStorageFactory(new ServiceStorageFactory()); - $extension->load($config, $container); + self::assertTrue($container->has('ksaveras_circuit_breaker.storage.cache_one')); + $storageDefinition = $container->getDefinition('ksaveras_circuit_breaker.storage.cache_one'); + self::assertSame(CacheStorage::class, $storageDefinition->getClass()); + self::assertInstanceOf(Reference::class, $storageDefinition->getArgument(0)); + self::assertSame('cache.adapter.redis', (string) $storageDefinition->getArgument(0)); + } + + public function testServiceStorage(): void + { + $container = $this->createContainer(); - $this->assertTrue($container->has('ksaveras_circuit_breaker.factory.demo')); + $container->loadFromExtension('ksaveras_circuit_breaker', [ + 'storages' => [ + 'my_service1' => '@my_service_id_one', + 'my_service2' => ['type' => 'service', 'id' => 'my_service_id_two'], + ], + ]); - $definition = $container->getDefinition('ksaveras_circuit_breaker.factory.demo'); - $this->assertEquals(CircuitBreakerFactory::class, $definition->getClass()); - $this->assertEquals(new Reference('ksaveras_circuit_breaker.storage.private'), $definition->getArgument(1)); + $this->compileContainer($container); - $this->assertTrue($container->has('ksaveras_circuit_breaker.circuit.demo')); + self::assertTrue($container->has('ksaveras_circuit_breaker.storage.my_service1')); + $storageAlias = $container->getAlias('ksaveras_circuit_breaker.storage.my_service1'); + self::assertSame('my_service_id_one', (string) $storageAlias); - $definition = $container->getDefinition('ksaveras_circuit_breaker.circuit.demo'); - $this->assertEquals(CircuitBreaker::class, $definition->getClass()); - $this->assertEquals('demo', $definition->getArgument(0)); + self::assertTrue($container->has('ksaveras_circuit_breaker.storage.my_service2')); + $storageAlias = $container->getAlias('ksaveras_circuit_breaker.storage.my_service2'); + self::assertSame('my_service_id_two', (string) $storageAlias); } - public function testUnregisteredStorage(): void + public function testCircuitBreakerFactory(): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Storage factory of type "service" is not registered'); - - $config = [ - [ - 'storage' => [ - 'private' => [ - 'type' => 'service', - 'id' => 'storage_service', + $container = $this->createContainer(); + + $container->loadFromExtension('ksaveras_circuit_breaker', [ + 'circuit_breakers' => [ + 'web-api' => [ + 'storage' => 'in_memory', + 'failure_threshold' => 3, + 'retry_policy' => [ + 'exponential' => [ + 'reset_timeout' => 10, + 'maximum_timeout' => 600, + ], ], ], ], - ]; + 'storages' => [ + 'in_memory' => null, + ], + ]); + + $container->setAlias( + 'circuit_breaker_factory_public', + 'ksaveras_circuit_breaker.factory.web_api' + )->setPublic(true); + + $this->compileContainer($container); + + self::assertTrue($container->has('ksaveras_circuit_breaker.factory.web_api')); + + $definition = $container->getDefinition('ksaveras_circuit_breaker.factory.web_api'); + self::assertSame(CircuitBreakerFactory::class, $definition->getClass()); + + self::assertSame(3, $definition->getArgument(0)); + + self::assertInstanceOf(Reference::class, $definition->getArgument(1)); + self::assertSame('ksaveras_circuit_breaker.storage.in_memory', (string) $definition->getArgument(1)); + + $retryPolicyDefinition = $definition->getArgument(2); + self::assertInstanceOf(Definition::class, $retryPolicyDefinition); + self::assertSame(ExponentialRetryPolicy::class, $retryPolicyDefinition->getClass()); + self::assertSame([10, 600, 2.0], $retryPolicyDefinition->getArguments()); + + $factory = $container->get('circuit_breaker_factory_public'); + self::assertInstanceOf(CircuitBreakerFactory::class, $factory); + } + + private function runAssertCircuitBreakerDefinition(Definition $definition, string $name): void + { + self::assertSame(CircuitBreaker::class, $definition->getClass()); + self::assertSame([$name], $definition->getArguments()); + + $factory = $definition->getFactory(); + self::assertIsArray($factory); + self::assertInstanceOf(Definition::class, $factory[0]); + self::assertSame(CircuitBreakerFactory::class, $factory[0]->getClass()); + + $factoryArguments = $factory[0]->getArguments(); + self::assertInstanceOf(Reference::class, $factoryArguments[1]); + self::assertInstanceOf(Definition::class, $factoryArguments[2]); + self::assertInstanceOf(Definition::class, $factoryArguments[3]); + + self::assertSame('create', $factory[1]); + } + + private function runAssertArgumentAlias(ContainerBuilder $container, string $name, string $serviceId): void + { + $argumentAlias = CircuitBreakerInterface::class.' '.$name; + self::assertTrue($container->hasAlias($argumentAlias)); + + $aliasDefinition = $container->getAlias($argumentAlias); + self::assertSame($serviceId, (string) $aliasDefinition); + self::assertTrue($aliasDefinition->isPrivate()); + } + + private function createContainer(): ContainerBuilder + { + $container = new ContainerBuilder(new ParameterBag([ + 'kernel.debug' => false, + 'kernel.environment' => 'test', + 'kernel.name' => 'kernel', + 'kernel.container_class' => 'TestContainer', + ])); + $container->registerExtension(new KsaverasCircuitBreakerExtension()); + + return $container; + } + + private function compileContainer(ContainerBuilder $container): void + { + $container->getCompilerPassConfig()->setOptimizationPasses([]); + $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); - $container = new ContainerBuilder(); + $bundle = new KsaverasCircuitBreakerBundle(); + $bundle->build($container); - $extension = new KsaverasCircuitBreakerExtension(); - $extension->load($config, $container); + $container->compile(); } }