first commit

This commit is contained in:
2025-01-06 20:47:25 +01:00
commit 3bdbd78c2f
25591 changed files with 3586440 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
workflow "Code Quality" {
on = "push"
resolves = [
"PHPStan",
"PHP-CS-Fixer",
"Psalm",
"PHPQA"
]
}
action "PHPStan" {
uses = "docker://oskarstark/phpstan-ga:with-extensions"
args = "analyse src tests --level max --configuration extension.neon"
secrets = ["GITHUB_TOKEN"]
}
action "PHP-CS-Fixer" {
uses = "docker://oskarstark/php-cs-fixer-ga"
secrets = ["GITHUB_TOKEN"]
args = "--config=.php_cs.dist --diff --dry-run"
}
action "Psalm" {
needs="PHPStan"
uses = "docker://mickaelandrieu/psalm-ga"
secrets = ["GITHUB_TOKEN"]
args = "--find-dead-code --diff --diff-methods"
}
action "PHPQA" {
needs="PHP-CS-Fixer"
uses = "docker://mickaelandrieu/phpqa-ga"
secrets = ["GITHUB_TOKEN"]
args = "--report --tools phpcs:0,phpmd:0,phpcpd:0,parallel-lint:0,phpmetrics,phploc,pdepend --ignoredDirs vendor,tests"
}

View File

@@ -0,0 +1,11 @@
change-template: '- $TITLE (#$NUMBER)'
branches:
- master
template: |
## Changelog
$CHANGES
### Contributors
$CONTRIBUTORS

View File

@@ -0,0 +1,5 @@
build/
vendor/
composer.lock
.php_cs.cache
psalm.xml

View File

@@ -0,0 +1,39 @@
<?php
$finder = PhpCsFixer\Finder::create()->in([
__DIR__.'/src',
__DIR__.'/tests',
]);
return PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'concat_space' => [
'spacing' => 'one',
],
'cast_spaces' => [
'space' => 'single',
],
'error_suppression' => [
'mute_deprecation_error' => false,
'noise_remaining_usages' => false,
'noise_remaining_usages_exclude' => [],
],
'function_to_constant' => false,
'no_alias_functions' => false,
'non_printable_character' => false,
'phpdoc_summary' => false,
'phpdoc_align' => [
'align' => 'left',
],
'protected_to_private' => false,
'psr4' => false,
'self_accessor' => false,
'yoda_style' => null,
'non_printable_character' => true,
'phpdoc_no_empty_return' => false,
])
->setFinder($finder)
->setCacheFile(__DIR__.'/.php_cs.cache')
;

View File

@@ -0,0 +1,30 @@
language: php
notifications:
email:
on_success: never
php:
- 7.2
- 7.3
- 5.6
matrix:
fast_finish: true
allow_failures:
- php: 7.3
cache:
directories:
- $HOME/.composer/cache
before_script:
- composer install -n
script:
- composer test -- --coverage-clover=coverage.xml
- composer phpcs
- composer psalm
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@@ -0,0 +1,19 @@
Copyright (c) 2018-present PrestaShop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,157 @@
# Circuit Breaker, an implementation for resilient PHP applications
[![codecov](https://codecov.io/gh/PrestaShop/circuit-breaker/branch/master/graph/badge.svg)](https://codecov.io/gh/PrestaShop/circuit-breaker) [![PHPStan](https://img.shields.io/badge/PHPStan-Level%207-brightgreen.svg?style=flat&logo=php)](https://shields.io/#/) [![Psalm](https://img.shields.io/badge/Psalm-Level%20Max-brightgreen.svg?style=flat&logo=php)](https://shields.io/#/) [![Build Status](https://travis-ci.com/PrestaShop/circuit-breaker.svg?branch=master)](https://travis-ci.com/PrestaShop/circuit-breaker)
## Main principles
![circuit breaker](https://user-images.githubusercontent.com/1247388/49721725-438bd700-fc63-11e8-8498-82ca681b15fb.png)
This library is compatible with PHP 5.6+.
## Installation
```
composer require prestashop/circuit-breaker
```
## Use
### Simple Circuit Breaker
You can use the factory to create a simple circuit breaker.
By default, you need to define 3 parameters for the circuit breaker:
* the **failures**: define how many times we try to access the service;
* the **timeout**: define how much time we wait before consider the service unreachable;
* the **threshold**: define how much time we wait before trying to access again the service (once it is considered unreachable);
The **fallback** callback will be used if the distant service is unreachable when the Circuit Breaker is Open (means "is used" if the service is unreachable).
> You'd better return the same type of response expected from your distant call.
```php
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\FactorySettings;
$circuitBreakerFactory = new SimpleCircuitBreakerFactory();
$circuitBreaker = $circuitBreakerFactory->create(new FactorySettings(2, 0.1, 10));
$fallbackResponse = function () {
return '{}';
};
$response = $circuitBreaker->call('https://api.domain.com', [], $fallbackResponse);
```
If you don't specify any fallback, by default the circuit breaker will return an empty string.
```php
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\FactorySettings;
$circuitBreakerFactory = new SimpleCircuitBreakerFactory();
$circuitBreaker = $circuitBreakerFactory->create(new FactorySettings(2, 0.1, 10));
$response = $circuitBreaker->call('https://unreacheable.api.domain.com', []); // $response == ''
```
You can also define the client options (or even set your own client if you prefer).
```php
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\FactorySettings;
$circuitBreakerFactory = new SimpleCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 10);
$settings->setClientOptions(['method' => 'POST']);
$circuitBreaker = $circuitBreakerFactory->create($settings);
$response = $circuitBreaker->call('https://api.domain.com/create/user', ['body' => ['firstname' => 'John', 'lastname' => 'Doe']]);
```
> For the Guzzle implementation, the Client options are described
> in the [HttpGuzzle documentation](http://docs.guzzlephp.org/en/stable/index.html).
### Advanced Circuit Breaker
If you need more control on your circuit breaker, you should use the `AdvancedCircuitBreaker` which manages more features:
* the **stripped failures**: define how many times we try to access the service when the circuit breaker is Half Open (when it retires to reach the service after it was unreachable);
* the **stripped timeout**: define how much time we wait before consider the service unreachable (again in Half open state);
* the **storage**: used to store the circuit breaker states and transitions. By default it's an `SimpleArray` so if you want to "cache" the fact that your service is unreachable you should use a persistent storage;
* the **transition dispatcher**: used if you need to subscribe to transition events (ex: a dispatcher based on Symfony EventDispatcher is available)
#### Storage
```php
use Doctrine\Common\Cache\FilesystemCache;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\FactorySettings;
use PrestaShop\CircuitBreaker\Storage\DoctrineCache;
$circuitBreakerFactory = new AdvancedCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 60); //60 seconds threshold
//Once the circuit breaker is open, the fallback response will be returned instantly during the next 60 seconds
//Since the state is persisted even other requests/processes will be aware that the circuit breaker is open
$doctrineCache = new FilesystemCache(_PS_CACHE_DIR_ . '/addons_category');
$storage = new DoctrineCache($doctrineCache);
$settings->setStorage($storage);
$circuitBreaker = $circuitBreakerFactory->create($settings);
$response = $circuitBreaker->call('https://unreachable.api.domain.com/create/user', []);
```
### Guzzle Cache
Besides caching the circuit breaker state, it may be interesting to cache the successful responses. The circuit breaker library doesn't manage the cache itself,
however you can use [Guzzle Subscriber](https://github.com/guzzle/cache-subscriber) to manage it.
```php
use Doctrine\Common\Cache\FilesystemCache;
use GuzzleHttp\Subscriber\Cache\CacheStorage;
use GuzzleHttp\Subscriber\Cache\CacheSubscriber;
use PrestaShop\CircuitBreaker\FactorySettings;
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
$circuitBreakerFactory = new SimpleCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 60); //60 seconds threshold
//Guzzle subsriber is also compatible with doctrine cache, which is why we proposed a storage based on it, this allows you to use for storage and cache
$doctrineCache = new FilesystemCache(_PS_CACHE_DIR_ . '/addons_category');
//By default the ttl is defined by the response headers but you can set a default one
$cacheStorage = new CacheStorage($doctrineCache, null, 60);
$cacheSubscriber = new CacheSubscriber($cacheStorage, function (Request $request) { return true; });
$settings->setClientOptions(['subscribers' => [$cacheSubscriber]]);
$circuitBreaker = $circuitBreakerFactory->create($settings);
$response = $circuitBreaker->call('https://api.domain.com/create/user', []);
```
## Tests
```
composer test
```
## Code quality
```
composer cs-fix && composer phpcb && composer psalm && composer phpcs
```
We also use [PHPQA](https://github.com/EdgedesignCZ/phpqa#phpqa) to check the Code quality
during the CI management of the contributions.
If you want to use it (using Docker):
```
docker run --rm -u $UID -v $(pwd):/app eko3alpha/docker-phpqa --report --ignoredDirs vendor,tests
```
If you want to use it (using Composer):
```
composer global require edgedesign/phpqa=v1.20.0 --update-no-dev
phpqa --report --ignoredDirs vendor,tests
```

View File

@@ -0,0 +1,66 @@
{
"name": "prestashop/circuit-breaker",
"description": "A circuit breaker implementation for PHP",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "PrestaShop SA",
"email": "contact@prestashop.com"
},
{
"name": "PrestaShop Community",
"homepage": "http://contributors.prestashop.com/"
}
],
"require": {
"php": ">=5.6",
"guzzlehttp/guzzle": "^5"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.12",
"phpunit/phpunit": "^5.7.0",
"doctrine/cache": "^1.6.0",
"symfony/cache": "^3.4.0",
"symfony/event-dispatcher": "^3.4",
"vimeo/psalm": "^1.1",
"squizlabs/php_codesniffer": "3.*"
},
"suggest": {
"symfony/cache": "Allows use of Symfony Cache adapters to store transactions",
"doctrine/cache": "Allows use of Doctrine Cache adapters to store transactions",
"ext-apcu": "Allows use of APCu adapter (performant) to store transactions",
"guzzlehttp/cache-subscriber": "Allow use of Guzzle cache (use dev-master for most recent changes)"
},
"autoload": {
"psr-4": {
"PrestaShop\\CircuitBreaker\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\PrestaShop\\CircuitBreaker\\": "tests/"
}
},
"scripts": {
"psalm": [
"@php ./vendor/bin/psalm --init",
"@php ./vendor/bin/psalm --find-dead-code --threads=8 --diff"
],
"phpcs": "@php ./vendor/bin/phpcs -p --standard=PSR2 --ignore=\"vendor/*\" \"./\" --extensions=php",
"phpcbf": "@php ./vendor/bin/phpcbf -p --standard=PSR2 --ignore=\"vendor/*\" \"./\" --extensions=php",
"cs-fix": "@php ./vendor/bin/php-cs-fixer fix",
"test": "@php ./vendor/bin/phpunit"
},
"scripts-descriptions": {
"psalm": "Execute Psalm on PHP7.0+, you need to install it",
"cs-fix": "Check and fix coding styles using PHP CS Fixer",
"test": "Launch PHPUnit test suite"
},
"config": {
"sort-packages": true,
"platform": {
"php": "5.6"
}
}
}

View File

@@ -0,0 +1,6 @@
parameters:
ignoreErrors:
# Can't use the PHPUnit extension on PHP 5.6, ignore Mocks false positive
- '#Access to an undefined property PHPUnit_Framework_MockObject_Invocation#'
- '#should return PHPUnit_Framework_MockObject_MockObject#'
- '#PHPUnit_Framework_MockObject_MockObject given#'

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit colors="true" bootstrap="./vendor/autoload.php">
<testsuites>
<testsuite name="PrestaShop Circuit Breaker Test suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<psalm
name="Psalm configuration for the PrestaShop Circuit Breaker"
totallyTyped="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config file:///home/dev/Projects/circuit-breaker/vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
<directory name="tests" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
<DeprecatedMethod errorLevel="info" />
<DeprecatedProperty errorLevel="info" />
<DeprecatedClass errorLevel="info" />
<DeprecatedConstant errorLevel="info" />
<DeprecatedInterface errorLevel="info" />
<DeprecatedTrait errorLevel="info" />
<InternalMethod errorLevel="info" />
<InternalProperty errorLevel="info" />
<InternalClass errorLevel="info" />
<MissingClosureReturnType errorLevel="info" />
<MissingReturnType errorLevel="info" />
<MissingPropertyType errorLevel="info" />
<InvalidDocblock errorLevel="info" />
<MisplacedRequiredParam errorLevel="info" />
<PropertyNotSetInConstructor errorLevel="info" />
<MissingConstructor errorLevel="info" />
<MissingClosureParamType errorLevel="info" />
<MissingParamType errorLevel="info" />
<RedundantCondition errorLevel="info" />
<DocblockTypeContradiction errorLevel="info" />
<RedundantConditionGivenDocblockType errorLevel="info" />
<UnresolvableInclude errorLevel="info" />
<RawObjectIteration errorLevel="info" />
<InvalidStringClass errorLevel="info" />
</issueHandlers>
</psalm>

View File

@@ -0,0 +1,176 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\SystemInterface;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
/**
* This implementation of the CircuitBreaker is a bit more advanced than the SimpleCircuitBreaker,
* it allows you to setup your client, system, storage and dispatcher.
*/
class AdvancedCircuitBreaker extends PartialCircuitBreaker
{
/** @var TransitionDispatcherInterface */
protected $dispatcher;
/** @var callable|null */
protected $defaultFallback;
/**
* @param SystemInterface $system
* @param ClientInterface $client
* @param StorageInterface $storage
* @param TransitionDispatcherInterface $dispatcher
*/
public function __construct(
SystemInterface $system,
ClientInterface $client,
StorageInterface $storage,
TransitionDispatcherInterface $dispatcher
) {
parent::__construct($system, $client, $storage);
$this->dispatcher = $dispatcher;
}
/**
* {@inheritdoc}
*/
public function call(
$service,
array $serviceParameters = [],
callable $fallback = null
) {
$transaction = $this->initTransaction($service);
try {
if ($this->isOpened()) {
if (!$this->canAccessService($transaction)) {
return $this->callFallback($fallback);
}
$this->moveStateTo(State::HALF_OPEN_STATE, $service);
$this->dispatchTransition(
Transition::CHECKING_AVAILABILITY_TRANSITION,
$service,
$serviceParameters
);
}
$response = $this->request($service, $serviceParameters);
$this->moveStateTo(State::CLOSED_STATE, $service);
$this->dispatchTransition(
Transition::CLOSING_TRANSITION,
$service,
$serviceParameters
);
return $response;
} catch (UnavailableServiceException $exception) {
$transaction->incrementFailures();
$this->storage->saveTransaction($service, $transaction);
if (!$this->isAllowedToRetry($transaction)) {
$this->moveStateTo(State::OPEN_STATE, $service);
$transition = $this->isHalfOpened() ? Transition::REOPENING_TRANSITION : Transition::OPENING_TRANSITION;
$this->dispatchTransition($transition, $service, $serviceParameters);
return $this->callFallback($fallback);
}
return $this->call(
$service,
$serviceParameters,
$fallback
);
}
}
/**
* @return callable|null
*/
public function getDefaultFallback()
{
return $this->defaultFallback;
}
/**
* @param callable $defaultFallback|null
*
* @return AdvancedCircuitBreaker
*/
public function setDefaultFallback(callable $defaultFallback = null)
{
$this->defaultFallback = $defaultFallback;
return $this;
}
/**
* {@inheritdoc}
*/
protected function callFallback(callable $fallback = null)
{
return parent::callFallback(null !== $fallback ? $fallback : $this->defaultFallback);
}
/**
* @param string $transition
* @param string $service
* @param array $serviceParameters
*
* @return void
*/
protected function dispatchTransition($transition, $service, array $serviceParameters)
{
$this->dispatcher->dispatchTransition($transition, $service, $serviceParameters);
}
/**
* {@inheritdoc}
*/
protected function initTransaction($service)
{
if (!$this->storage->hasTransaction($service)) {
$this->dispatchTransition(Transition::INITIATING_TRANSITION, $service, []);
}
return parent::initTransaction($service);
}
/**
* {@inheritdoc}
*/
protected function request($service, array $parameters = [])
{
$this->dispatchTransition(Transition::TRIAL_TRANSITION, $service, $parameters);
return parent::request($service, $parameters);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\FactoryInterface;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Storage\SimpleArray;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Transition\NullDispatcher;
/**
* Advanced implementation of Circuit Breaker Factory
* Used to create an AdvancedCircuitBreaker instance.
*/
final class AdvancedCircuitBreakerFactory implements FactoryInterface
{
/**
* {@inheritdoc}
*/
public function create(FactorySettingsInterface $settings)
{
$closedPlace = new ClosedPlace($settings->getFailures(), $settings->getTimeout(), 0);
$openPlace = new OpenPlace(0, 0, $settings->getThreshold());
$halfOpenPlace = new HalfOpenPlace($settings->getFailures(), $settings->getStrippedTimeout(), 0);
$system = new MainSystem($closedPlace, $halfOpenPlace, $openPlace);
/** @var ClientInterface $client */
$client = $settings->getClient() ?: new GuzzleClient($settings->getClientOptions());
/** @var StorageInterface $storage */
$storage = $settings->getStorage() ?: new SimpleArray();
/** @var TransitionDispatcherInterface $dispatcher */
$dispatcher = $settings->getDispatcher() ?: new NullDispatcher();
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$storage,
$dispatcher
);
if (null !== $settings->getDefaultFallback()) {
$circuitBreaker->setDefaultFallback($settings->getDefaultFallback());
}
return $circuitBreaker;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace PrestaShop\CircuitBreaker\Client;
use Exception;
use GuzzleHttp\Client as OriginalGuzzleClient;
use GuzzleHttp\Subscriber\Mock;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
use PrestaShop\CircuitBreaker\Exception\UnsupportedMethodException;
/**
* Guzzle implementation of client.
* The possibility of extending this client is intended.
*/
class GuzzleClient implements ClientInterface
{
/**
* @var string by default, calls are sent using GET method
*/
const DEFAULT_METHOD = 'GET';
/**
* Supported HTTP methods
*/
const SUPPORTED_METHODS = [
'GET' => true,
'HEAD' => true,
'POST' => true,
'PUT' => true,
'DELETE' => true,
'OPTIONS' => true,
];
/**
* @var array the Client default options
*/
private $defaultOptions;
public function __construct(array $defaultOptions = [])
{
$this->defaultOptions = $defaultOptions;
}
/**
* {@inheritdoc}
*
* @throws UnavailableServiceException
*/
public function request($resource, array $options)
{
try {
$options = array_merge($this->defaultOptions, $options);
$client = $this->buildClient($options);
$method = $this->getHttpMethod($options);
$options['exceptions'] = true;
// prevents unhandled method errors in Guzzle 5
unset($options['method'], $options['mock']);
$request = $client->createRequest($method, $resource, $options);
return (string) $client->send($request)->getBody();
} catch (Exception $e) {
throw new UnavailableServiceException($e->getMessage(), (int) $e->getCode(), $e);
}
}
/**
* @param array $options the list of options
*
* @return string the method
*
* @throws UnsupportedMethodException
*/
private function getHttpMethod(array $options)
{
if (isset($options['method'])) {
if (!array_key_exists($options['method'], self::SUPPORTED_METHODS)) {
throw UnsupportedMethodException::unsupportedMethod($options['method']);
}
return $options['method'];
}
return self::DEFAULT_METHOD;
}
/**
* @param array $options
*
* @return OriginalGuzzleClient
*/
private function buildClient(array $options)
{
if (isset($options['mock']) && $options['mock'] instanceof Mock) {
return $this->buildMockClient($options);
}
return new OriginalGuzzleClient($options);
}
/**
* Builds a client with a mock
*
* @param array $options
*
* @return OriginalGuzzleClient
*/
private function buildMockClient(array $options)
{
/** @var Mock $mock */
$mock = $options['mock'];
unset($options['mock']);
$client = new OriginalGuzzleClient($options);
$client->getEmitter()->attach($mock);
return $client;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* A circuit breaker is used to provide
* an alternative response when a tiers service
* is unreachable.
*/
interface CircuitBreakerInterface
{
/**
* @return string the circuit breaker state
*/
public function getState();
/**
* The function that execute the service.
*
* @param string $service the service to call
* @param array $parameters the parameters for the request
* @param callable|null $fallback if the service is unavailable, rely on the fallback
*
* @return string
*/
public function call($service, array $parameters = [], callable $fallback = null);
/**
* @return bool checks if the circuit breaker is open
*/
public function isOpened();
/**
* @return bool checks if the circuit breaker is half open
*/
public function isHalfOpened();
/**
* @return bool checks if the circuit breaker is closed
*/
public function isClosed();
}

View File

@@ -0,0 +1,18 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* In charge of calling the resource and return a response.
* Must throw UnavailableService exception if not reachable.
*/
interface ClientInterface
{
/**
* @param string $resource the URI of the service to be reached
* @param array $options the options if needed
*
* @return string
*/
public function request($resource, array $options);
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* Ease the creation of the Circuit Breaker.
*/
interface FactoryInterface
{
/**
* @param FactorySettingsInterface $settings the settings for the Place
*
* @return CircuitBreakerInterface
*/
public function create(FactorySettingsInterface $settings);
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Contract;
/**
* Interface FactorySettingsInterface contains the settings used by the Factory
*/
interface FactorySettingsInterface
{
/**
* @param FactorySettingsInterface $settingsA
* @param FactorySettingsInterface $settingsB
*
* @return FactorySettingsInterface
*/
public static function merge(FactorySettingsInterface $settingsA, FactorySettingsInterface $settingsB);
/**
* @return int
*/
public function getFailures();
/**
* @return float
*/
public function getTimeout();
/**
* @return int
*/
public function getThreshold();
/**
* @return float
*/
public function getStrippedTimeout();
/**
* @return int
*/
public function getStrippedFailures();
/**
* @return StorageInterface|null
*/
public function getStorage();
/**
* @return TransitionDispatcherInterface|null
*/
public function getDispatcher();
/**
* @return array
*/
public function getClientOptions();
/**
* @return ClientInterface|null
*/
public function getClient();
/**
* @return callable|null
*/
public function getDefaultFallback();
}

View File

@@ -0,0 +1,33 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* A circuit breaker can be in 3 places:
* closed, half open or open. Each place have its
* own properties and behaviors.
*/
interface PlaceInterface
{
/**
* Return the current state of the Circuit Breaker.
*
* @return string
*/
public function getState();
/**
* @return int the number of failures
*/
public function getFailures();
/**
* @return int the allowed number of trials
*/
public function getThreshold();
/**
* @return float the allowed timeout
*/
public function getTimeout();
}

View File

@@ -0,0 +1,49 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
/**
* Store the transaction between the Circuit Breaker
* and the tiers service.
*/
interface StorageInterface
{
/**
* Save the CircuitBreaker transaction.
*
* @param string $service The service name
* @param TransactionInterface $transaction the transaction
*
* @return bool
*/
public function saveTransaction($service, TransactionInterface $transaction);
/**
* Retrieve the CircuitBreaker transaction for a specific service.
*
* @param string $service the service name
*
* @return TransactionInterface
*
* @throws TransactionNotFoundException
*/
public function getTransaction($service);
/**
* Checks if the transaction exists.
*
* @param string $service the service name
*
* @return bool
*/
public function hasTransaction($service);
/**
* Clear the Circuit Breaker storage.
*
* @return bool
*/
public function clear();
}

View File

@@ -0,0 +1,20 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
/**
* The System define the places available
* for the Circuit Breaker and the initial Place.
*/
interface SystemInterface
{
/**
* @return PlaceInterface[] the list of places of the system
*/
public function getPlaces();
/**
* @return PlaceInterface the initial place of the system
*/
public function getInitialPlace();
}

View File

@@ -0,0 +1,40 @@
<?php
namespace PrestaShop\CircuitBreaker\Contract;
use DateTime;
/**
* Once the circuit breaker call a service,
* a transaction is initialized and stored.
*/
interface TransactionInterface
{
/**
* @return string the service name
*/
public function getService();
/**
* @return int the number of failures to call the service
*/
public function getFailures();
/**
* @return string the current state of the Circuit Breaker
*/
public function getState();
/**
* @return DateTime the time when the circuit breaker move
* from open to half open state
*/
public function getThresholdDateTime();
/**
* Everytime the service call fails, increment the number of failures.
*
* @return bool
*/
public function incrementFailures();
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Contract;
/**
* This interface is used for a circuit breaker to dispatch its state transitions.
*/
interface TransitionDispatcherInterface
{
/**
* @param string $transition the transition name
* @param string $service the Service URI
* @param array $serviceParameters the Service parameters
*
* @return void
*/
public function dispatchTransition($transition, $service, array $serviceParameters);
}

View File

@@ -0,0 +1,59 @@
<?php
namespace PrestaShop\CircuitBreaker\Event;
use Symfony\Component\EventDispatcher\Event;
class TransitionEvent extends Event
{
/**
* @var string the Transition name
*/
private $eventName;
/**
* @var string the Service URI
*/
private $service;
/**
* @var array the Service parameters
*/
private $parameters;
/**
* @param string $eventName the transition name
* @param string $service the Service URI
* @param array $parameters the Service parameters
*/
public function __construct($eventName, $service, array $parameters)
{
$this->eventName = $eventName;
$this->service = $service;
$this->parameters = $parameters;
}
/**
* @return string the Transition name
*/
public function getEvent()
{
return $this->eventName;
}
/**
* @return string the Service URI
*/
public function getService()
{
return $this->service;
}
/**
* @return array the Service parameters
*/
public function getParameters()
{
return $this->parameters;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
use Exception;
/**
* Base exception for Circuit Breaker exceptions
*/
class CircuitBreakerException extends Exception
{
}

View File

@@ -0,0 +1,26 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
use PrestaShop\CircuitBreaker\Util\ErrorFormatter;
final class InvalidPlaceException extends CircuitBreakerException
{
/**
* @param mixed $failures the failures
* @param mixed $timeout the timeout
* @param mixed $threshold the threshold
*
* @return self
*/
public static function invalidSettings($failures, $timeout, $threshold)
{
$exceptionMessage = 'Invalid settings for Place' . PHP_EOL .
ErrorFormatter::format('failures', $failures, 'isPositiveInteger', 'a positive integer') .
ErrorFormatter::format('timeout', $timeout, 'isPositiveValue', 'a float') .
ErrorFormatter::format('threshold', $threshold, 'isPositiveInteger', 'a positive integer')
;
return new self($exceptionMessage);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
use PrestaShop\CircuitBreaker\Util\ErrorFormatter;
final class InvalidTransactionException extends CircuitBreakerException
{
/**
* @param mixed $service the service URI
* @param mixed $failures the failures
* @param mixed $state the Circuit Breaker
* @param mixed $threshold the threshold
*
* @return self
*/
public static function invalidParameters($service, $failures, $state, $threshold)
{
$exceptionMessage = 'Invalid parameters for Transaction' . PHP_EOL .
ErrorFormatter::format('service', $service, 'isURI', 'an URI') .
ErrorFormatter::format('failures', $failures, 'isPositiveInteger', 'a positive integer') .
ErrorFormatter::format('state', $state, 'isString', 'a string') .
ErrorFormatter::format('threshold', $threshold, 'isPositiveInteger', 'a positive integer')
;
return new self($exceptionMessage);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
final class TransactionNotFoundException extends CircuitBreakerException
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
final class UnavailableServiceException extends CircuitBreakerException
{
}

View File

@@ -0,0 +1,19 @@
<?php
namespace PrestaShop\CircuitBreaker\Exception;
/**
* Used when trying to use an unsupported HTTP method
*/
class UnsupportedMethodException extends CircuitBreakerException
{
/**
* @param string $methodName
*
* @return UnsupportedMethodException
*/
public static function unsupportedMethod($methodName)
{
return new static(sprintf('Unsupported method: "%s"', $methodName));
}
}

View File

@@ -0,0 +1,313 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
/**
* Class FactorySettings is a simple implementation of FactorySettingsInterface, it is mainly
* a settings container and can be used with any Factory class.
*/
class FactorySettings implements FactorySettingsInterface
{
/** @var int */
private $failures;
/** @var float */
private $timeout;
/** @var int */
private $threshold;
/** @var float */
private $strippedTimeout;
/** @var int */
private $strippedFailures;
/** @var StorageInterface|null */
private $storage;
/** @var TransitionDispatcherInterface|null */
private $dispatcher;
/** @var array */
private $clientOptions = [];
/** @var ClientInterface|null */
private $client;
/** @var callable|null */
private $defaultFallback;
/**
* @param int $failures
* @param float $timeout
* @param int $threshold
*/
public function __construct(
$failures,
$timeout,
$threshold
) {
$this->failures = $this->strippedFailures = $failures;
$this->timeout = $this->strippedTimeout = $timeout;
$this->threshold = $threshold;
}
/**
* {@inheritdoc}
*/
public static function merge(FactorySettingsInterface $settingsA, FactorySettingsInterface $settingsB)
{
$mergedSettings = new FactorySettings(
$settingsB->getFailures(),
$settingsB->getTimeout(),
$settingsB->getThreshold()
);
$mergedSettings
->setStrippedFailures($settingsB->getStrippedFailures())
->setStrippedTimeout($settingsB->getStrippedTimeout())
;
$mergedSettings->setClientOptions(array_merge(
$settingsA->getClientOptions(),
$settingsB->getClientOptions()
));
if (null !== $settingsB->getClient()) {
$mergedSettings->setClient($settingsB->getClient());
} elseif (null !== $settingsA->getClient()) {
$mergedSettings->setClient($settingsA->getClient());
}
return $mergedSettings;
}
/**
* {@inheritdoc}
*/
public function getFailures()
{
return $this->failures;
}
/**
* @param int $failures
*
* @return FactorySettings
*/
public function setFailures($failures)
{
$this->failures = $failures;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTimeout()
{
return $this->timeout;
}
/**
* @param float $timeout
*
* @return FactorySettings
*/
public function setTimeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* {@inheritdoc}
*/
public function getThreshold()
{
return $this->threshold;
}
/**
* @param int $threshold
*
* @return FactorySettings
*/
public function setThreshold($threshold)
{
$this->threshold = $threshold;
return $this;
}
/**
* {@inheritdoc}
*/
public function getStrippedTimeout()
{
return $this->strippedTimeout;
}
/**
* @param float $strippedTimeout
*
* @return FactorySettings
*/
public function setStrippedTimeout($strippedTimeout)
{
$this->strippedTimeout = $strippedTimeout;
return $this;
}
/**
* @return int
*/
public function getStrippedFailures()
{
return $this->strippedFailures;
}
/**
* @param int $strippedFailures
*
* @return FactorySettings
*/
public function setStrippedFailures($strippedFailures)
{
$this->strippedFailures = $strippedFailures;
return $this;
}
/**
* {@inheritdoc}
*/
public function getStorage()
{
return $this->storage;
}
/**
* @param StorageInterface $storage
*
* @return FactorySettings
*/
public function setStorage(StorageInterface $storage)
{
$this->storage = $storage;
return $this;
}
/**
* {@inheritdoc}
*/
public function getDispatcher()
{
return $this->dispatcher;
}
/**
* @param TransitionDispatcherInterface $dispatcher
*
* @return FactorySettings
*/
public function setDispatcher(TransitionDispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
return $this;
}
/**
* {@inheritdoc}
*/
public function getClientOptions()
{
return $this->clientOptions;
}
/**
* @param array $clientOptions
*
* @return FactorySettings
*/
public function setClientOptions(array $clientOptions)
{
$this->clientOptions = $clientOptions;
return $this;
}
/**
* {@inheritdoc}
*/
public function getClient()
{
return $this->client;
}
/**
* @param ClientInterface|null $client
*
* @return FactorySettings
*/
public function setClient(ClientInterface $client = null)
{
$this->client = $client;
return $this;
}
/**
* {@inheritdoc}
*/
public function getDefaultFallback()
{
return $this->defaultFallback;
}
/**
* @param callable $defaultFallback
*
* @return FactorySettings
*/
public function setDefaultFallback(callable $defaultFallback)
{
$this->defaultFallback = $defaultFallback;
return $this;
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Transaction\SimpleTransaction;
use PrestaShop\CircuitBreaker\Contract\CircuitBreakerInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\SystemInterface;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use DateTime;
abstract class PartialCircuitBreaker implements CircuitBreakerInterface
{
/**
* @param SystemInterface $system
* @param ClientInterface $client
* @param StorageInterface $storage
*/
public function __construct(
SystemInterface $system,
ClientInterface $client,
StorageInterface $storage
) {
$this->currentPlace = $system->getInitialPlace();
$this->places = $system->getPlaces();
$this->client = $client;
$this->storage = $storage;
}
/**
* @var ClientInterface the Client that consumes the service URI
*/
protected $client;
/**
* @var PlaceInterface the current Place of the Circuit Breaker
*/
protected $currentPlace;
/**
* @var PlaceInterface[] the Circuit Breaker places
*/
protected $places = [];
/**
* @var StorageInterface the Circuit Breaker storage
*/
protected $storage;
/**
* {@inheritdoc}
*/
abstract public function call($service, array $serviceParameters = [], callable $fallback = null);
/**
* {@inheritdoc}
*/
public function getState()
{
return $this->currentPlace->getState();
}
/**
* {@inheritdoc}
*/
public function isOpened()
{
return State::OPEN_STATE === $this->currentPlace->getState();
}
/**
* {@inheritdoc}
*/
public function isHalfOpened()
{
return State::HALF_OPEN_STATE === $this->currentPlace->getState();
}
/**
* {@inheritdoc}
*/
public function isClosed()
{
return State::CLOSED_STATE === $this->currentPlace->getState();
}
/**
* @param callable|null $fallback
*
* @return string
*/
protected function callFallback(callable $fallback = null)
{
if (null === $fallback) {
return '';
}
return call_user_func($fallback);
}
/**
* @param string $state the Place state
* @param string $service the service URI
*
* @return bool
*/
protected function moveStateTo($state, $service)
{
$this->currentPlace = $this->places[$state];
$transaction = SimpleTransaction::createFromPlace(
$this->currentPlace,
$service
);
return $this->storage->saveTransaction($service, $transaction);
}
/**
* @param string $service the service URI
*
* @return TransactionInterface
*/
protected function initTransaction($service)
{
if ($this->storage->hasTransaction($service)) {
$transaction = $this->storage->getTransaction($service);
// CircuitBreaker needs to be in the same state as its last transaction
if ($this->getState() !== $transaction->getState()) {
$this->currentPlace = $this->places[$transaction->getState()];
}
} else {
$transaction = SimpleTransaction::createFromPlace(
$this->currentPlace,
$service
);
$this->storage->saveTransaction($service, $transaction);
}
return $transaction;
}
/**
* @param TransactionInterface $transaction the Transaction
*
* @return bool
*/
protected function isAllowedToRetry(TransactionInterface $transaction)
{
return $transaction->getFailures() < $this->currentPlace->getFailures();
}
/**
* @param TransactionInterface $transaction the Transaction
*
* @return bool
*/
protected function canAccessService(TransactionInterface $transaction)
{
return $transaction->getThresholdDateTime() < new DateTime();
}
/**
* Calls the client with the right information.
*
* @param string $service the service URI
* @param array $parameters the service URI parameters
*
* @return string
*/
protected function request($service, array $parameters = [])
{
return $this->client->request(
$service,
array_merge($parameters, [
'connect_timeout' => $this->currentPlace->getTimeout(),
'timeout' => $this->currentPlace->getTimeout(),
])
);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
use PrestaShop\CircuitBreaker\Util\Assert;
abstract class AbstractPlace implements PlaceInterface
{
/**
* @var int the Place failures
*/
private $failures;
/**
* @var float the Place timeout
*/
private $timeout;
/**
* @var int the Place threshold
*/
private $threshold;
/**
* @param int $failures the Place failures
* @param float $timeout the Place timeout
* @param int $threshold the Place threshold
*
* @throws InvalidPlaceException
*/
public function __construct($failures, $timeout, $threshold)
{
$this->validate($failures, $timeout, $threshold);
$this->failures = $failures;
$this->timeout = $timeout;
$this->threshold = $threshold;
}
/**
* {@inheritdoc}
*/
abstract public function getState();
/**
* {@inheritdoc}
*/
public function getFailures()
{
return $this->failures;
}
/**
* {@inheritdoc}
*/
public function getTimeout()
{
return $this->timeout;
}
/**
* {@inheritdoc}
*/
public function getThreshold()
{
return $this->threshold;
}
/**
* Ensure the place is valid (PHP5 is permissive).
*
* @param int $failures the failures should be a positive value
* @param float $timeout the timeout should be a positive value
* @param int $threshold the threshold should be a positive value
*
* @return bool true if valid
*
* @throws InvalidPlaceException
*/
private function validate($failures, $timeout, $threshold)
{
$assertionsAreValid = Assert::isPositiveInteger($failures)
&& Assert::isPositiveValue($timeout)
&& Assert::isPositiveInteger($threshold)
;
if ($assertionsAreValid) {
return true;
}
throw InvalidPlaceException::invalidSettings($failures, $timeout, $threshold);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\State;
final class ClosedPlace extends AbstractPlace
{
/**
* {@inheritdoc}
*/
public function getState()
{
return State::CLOSED_STATE;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\State;
final class HalfOpenPlace extends AbstractPlace
{
/**
* {@inheritdoc}
*/
public function getState()
{
return State::HALF_OPEN_STATE;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\State;
final class OpenPlace extends AbstractPlace
{
/**
* {@inheritdoc}
*/
public function getState()
{
return State::OPEN_STATE;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Storage\SimpleArray;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
/**
* Main implementation of Circuit Breaker.
*/
final class SimpleCircuitBreaker extends PartialCircuitBreaker
{
public function __construct(
PlaceInterface $openPlace,
PlaceInterface $halfOpenPlace,
PlaceInterface $closedPlace,
ClientInterface $client
) {
$system = new MainSystem($closedPlace, $halfOpenPlace, $openPlace);
parent::__construct($system, $client, new SimpleArray());
}
/**
* {@inheritdoc}
*/
public function call(
$service,
array $serviceParameters = [],
callable $fallback = null
) {
$transaction = $this->initTransaction($service);
try {
if ($this->isOpened()) {
if (!$this->canAccessService($transaction)) {
return $this->callFallback($fallback);
}
$this->moveStateTo(State::HALF_OPEN_STATE, $service);
}
$response = $this->request($service, $serviceParameters);
$this->moveStateTo(State::CLOSED_STATE, $service);
return $response;
} catch (UnavailableServiceException $exception) {
$transaction->incrementFailures();
$this->storage->saveTransaction($service, $transaction);
if (!$this->isAllowedToRetry($transaction)) {
$this->moveStateTo(State::OPEN_STATE, $service);
return $this->callFallback($fallback);
}
return $this->call($service, $serviceParameters, $fallback);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\FactoryInterface;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
/**
* Main implementation of Circuit Breaker Factory
* Used to create a SimpleCircuitBreaker instance.
*/
final class SimpleCircuitBreakerFactory implements FactoryInterface
{
/**
* {@inheritdoc}
*/
public function create(FactorySettingsInterface $settings)
{
$closedPlace = new ClosedPlace($settings->getFailures(), $settings->getTimeout(), 0);
$openPlace = new OpenPlace(0, 0, $settings->getThreshold());
$halfOpenPlace = new HalfOpenPlace($settings->getFailures(), $settings->getStrippedTimeout(), 0);
/** @var ClientInterface $client */
$client = $settings->getClient() ?: new GuzzleClient($settings->getClientOptions());
return new SimpleCircuitBreaker(
$openPlace,
$halfOpenPlace,
$closedPlace,
$client
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace PrestaShop\CircuitBreaker;
/**
* Define the available states of the Circuit Breaker;.
*/
final class State
{
/**
* Once opened, a circuit breaker doesn't do any call
* to third-party services. Only the alternative call is done.
*/
const OPEN_STATE = 'OPEN';
/**
* After some conditions are valid, the circuit breaker
* try to access the third-party service. If the service is valid,
* the circuit breaker go to CLOSED state. If it's not, the circuit breaker
* go to OPEN state.
*/
const HALF_OPEN_STATE = 'HALF OPEN';
/**
* On the first call of the service, or if the service is valid
* the circuit breaker is in CLOSED state. This means that the callable
* to evaluate is done and not the alternative call.
*/
const CLOSED_STATE = 'CLOSED';
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Storage;
use Doctrine\Common\Cache\CacheProvider;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
/**
* Implementation of Storage using the Doctrine Cache.
*/
class DoctrineCache implements StorageInterface
{
/** @var CacheProvider */
private $cacheProvider;
/**
* @param CacheProvider $cacheProvider
*/
public function __construct(CacheProvider $cacheProvider)
{
$this->cacheProvider = $cacheProvider;
}
/**
* {@inheritdoc}
*/
public function saveTransaction($service, TransactionInterface $transaction)
{
$key = $this->getKey($service);
return $this->cacheProvider->save($key, $transaction);
}
/**
* {@inheritdoc}
*/
public function getTransaction($service)
{
$key = $this->getKey($service);
if ($this->hasTransaction($service)) {
return $this->cacheProvider->fetch($key);
}
throw new TransactionNotFoundException();
}
/**
* {@inheritdoc}
*/
public function hasTransaction($service)
{
$key = $this->getKey($service);
return $this->cacheProvider->contains($key);
}
/**
* {@inheritdoc}
*/
public function clear()
{
return $this->cacheProvider->deleteAll();
}
/**
* Helper method to properly store the transaction.
*
* @param string $service the service URI
*
* @return string the transaction unique identifier
*/
private function getKey($service)
{
return md5($service);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace PrestaShop\CircuitBreaker\Storage;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
/**
* Very simple implementation of Storage using a simple PHP array.
*/
final class SimpleArray implements StorageInterface
{
/**
* @var array the circuit breaker transactions
*/
public static $transactions = [];
/**
* {@inheritdoc}
*/
public function saveTransaction($service, TransactionInterface $transaction)
{
$key = $this->getKey($service);
self::$transactions[$key] = $transaction;
return true;
}
/**
* {@inheritdoc}
*/
public function getTransaction($service)
{
$key = $this->getKey($service);
if ($this->hasTransaction($service)) {
return self::$transactions[$key];
}
throw new TransactionNotFoundException();
}
/**
* {@inheritdoc}
*/
public function hasTransaction($service)
{
$key = $this->getKey($service);
return array_key_exists($key, self::$transactions);
}
/**
* {@inheritdoc}
*/
public function clear()
{
self::$transactions = [];
return true;
}
/**
* Helper method to properly store the transaction.
*
* @param string $service the service URI
*
* @return string the transaction unique identifier
*/
private function getKey($service)
{
return md5($service);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace PrestaShop\CircuitBreaker\Storage;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
use Psr\SimpleCache\CacheInterface;
/**
* Implementation of Storage using the Symfony Cache Component.
*/
final class SymfonyCache implements StorageInterface
{
/**
* @var CacheInterface the Symfony Cache
*/
private $symfonyCache;
public function __construct(CacheInterface $symfonyCache)
{
$this->symfonyCache = $symfonyCache;
}
/**
* {@inheritdoc}
*/
public function saveTransaction($service, TransactionInterface $transaction)
{
$key = $this->getKey($service);
return $this->symfonyCache->set($key, $transaction);
}
/**
* {@inheritdoc}
*/
public function getTransaction($service)
{
$key = $this->getKey($service);
if ($this->hasTransaction($service)) {
return $this->symfonyCache->get($key);
}
throw new TransactionNotFoundException();
}
/**
* {@inheritdoc}
*/
public function hasTransaction($service)
{
$key = $this->getKey($service);
return $this->symfonyCache->has($key);
}
/**
* {@inheritdoc}
*/
public function clear()
{
return $this->symfonyCache->clear();
}
/**
* Helper method to properly store the transaction.
*
* @param string $service the service URI
*
* @return string the transaction unique identifier
*/
private function getKey($service)
{
return md5($service);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\Contract\ClientInterface;
use PrestaShop\CircuitBreaker\Contract\SystemInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Transition\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Symfony implementation of Circuit Breaker.
*/
final class SymfonyCircuitBreaker extends AdvancedCircuitBreaker
{
/**
* @var EventDispatcherInterface the Symfony Event Dispatcher
*/
private $eventDispatcher;
/**
* @param SystemInterface $system
* @param ClientInterface $client
* @param StorageInterface $storage
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(
SystemInterface $system,
ClientInterface $client,
StorageInterface $storage,
EventDispatcherInterface $eventDispatcher
) {
$this->eventDispatcher = $eventDispatcher;
parent::__construct($system, $client, $storage, new EventDispatcher($eventDispatcher));
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace PrestaShop\CircuitBreaker\System;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Contract\SystemInterface;
use PrestaShop\CircuitBreaker\State;
/**
* Implement the system described by the documentation.
* The main system is built with 3 places:
* - A Closed place
* - A Half Open Place
* - An Open Place
*/
final class MainSystem implements SystemInterface
{
/**
* @var PlaceInterface[]
*/
private $places;
public function __construct(
PlaceInterface $closedPlace,
PlaceInterface $halfOpenPlace,
PlaceInterface $openPlace
) {
$this->places = [
$closedPlace->getState() => $closedPlace,
$halfOpenPlace->getState() => $halfOpenPlace,
$openPlace->getState() => $openPlace,
];
}
/**
* {@inheritdoc}
*/
public function getInitialPlace()
{
return $this->places[State::CLOSED_STATE];
}
/**
* {@inheritdoc}
*/
public function getPlaces()
{
return $this->places;
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace PrestaShop\CircuitBreaker\Transaction;
use DateTime;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\InvalidTransactionException;
use PrestaShop\CircuitBreaker\Util\Assert;
/**
* Main implementation of Circuit Breaker transaction.
*/
final class SimpleTransaction implements TransactionInterface
{
/**
* @var string the URI of the service
*/
private $service;
/**
* @var int the failures when we call the service
*/
private $failures;
/**
* @var string the Circuit Breaker state
*/
private $state;
/**
* @var DateTime the Transaction threshold datetime
*/
private $thresholdDateTime;
/**
* @param string $service the service URI
* @param int $failures the allowed failures
* @param string $state the circuit breaker state/place
* @param int $threshold the place threshold
*/
public function __construct($service, $failures, $state, $threshold)
{
$this->validate($service, $failures, $state, $threshold);
$this->service = $service;
$this->failures = $failures;
$this->state = $state;
$this->initThresholdDateTime($threshold);
}
/**
* {@inheritdoc}
*/
public function getService()
{
return $this->service;
}
/**
* {@inheritdoc}
*/
public function getFailures()
{
return $this->failures;
}
/**
* {@inheritdoc}
*/
public function getState()
{
return $this->state;
}
/**
* {@inheritdoc}
*/
public function getThresholdDateTime()
{
return $this->thresholdDateTime;
}
/**
* {@inheritdoc}
*/
public function incrementFailures()
{
++$this->failures;
return true;
}
/**
* Helper to create a transaction from the Place.
*
* @param PlaceInterface $place the Circuit Breaker place
* @param string $service the service URI
*
* @return self
*/
public static function createFromPlace(PlaceInterface $place, $service)
{
$threshold = $place->getThreshold();
return new self(
$service,
0,
$place->getState(),
$threshold
);
}
/**
* Set the right DateTime from the threshold value.
*
* @param int $threshold the Transaction threshold
*
* @return void
*/
private function initThresholdDateTime($threshold)
{
$thresholdDateTime = new DateTime();
$thresholdDateTime->modify("+$threshold second");
$this->thresholdDateTime = $thresholdDateTime;
}
/**
* Ensure the transaction is valid (PHP5 is permissive).
*
* @param string $service the service URI
* @param int $failures the failures should be a positive value
* @param string $state the Circuit Breaker state
* @param int $threshold the threshold should be a positive value
*
* @return bool true if valid
*
* @throws InvalidTransactionException
*/
private function validate($service, $failures, $state, $threshold)
{
$assertionsAreValid = Assert::isURI($service)
&& Assert::isPositiveInteger($failures)
&& Assert::isString($state)
&& Assert::isPositiveInteger($threshold)
;
if ($assertionsAreValid) {
return true;
}
throw InvalidTransactionException::invalidParameters($service, $failures, $state, $threshold);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace PrestaShop\CircuitBreaker;
/**
* Define the available transitions of the Circuit Breaker;.
*/
final class Transition
{
/**
* Happened only once when calling the Circuit Breaker.
*/
const INITIATING_TRANSITION = 'INITIATING';
/**
* Happened when we open the Circuit Breaker.
* This means once the Circuit Breaker is in failure.
*/
const OPENING_TRANSITION = 'OPENING';
/**
* Happened once the conditions of retry are met
* in OPEN state to move to HALF_OPEN state in the
* Circuit Breaker.
*/
const CHECKING_AVAILABILITY_TRANSITION = 'CHECKING AVAILABILITY';
/**
* Happened when we come back to OPEN state
* in the Circuit Breaker from the HALF_OPEN state.
*/
const REOPENING_TRANSITION = 'REOPENING';
/**
* Happened if the service is available again.
*/
const CLOSING_TRANSITION = 'CLOSING';
/**
* Happened on each try to call the service.
*/
const TRIAL_TRANSITION = 'TRIAL';
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Transition;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\Event\TransitionEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Class EventDispatcher implements the TransitionDispatcher using the Symfony EventDispatcherInterface
*/
class EventDispatcher implements TransitionDispatcherInterface
{
/**
* @var EventDispatcherInterface the Symfony Event Dispatcher
*/
private $eventDispatcher;
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* {@inheritdoc}
*/
public function dispatchTransition($transition, $service, array $serviceParameters)
{
$event = new TransitionEvent($transition, $service, $serviceParameters);
$this->eventDispatcher->dispatch($transition, $event);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\CircuitBreaker\Transition;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
/**
* Class NullDispatcher is used when you have no TransitionDispatcher to inject
* because you don't need it.
*/
class NullDispatcher implements TransitionDispatcherInterface
{
/**
* {@inheritdoc}
*/
public function dispatchTransition($transition, $service, array $serviceParameters)
{
// Simply does nothing
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace PrestaShop\CircuitBreaker\Util;
/**
* Util class to handle object validation
* Should be deprecated for most parts once
* the library will drop PHP5 support.
*/
final class Assert
{
/**
* @param mixed $value the value to evaluate
*
* @return bool
*/
public static function isPositiveValue($value)
{
return !is_string($value) && is_numeric($value) && $value >= 0;
}
/**
* @param mixed $value the value to evaluate
*
* @return bool
*/
public static function isPositiveInteger($value)
{
return self::isPositiveValue($value) && is_int($value);
}
/**
* @param mixed $value the value to evaluate
*
* @return bool
*/
public static function isURI($value)
{
return null !== $value
&& !is_numeric($value)
&& !is_bool($value)
&& false !== filter_var($value, FILTER_SANITIZE_URL)
;
}
/**
* @param mixed $value the value to evaluate
*
* @return bool
*/
public static function isString($value)
{
return !empty($value) && is_string($value);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace PrestaShop\CircuitBreaker\Util;
/**
* Helper to provide complete and easy to read
* error messages.
* Mostly used to build Exception messages.
*/
final class ErrorFormatter
{
/**
* Format error message.
*
* @param string $parameter the parameter to evaluate
* @param mixed $value the value to format
* @param string $function the validation function
* @param string $expectedType the expected type
*
* @return string
*/
public static function format($parameter, $value, $function, $expectedType)
{
$errorMessage = '';
$isValid = Assert::$function($value);
$type = gettype($value);
$hasStringValue = in_array($type, ['integer', 'float', 'string'], true);
if (!$isValid) {
$errorMessage = sprintf(
'Excepted %s to be %s, got %s',
$parameter,
$expectedType,
$type
);
if ($hasStringValue) {
$errorMessage .= sprintf(' (%s)', (string) $value);
}
$errorMessage .= PHP_EOL;
}
return $errorMessage;
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace Tests\PrestaShop\CircuitBreaker;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreaker;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreakerFactory;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\FactorySettings;
use PrestaShop\CircuitBreaker\State;
use PrestaShop\CircuitBreaker\Transition;
class AdvancedCircuitBreakerFactoryTest extends TestCase
{
/**
* @dataProvider getSettings
*
* @param FactorySettingsInterface $settings the Circuit Breaker settings
*
* @return void
*/
public function testCircuitBreakerCreation(FactorySettingsInterface $settings)
{
$factory = new AdvancedCircuitBreakerFactory();
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(AdvancedCircuitBreaker::class, $circuitBreaker);
}
public function testCircuitBreakerWithDispatcher()
{
$dispatcher = $this->getMockBuilder(TransitionDispatcherInterface::class)
->disableOriginalConstructor()
->getMock()
;
$localeService = 'file://' . __FILE__;
$expectedParameters = ['toto' => 'titi', 42 => 51];
$dispatcher
->expects($this->at(0))
->method('dispatchTransition')
->with(
$this->equalTo(Transition::INITIATING_TRANSITION),
$this->equalTo($localeService),
$this->equalTo([])
)
;
$dispatcher
->expects($this->at(1))
->method('dispatchTransition')
->with(
$this->equalTo(Transition::TRIAL_TRANSITION),
$this->equalTo($localeService),
$this->equalTo($expectedParameters)
)
;
$factory = new AdvancedCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 10);
$settings
->setStrippedTimeout(0.2)
->setDispatcher($dispatcher)
;
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(AdvancedCircuitBreaker::class, $circuitBreaker);
$circuitBreaker->call($localeService, $expectedParameters, function () {
});
}
public function testCircuitBreakerWithStorage()
{
$storage = $this->getMockBuilder(StorageInterface::class)
->disableOriginalConstructor()
->getMock()
;
$factory = new AdvancedCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 10);
$settings
->setStrippedTimeout(0.2)
->setStorage($storage)
;
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(AdvancedCircuitBreaker::class, $circuitBreaker);
}
public function testCircuitBreakerWithDefaultFallback()
{
$factory = new AdvancedCircuitBreakerFactory();
$settings = new FactorySettings(2, 0.1, 10);
$settings->setDefaultFallback(function () {
return 'default_fallback';
});
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(AdvancedCircuitBreaker::class, $circuitBreaker);
$response = $circuitBreaker->call('unknown_service');
$this->assertEquals(State::OPEN_STATE, $circuitBreaker->getState());
$this->assertEquals('default_fallback', $response);
}
/**
* @return array
*/
public function getSettings()
{
return [
[
(new FactorySettings(2, 0.1, 10))
->setStrippedTimeout(0.2)
->setClientOptions(['proxy' => '192.168.16.1:10']),
],
[
(new FactorySettings(2, 0.1, 10))
->setStrippedTimeout(0.2)
->setClient(new GuzzleClient(['proxy' => '192.168.16.1:10'])),
],
];
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace Tests\PrestaShop\CircuitBreaker;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\Request;
use PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Subscriber\Mock;
use PHPUnit_Framework_MockObject_MockObject;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreaker;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Contract\TransitionDispatcherInterface;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\State;
use PrestaShop\CircuitBreaker\Storage\SymfonyCache;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Transition\NullDispatcher;
use Symfony\Component\Cache\Simple\ArrayCache;
class AdvancedCircuitBreakerTest extends CircuitBreakerTestCase
{
/**
* Used to track the dispatched events.
*
* @var PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount
*/
private $spy;
/**
* We should see the circuit breaker initialized,
* a call being done and then the circuit breaker closed.
*/
public function testCircuitBreakerEventsOnFirstFailedCall()
{
$circuitBreaker = $this->createCircuitBreaker();
$circuitBreaker->call(
'https://httpbin.org/get/foo',
['toto' => 'titi'],
function () {
return '{}';
}
);
/**
* The circuit breaker is initiated
* the 2 failed trials are done
* then the conditions are met to open the circuit breaker
*/
$invocations = $this->spy->getInvocations();
$this->assertCount(4, $invocations);
$this->assertSame('INITIATING', $invocations[0]->parameters[0]);
$this->assertSame('TRIAL', $invocations[1]->parameters[0]);
$this->assertSame('TRIAL', $invocations[2]->parameters[0]);
$this->assertSame('OPENING', $invocations[3]->parameters[0]);
}
public function testSimpleCall()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new Response(200, [], Stream::factory('{"hello": "world"}')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$this->assertEquals(0, $mock->count());
$this->assertEquals('{"hello": "world"}', $response);
}
public function testOpenStateAfterTooManyFailures()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(0, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
}
public function testNoFallback()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything');
$this->assertEquals(0, $mock->count());
$this->assertEquals('', $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
}
public function testBackToClosedStateAfterSuccess()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
new Response(200, [], Stream::factory('{"hello": "world"}')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(1, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
//Stay in OPEN state
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(1, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
sleep(2);
//Switch to CLOSED state on success
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(0, $mock->count());
$this->assertEquals('{"hello": "world"}', $response);
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
}
public function testStayInOpenStateAfterFailure()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
]);
$client = new GuzzleClient(['mock' => $mock]);
$circuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$symfonyCache,
new NullDispatcher()
);
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(1, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
//Stay in OPEN state
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(1, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
sleep(2);
//Switch to OPEN state on failure
$response = $circuitBreaker->call('anything', [], function () {
return false;
});
$this->assertEquals(0, $mock->count());
$this->assertEquals(false, $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
}
/**
* @return AdvancedCircuitBreaker the circuit breaker for testing purposes
*/
private function createCircuitBreaker()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
/** @var PHPUnit_Framework_MockObject_MockObject|TransitionDispatcherInterface $dispatcher */
$dispatcher = $this->createMock(TransitionDispatcherInterface::class);
$dispatcher->expects($this->spy = $this->any())
->method('dispatchTransition')
;
return new AdvancedCircuitBreaker(
$system,
$this->getTestClient(),
$symfonyCache,
$dispatcher
);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Subscriber\Mock;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use GuzzleHttp\Exception\RequestException;
use PHPUnit\Framework\TestCase;
/**
* Helper to get a fake Guzzle client.
*/
abstract class CircuitBreakerTestCase extends TestCase
{
/**
* Returns an instance of Client able to emulate
* available and not available services.
*
* @return GuzzleClient
*/
protected function getTestClient()
{
$mock = new Mock([
new RequestException('Service unavailable', new Request('GET', 'test')),
new RequestException('Service unavailable', new Request('GET', 'test')),
new Response(200, [], Stream::factory('{"hello": "world"}')),
]);
return new GuzzleClient(['mock' => $mock]);
}
}

View File

@@ -0,0 +1,263 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker;
use PrestaShop\CircuitBreaker\AdvancedCircuitBreaker;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Contract\CircuitBreakerInterface;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
use PrestaShop\CircuitBreaker\State;
use PrestaShop\CircuitBreaker\Storage\SimpleArray;
use PrestaShop\CircuitBreaker\Transition\NullDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcher;
use PrestaShop\CircuitBreaker\Storage\SymfonyCache;
use PrestaShop\CircuitBreaker\SymfonyCircuitBreaker;
use PrestaShop\CircuitBreaker\SimpleCircuitBreaker;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use Symfony\Component\Cache\Simple\ArrayCache;
class CircuitBreakerWorkflowTest extends CircuitBreakerTestCase
{
const OPEN_THRESHOLD = 1;
/**
* {@inheritdoc}
*/
protected function setUp()
{
parent::setUp();
//For SimpleCircuitBreaker tests we need to clear the storage cache because it is stored in a static variable
$storage = new SimpleArray();
$storage->clear();
}
/**
* When we use the circuit breaker on unreachable service
* the fallback response is used.
*
* @dataProvider getCircuitBreakers
*
* @param CircuitBreakerInterface $circuitBreaker
*/
public function testCircuitBreakerIsInClosedStateAtStart($circuitBreaker)
{
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
}
/**
* Once the number of failures is reached, the circuit breaker
* is open. This time no calls to the services are done.
*
* @dataProvider getCircuitBreakers
*
* @param CircuitBreakerInterface $circuitBreaker
*/
public function testCircuitBreakerWillBeOpenInCaseOfFailures($circuitBreaker)
{
// CLOSED
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$response = $circuitBreaker->call('https://httpbin.org/get/foo', [], $this->createFallbackResponse());
$this->assertSame('{}', $response);
//After two failed calls switch to OPEN state
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
$this->assertSame(
'{}',
$circuitBreaker->call(
'https://httpbin.org/get/foo',
[],
$this->createFallbackResponse()
)
);
}
/**
* Once the number of failures is reached, the circuit breaker
* is open. This time no calls to the services are done.
*
* @dataProvider getCircuitBreakers
*
* @param CircuitBreakerInterface $circuitBreaker
*/
public function testCircuitBreakerWillBeOpenWithoutFallback($circuitBreaker)
{
// CLOSED
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$response = $circuitBreaker->call('https://httpbin.org/get/foo');
$this->assertSame('', $response);
//After two failed calls switch to OPEN state
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
$this->assertSame(
'{}',
$circuitBreaker->call(
'https://httpbin.org/get/foo',
[],
$this->createFallbackResponse()
)
);
}
/**
* In HalfOpen state, if the service is back we can
* close the CircuitBreaker.
*
* @dataProvider getCircuitBreakers
*
* @param CircuitBreakerInterface $circuitBreaker
*/
public function testOnceInHalfOpenModeServiceIsFinallyReachable($circuitBreaker)
{
// CLOSED - first call fails (twice)
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$response = $circuitBreaker->call('https://httpbin.org/get/foo', [], $this->createFallbackResponse());
$this->assertSame('{}', $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
// OPEN - no call to client
$response = $circuitBreaker->call('https://httpbin.org/get/foo', [], $this->createFallbackResponse());
$this->assertSame('{}', $response);
$this->assertSame(State::OPEN_STATE, $circuitBreaker->getState());
sleep(2 * self::OPEN_THRESHOLD);
// SWITCH TO HALF OPEN - retry to call the service
$this->assertSame(
'{"hello": "world"}',
$circuitBreaker->call(
'https://httpbin.org/get/foo',
[],
$this->createFallbackResponse()
)
);
$this->assertSame(State::CLOSED_STATE, $circuitBreaker->getState());
$this->assertTrue($circuitBreaker->isClosed());
}
/**
* This is not useful for SimpleCircuitBreaker since it has a SimpleArray storage
*/
public function testRememberLastTransactionState()
{
$system = new MainSystem(
new ClosedPlace(1, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$storage = new SymfonyCache(new ArrayCache());
$client = $this->createMock(GuzzleClient::class);
$client
->expects($this->once())
->method('request')
->willThrowException(new UnavailableServiceException())
;
$firstCircuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$storage,
new NullDispatcher()
);
$this->assertEquals(State::CLOSED_STATE, $firstCircuitBreaker->getState());
$firstCircuitBreaker->call('fake_service', [], function () {
return false;
});
$this->assertEquals(State::OPEN_STATE, $firstCircuitBreaker->getState());
$this->assertTrue($storage->hasTransaction('fake_service'));
$secondCircuitBreaker = new AdvancedCircuitBreaker(
$system,
$client,
$storage,
new NullDispatcher()
);
$this->assertEquals(State::CLOSED_STATE, $secondCircuitBreaker->getState());
$secondCircuitBreaker->call('fake_service', [], function () {
return false;
});
$this->assertEquals(State::OPEN_STATE, $secondCircuitBreaker->getState());
}
/**
* Return the list of supported circuit breakers
*
* @return array
*/
public function getCircuitBreakers()
{
return [
'simple' => [$this->createSimpleCircuitBreaker()],
'symfony' => [$this->createSymfonyCircuitBreaker()],
'advanced' => [$this->createAdvancedCircuitBreaker()],
];
}
/**
* @return SimpleCircuitBreaker the circuit breaker for testing purposes
*/
private function createSimpleCircuitBreaker()
{
return new SimpleCircuitBreaker(
new OpenPlace(0, 0, self::OPEN_THRESHOLD), // threshold 1s
new HalfOpenPlace(0, 0.2, 0), // timeout 0.2s to test the service
new ClosedPlace(2, 0.2, 0), // 2 failures allowed, 0.2s timeout
$this->getTestClient()
);
}
/**
* @return AdvancedCircuitBreaker the circuit breaker for testing purposes
*/
private function createAdvancedCircuitBreaker()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, self::OPEN_THRESHOLD)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
return new AdvancedCircuitBreaker(
$system,
$this->getTestClient(),
$symfonyCache,
new NullDispatcher()
);
}
/**
* @return SymfonyCircuitBreaker the circuit breaker for testing purposes
*/
private function createSymfonyCircuitBreaker()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, self::OPEN_THRESHOLD)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$eventDispatcherS = $this->createMock(EventDispatcher::class);
return new SymfonyCircuitBreaker(
$system,
$this->getTestClient(),
$symfonyCache,
$eventDispatcherS
);
}
/**
* @return callable the fallback callable
*/
private function createFallbackResponse()
{
return function () {
return '{}';
};
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Client;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Client\GuzzleClient;
use PrestaShop\CircuitBreaker\Exception\UnavailableServiceException;
class GuzzleClientTest extends TestCase
{
public function testRequestWorksAsExpected()
{
$client = new GuzzleClient();
$this->assertNotNull($client->request('https://www.google.com', [
'method' => 'GET',
]));
}
public function testWrongRequestThrowsAnException()
{
$this->expectException(UnavailableServiceException::class);
$client = new GuzzleClient();
$client->request('http://not-even-a-valid-domain.xxx', []);
}
public function testTheClientAcceptsHttpMethodOverride()
{
$client = new GuzzleClient([
'method' => 'HEAD',
]);
$this->assertEmpty($client->request('https://www.google.fr', []));
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Event;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Event\TransitionEvent;
class TransitionEventTest extends TestCase
{
public function testCreation()
{
$event = new TransitionEvent('foo', 'bar', []);
$this->assertInstanceOf(TransitionEvent::class, $event);
}
/**
* @depends testCreation
*/
public function testGetService()
{
$event = new TransitionEvent('eventName', 'service', []);
$this->assertSame('service', $event->getService());
}
/**
* @depends testCreation
*/
public function testGetEvent()
{
$event = new TransitionEvent('eventName', 'service', []);
$this->assertSame('eventName', $event->getEvent());
}
/**
* @depends testCreation
*/
public function testGetParameters()
{
$parameters = [
'foo' => 'myFoo',
'bar' => true,
];
$event = new TransitionEvent('eventName', 'service', $parameters);
$this->assertSame($parameters, $event->getParameters());
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Exception;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
class InvalidPlaceTest extends TestCase
{
public function testCreation()
{
$invalidPlace = new InvalidPlaceException();
$this->assertInstanceOf(InvalidPlaceException::class, $invalidPlace);
}
/**
* @dataProvider getSettings
*
* @param array $settings
* @param string $expectedExceptionMessage
*/
public function testInvalidSettings($settings, $expectedExceptionMessage)
{
$invalidPlace = InvalidPlaceException::invalidSettings(
$settings[0], // failures
$settings[1], // timeout
$settings[2] // threshold
);
$this->assertSame($invalidPlace->getMessage(), $expectedExceptionMessage);
}
/**
* @return array
*/
public function getSettings()
{
return [
'all_invalid_settings' => [
['0', '1', null],
'Invalid settings for Place' . PHP_EOL .
'Excepted failures to be a positive integer, got string (0)' . PHP_EOL .
'Excepted timeout to be a float, got string (1)' . PHP_EOL .
'Excepted threshold to be a positive integer, got NULL' . PHP_EOL,
],
'2_invalid_settings' => [
[0, '1', null],
'Invalid settings for Place' . PHP_EOL .
'Excepted timeout to be a float, got string (1)' . PHP_EOL .
'Excepted threshold to be a positive integer, got NULL' . PHP_EOL,
],
'1_invalid_settings' => [
[0, '1', 2],
'Invalid settings for Place' . PHP_EOL .
'Excepted timeout to be a float, got string (1)' . PHP_EOL,
],
'all_valid_settings' => [
[0, 1.1, 2],
'Invalid settings for Place' . PHP_EOL,
],
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Exception;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Exception\InvalidTransactionException;
class InvalidTransactionTest extends TestCase
{
public function testCreation()
{
$invalidPlace = new InvalidTransactionException();
$this->assertInstanceOf(InvalidTransactionException::class, $invalidPlace);
}
/**
* @dataProvider getParameters
*
* @param array $parameters
* @param string $expectedExceptionMessage
*/
public function testInvalidParameters($parameters, $expectedExceptionMessage)
{
$invalidPlace = InvalidTransactionException::invalidParameters(
$parameters[0], // service
$parameters[1], // failures
$parameters[2], // state
$parameters[3] // threshold
);
$this->assertSame($invalidPlace->getMessage(), $expectedExceptionMessage);
}
/**
* @return array
*/
public function getParameters()
{
return [
'all_invalid_parameters' => [
[100, '0', null, 'toto'],
'Invalid parameters for Transaction' . PHP_EOL .
'Excepted service to be an URI, got integer (100)' . PHP_EOL .
'Excepted failures to be a positive integer, got string (0)' . PHP_EOL .
'Excepted state to be a string, got NULL' . PHP_EOL .
'Excepted threshold to be a positive integer, got string (toto)' . PHP_EOL,
],
'3_invalid_parameters' => [
['http://www.prestashop.com', '1', null, 'toto'],
'Invalid parameters for Transaction' . PHP_EOL .
'Excepted failures to be a positive integer, got string (1)' . PHP_EOL .
'Excepted state to be a string, got NULL' . PHP_EOL .
'Excepted threshold to be a positive integer, got string (toto)' . PHP_EOL,
],
'2_invalid_parameters' => [
['http://www.prestashop.com', 10, null, null],
'Invalid parameters for Transaction' . PHP_EOL .
'Excepted state to be a string, got NULL' . PHP_EOL .
'Excepted threshold to be a positive integer, got NULL' . PHP_EOL,
],
'none_invalid' => [
['http://www.prestashop.com', 10, 'CLOSED_STATE', 1],
'Invalid parameters for Transaction' . PHP_EOL,
],
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* 2007-2019 PrestaShop SA and Contributors
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace Tests\PrestaShop\CircuitBreaker;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\FactorySettings;
class FactorySettingsTest extends TestCase
{
public function testSimpleSettings()
{
$settings = new FactorySettings(2, 0.5, 10);
$this->assertNotNull($settings);
$this->assertEquals(2, $settings->getFailures());
$this->assertEquals(0.5, $settings->getTimeout());
$this->assertEquals(10, $settings->getThreshold());
$this->assertEquals(2, $settings->getStrippedFailures());
$this->assertEquals(0.5, $settings->getStrippedTimeout());
}
public function testMergeSettings()
{
$defaultSettings = new FactorySettings(2, 0.5, 10);
$defaultSettings
->setStrippedTimeout(1.2)
->setStrippedFailures(1)
;
$this->assertEquals(2, $defaultSettings->getFailures());
$this->assertEquals(0.5, $defaultSettings->getTimeout());
$this->assertEquals(10, $defaultSettings->getThreshold());
$this->assertEquals(1, $defaultSettings->getStrippedFailures());
$this->assertEquals(1.2, $defaultSettings->getStrippedTimeout());
$settings = new FactorySettings(2, 1.5, 20);
$mergedSettings = FactorySettings::merge($defaultSettings, $settings);
$this->assertEquals(2, $mergedSettings->getFailures());
$this->assertEquals(1.5, $mergedSettings->getTimeout());
$this->assertEquals(20, $mergedSettings->getThreshold());
$this->assertEquals(2, $mergedSettings->getStrippedFailures());
$this->assertEquals(1.5, $mergedSettings->getStrippedTimeout());
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\State;
class ClosedPlaceTest extends PlaceTestCase
{
/**
* @dataProvider getFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWith($failures, $timeout, $threshold)
{
$closedPlace = new ClosedPlace($failures, $timeout, $threshold);
$this->assertSame($failures, $closedPlace->getFailures());
$this->assertSame($timeout, $closedPlace->getTimeout());
$this->assertSame($threshold, $closedPlace->getThreshold());
}
/**
* @dataProvider getInvalidFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWithInvalidValues($failures, $timeout, $threshold)
{
$this->expectException(InvalidPlaceException::class);
new ClosedPlace($failures, $timeout, $threshold);
}
public function testGetExpectedState()
{
$closedPlace = new ClosedPlace(1, 1, 1);
$this->assertSame(State::CLOSED_STATE, $closedPlace->getState());
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\State;
class HalfOpenPlaceTest extends PlaceTestCase
{
/**
* @dataProvider getFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWith($failures, $timeout, $threshold)
{
$halfOpenPlace = new HalfOpenPlace($failures, $timeout, $threshold);
$this->assertSame($failures, $halfOpenPlace->getFailures());
$this->assertSame($timeout, $halfOpenPlace->getTimeout());
$this->assertSame($threshold, $halfOpenPlace->getThreshold());
}
/**
* @dataProvider getInvalidFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWithInvalidValues($failures, $timeout, $threshold)
{
$this->expectException(InvalidPlaceException::class);
new HalfOpenPlace($failures, $timeout, $threshold);
}
public function testGetExpectedState()
{
$halfOpenPlace = new HalfOpenPlace(1, 1, 1);
$this->assertSame(State::HALF_OPEN_STATE, $halfOpenPlace->getState());
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Place;
use PrestaShop\CircuitBreaker\Exception\InvalidPlaceException;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\State;
class OpenPlaceTest extends PlaceTestCase
{
/**
* @dataProvider getFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWith($failures, $timeout, $threshold)
{
$openPlace = new OpenPlace($failures, $timeout, $threshold);
$this->assertSame($failures, $openPlace->getFailures());
$this->assertSame($timeout, $openPlace->getTimeout());
$this->assertSame($threshold, $openPlace->getThreshold());
}
/**
* @dataProvider getInvalidFixtures
*
* @param mixed $failures
* @param mixed $timeout
* @param mixed $threshold
*/
public function testCreationWithInvalidValues($failures, $timeout, $threshold)
{
$this->expectException(InvalidPlaceException::class);
new OpenPlace($failures, $timeout, $threshold);
}
public function testGetExpectedState()
{
$openPlace = new OpenPlace(1, 1, 1);
$this->assertSame(State::OPEN_STATE, $openPlace->getState());
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Place;
use PHPUnit\Framework\TestCase;
/**
* Helper to share fixtures accross Place tests.
*/
class PlaceTestCase extends TestCase
{
/**
* @return array
*/
public function getFixtures()
{
return [
'0_0_0' => [0, 0, 0],
'1_100_0' => [1, 100, 0],
'3_0.6_3' => [3, 0.6, 3],
];
}
/**
* @return array
*/
public function getArrayFixtures()
{
return [
'assoc_array' => [[
'timeout' => 3,
'threshold' => 2,
'failures' => 1,
]],
];
}
/**
* @return array
*/
public function getInvalidFixtures()
{
return [
'minus1_null_false' => [-1, null, false],
'3_0.6_3.14' => [3, 0.6, 3.14],
];
}
/**
* @return array
*/
public function getInvalidArrayFixtures()
{
return [
'invalid_indexes' => [[
0 => 3,
1 => 2,
4 => 1,
]],
'invalid_keys' => [[
'timeout' => 3,
'max_wait' => 2,
'failures' => 1,
]],
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Contract\FactorySettingsInterface;
use PrestaShop\CircuitBreaker\FactorySettings;
use PrestaShop\CircuitBreaker\SimpleCircuitBreaker;
use PrestaShop\CircuitBreaker\SimpleCircuitBreakerFactory;
class SimpleCircuitBreakerFactoryTest extends TestCase
{
/**
* @return void
*/
public function testCreation()
{
$factory = new SimpleCircuitBreakerFactory();
$this->assertInstanceOf(SimpleCircuitBreakerFactory::class, $factory);
}
/**
* @depends testCreation
* @dataProvider getSettings
*
* @param FactorySettingsInterface $settings the Circuit Breaker settings
*
* @return void
*/
public function testCircuitBreakerCreation(FactorySettingsInterface $settings)
{
$factory = new SimpleCircuitBreakerFactory();
$circuitBreaker = $factory->create($settings);
$this->assertInstanceOf(SimpleCircuitBreaker::class, $circuitBreaker);
}
/**
* @return array
*/
public function getSettings()
{
return [
[
(new FactorySettings(2, 0.1, 10))
->setStrippedTimeout(0.2)
->setStrippedFailures(1),
],
[
(new FactorySettings(2, 0.1, 10))
->setStrippedTimeout(0.2)
->setStrippedFailures(1)
->setClientOptions(['proxy' => '192.168.16.1:10']),
],
];
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Storage;
use Doctrine\Common\Cache\FilesystemCache;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Contract\StorageInterface;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
use PrestaShop\CircuitBreaker\Storage\DoctrineCache;
class DoctrineCacheTest extends TestCase
{
/**
* @var StorageInterface the Doctrine Cache storage
*/
private $doctrineCache;
/**
* {@inheritdoc}
*/
protected function setUp()
{
$this->doctrineCache = new DoctrineCache(
new FilesystemCache(sys_get_temp_dir() . '/ps__circuit_breaker')
);
}
/**
* {@inheritdoc}
*/
protected function tearDown()
{
$filesystemAdapter = new FilesystemCache(sys_get_temp_dir() . '/ps__circuit_breaker');
$filesystemAdapter->deleteAll();
}
public function testCreation()
{
$doctrineCache = new DoctrineCache(
new FilesystemCache(sys_get_temp_dir() . '/ps__circuit_breaker')
);
$this->assertInstanceOf(DoctrineCache::class, $doctrineCache);
}
/**
* @depends testCreation
*/
public function testSaveTransaction()
{
$operation = $this->doctrineCache->saveTransaction(
'http://test.com',
$this->createMock(TransactionInterface::class)
);
$this->assertTrue($operation);
}
/**
* @depends testCreation
* @depends testSaveTransaction
*/
public function testHasTransaction()
{
$this->doctrineCache->saveTransaction('http://test.com', $this->createMock(TransactionInterface::class));
$this->assertTrue($this->doctrineCache->hasTransaction('http://test.com'));
}
/**
* @depends testCreation
* @depends testSaveTransaction
* @depends testHasTransaction
*/
public function testGetTransaction()
{
$translationStub = $this->createMock(TransactionInterface::class);
$this->doctrineCache->saveTransaction('http://test.com', $translationStub);
$transaction = $this->doctrineCache->getTransaction('http://test.com');
$this->assertEquals($transaction, $translationStub);
}
/**
* @depends testCreation
* @depends testGetTransaction
* @depends testHasTransaction
*/
public function testGetNotFoundTransactionThrowsAnException()
{
$this->expectException(TransactionNotFoundException::class);
$this->doctrineCache->getTransaction('http://test.com');
}
/**
* @depends testSaveTransaction
* @depends testGetTransaction
*/
public function testClear()
{
$translationStub = $this->createMock(TransactionInterface::class);
$this->doctrineCache->saveTransaction('http://a.com', $translationStub);
$this->doctrineCache->saveTransaction('http://b.com', $translationStub);
// We have stored 2 transactions
$this->assertTrue($this->doctrineCache->clear());
$this->expectException(TransactionNotFoundException::class);
$this->doctrineCache->getTransaction('http://a.com');
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Storage;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
use PrestaShop\CircuitBreaker\Storage\SimpleArray;
class SimpleArrayTest extends TestCase
{
/**
* {@inheritdoc}
*/
protected function setUp()
{
$simpleArray = new SimpleArray();
$simpleArray::$transactions = [];
}
/**
* @return void
*/
public function testCreation()
{
$simpleArray = new SimpleArray();
$this->assertCount(0, $simpleArray::$transactions);
$this->assertInstanceOf(SimpleArray::class, $simpleArray);
}
/**
* @depends testCreation
*
* @return void
*/
public function testSaveTransaction()
{
$simpleArray = new SimpleArray();
$operation = $simpleArray->saveTransaction(
'http://test.com',
$this->createMock(TransactionInterface::class)
);
$this->assertTrue($operation);
$this->assertCount(1, $simpleArray::$transactions);
}
/**
* @depends testCreation
* @depends testSaveTransaction
*
* @return void
*/
public function testHasTransaction()
{
$simpleArray = new SimpleArray();
$simpleArray->saveTransaction('http://test.com', $this->createMock(TransactionInterface::class));
$this->assertTrue($simpleArray->hasTransaction('http://test.com'));
}
/**
* @depends testCreation
* @depends testSaveTransaction
* @depends testHasTransaction
*
* @return void
*/
public function testGetTransaction()
{
$simpleArray = new SimpleArray();
$translationStub = $this->createMock(TransactionInterface::class);
$simpleArray->saveTransaction('http://test.com', $translationStub);
$transaction = $simpleArray->getTransaction('http://test.com');
$this->assertSame($transaction, $translationStub);
}
/**
* @depends testCreation
* @depends testGetTransaction
* @depends testHasTransaction
*
* @return void
*/
public function testGetNotFoundTransactionThrowsAnException()
{
$this->expectException(TransactionNotFoundException::class);
$simpleArray = new SimpleArray();
$simpleArray->getTransaction('http://test.com');
}
/**
* @depends testSaveTransaction
* @depends testGetTransaction
*
* @return void
*/
public function testClear()
{
$simpleArray = new SimpleArray();
$translationStub = $this->createMock(TransactionInterface::class);
$simpleArray->saveTransaction('http://a.com', $translationStub);
$simpleArray->saveTransaction('http://b.com', $translationStub);
// We have stored 2 transactions
$simpleArray->clear();
$transactions = $simpleArray::$transactions;
$this->assertEmpty($transactions);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Storage;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Contract\TransactionInterface;
use PrestaShop\CircuitBreaker\Exception\TransactionNotFoundException;
use PrestaShop\CircuitBreaker\Storage\SymfonyCache;
use Symfony\Component\Cache\Simple\FilesystemCache;
class SymfonyCacheTest extends TestCase
{
/**
* @var SymfonyCache the Symfony Cache storage
*/
private $symfonyCache;
/**
* {@inheritdoc}
*/
protected function setUp()
{
$this->symfonyCache = new SymfonyCache(
new FilesystemCache('ps__circuit_breaker', 20)
);
}
/**
* {@inheritdoc}
*/
protected function tearDown()
{
$filesystemAdapter = new FilesystemCache('ps__circuit_breaker', 20);
$filesystemAdapter->clear();
}
public function testCreation()
{
$symfonyCache = new SymfonyCache(
new FilesystemCache('ps__circuit_breaker')
);
$this->assertInstanceOf(SymfonyCache::class, $symfonyCache);
}
/**
* @depends testCreation
*/
public function testSaveTransaction()
{
$operation = $this->symfonyCache->saveTransaction(
'http://test.com',
$this->createMock(TransactionInterface::class)
);
$this->assertTrue($operation);
}
/**
* @depends testCreation
* @depends testSaveTransaction
*/
public function testHasTransaction()
{
$this->symfonyCache->saveTransaction('http://test.com', $this->createMock(TransactionInterface::class));
$this->assertTrue($this->symfonyCache->hasTransaction('http://test.com'));
}
/**
* @depends testCreation
* @depends testSaveTransaction
* @depends testHasTransaction
*/
public function testGetTransaction()
{
$translationStub = $this->createMock(TransactionInterface::class);
$this->symfonyCache->saveTransaction('http://test.com', $translationStub);
$transaction = $this->symfonyCache->getTransaction('http://test.com');
$this->assertEquals($transaction, $translationStub);
}
/**
* @depends testCreation
* @depends testGetTransaction
* @depends testHasTransaction
*/
public function testGetNotFoundTransactionThrowsAnException()
{
$this->expectException(TransactionNotFoundException::class);
$this->symfonyCache->getTransaction('http://test.com');
}
/**
* @depends testSaveTransaction
* @depends testGetTransaction
*/
public function testClear()
{
$translationStub = $this->createMock(TransactionInterface::class);
$this->symfonyCache->saveTransaction('http://a.com', $translationStub);
$this->symfonyCache->saveTransaction('http://b.com', $translationStub);
// We have stored 2 transactions
$this->assertTrue($this->symfonyCache->clear());
$this->expectException(TransactionNotFoundException::class);
$this->symfonyCache->getTransaction('http://a.com');
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker;
use PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount;
use Symfony\Component\EventDispatcher\EventDispatcher;
use PrestaShop\CircuitBreaker\SymfonyCircuitBreaker;
use PrestaShop\CircuitBreaker\Storage\SymfonyCache;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\System\MainSystem;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use Symfony\Component\Cache\Simple\ArrayCache;
class SymfonyCircuitBreakerEventsTest extends CircuitBreakerTestCase
{
/**
* Used to track the dispatched events.
*
* @var PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount
*/
private $spy;
/**
* We should see the circuit breaker initialized,
* a call being done and then the circuit breaker closed.
*/
public function testCircuitBreakerEventsOnFirstFailedCall()
{
$circuitBreaker = $this->createCircuitBreaker();
$circuitBreaker->call(
'https://httpbin.org/get/foo',
[],
function () {
return '{}';
}
);
/**
* The circuit breaker is initiated
* the 2 failed trials are done
* then the conditions are met to open the circuit breaker
*/
$invocations = $this->spy->getInvocations();
$this->assertCount(4, $invocations);
$this->assertSame('INITIATING', $invocations[0]->parameters[0]);
$this->assertSame('TRIAL', $invocations[1]->parameters[0]);
$this->assertSame('TRIAL', $invocations[2]->parameters[0]);
$this->assertSame('OPENING', $invocations[3]->parameters[0]);
}
/**
* @return SymfonyCircuitBreaker the circuit breaker for testing purposes
*/
private function createCircuitBreaker()
{
$system = new MainSystem(
new ClosedPlace(2, 0.2, 0),
new HalfOpenPlace(0, 0.2, 0),
new OpenPlace(0, 0, 1)
);
$symfonyCache = new SymfonyCache(new ArrayCache());
$eventDispatcherS = $this->createMock(EventDispatcher::class);
$eventDispatcherS->expects($this->spy = $this->any())
->method('dispatch')
;
return new SymfonyCircuitBreaker(
$system,
$this->getTestClient(),
$symfonyCache,
$eventDispatcherS
);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\System;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\State;
use PrestaShop\CircuitBreaker\Place\OpenPlace;
use PrestaShop\CircuitBreaker\Place\HalfOpenPlace;
use PrestaShop\CircuitBreaker\Place\ClosedPlace;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\System\MainSystem;
class MainSystemTest extends TestCase
{
public function testCreation()
{
$openPlace = new OpenPlace(1, 1, 1);
$halfOpenPlace = new HalfOpenPlace(1, 1, 1);
$closedPlace = new ClosedPlace(1, 1, 1);
$mainSystem = new MainSystem(
$openPlace,
$halfOpenPlace,
$closedPlace
);
$this->assertInstanceOf(MainSystem::class, $mainSystem);
}
/**
* @depends testCreation
*/
public function testGetInitialPlace()
{
$mainSystem = $this->createMainSystem();
$initialPlace = $mainSystem->getInitialPlace();
$this->assertInstanceOf(PlaceInterface::class, $initialPlace);
$this->assertSame(State::CLOSED_STATE, $initialPlace->getState());
}
/**
* @depends testCreation
*/
public function testGetPlaces()
{
$mainSystem = $this->createMainSystem();
$places = $mainSystem->getPlaces();
$this->assertInternalType('array', $places);
$this->assertCount(3, $places);
foreach ($places as $place) {
$this->assertInstanceOf(PlaceInterface::class, $place);
}
}
/**
* Returns an instance of MainSystem for tests.
*
* @return MainSystem
*/
private function createMainSystem()
{
$openPlace = new OpenPlace(1, 1, 1);
$halfOpenPlace = new HalfOpenPlace(1, 1, 1);
$closedPlace = new ClosedPlace(1, 1, 1);
return new MainSystem(
$openPlace,
$halfOpenPlace,
$closedPlace
);
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Transaction;
use DateTime;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject;
use PrestaShop\CircuitBreaker\Contract\PlaceInterface;
use PrestaShop\CircuitBreaker\Transaction\SimpleTransaction;
class SimpleTransactionTest extends TestCase
{
public function testCreation()
{
$placeStub = $this->createPlaceStub();
$simpleTransaction = new SimpleTransaction(
'http://some-uri.domain',
0,
$placeStub->getState(),
2
);
$this->assertInstanceOf(SimpleTransaction::class, $simpleTransaction);
}
/**
* @depends testCreation
*/
public function testGetService()
{
$simpleTransaction = $this->createSimpleTransaction();
$this->assertSame('http://some-uri.domain', $simpleTransaction->getService());
}
/**
* @depends testCreation
*/
public function testGetFailures()
{
$simpleTransaction = $this->createSimpleTransaction();
$this->assertSame(0, $simpleTransaction->getFailures());
}
/**
* @depends testCreation
*/
public function testGetState()
{
$simpleTransaction = $this->createSimpleTransaction();
$this->assertSame('FAKE_STATE', $simpleTransaction->getState());
}
/**
* @depends testCreation
*/
public function testGetThresholdDateTime()
{
$simpleTransaction = $this->createSimpleTransaction();
$expectedDateTime = (new DateTime('+2 second'))->format('d/m/Y H:i:s');
$simpleTransactionDateTime = $simpleTransaction->getThresholdDateTime()->format('d/m/Y H:i:s');
$this->assertSame($expectedDateTime, $simpleTransactionDateTime);
}
/**
* @depends testCreation
* @depends testGetFailures
*/
public function testIncrementFailures()
{
$simpleTransaction = $this->createSimpleTransaction();
$simpleTransaction->incrementFailures();
$this->assertSame(1, $simpleTransaction->getFailures());
}
/**
* @depends testCreation
*/
public function testCreationFromPlaceHelper()
{
$simpleTransactionFromHelper = SimpleTransaction::createFromPlace(
$this->createPlaceStub(),
'http://some-uri.domain'
);
$simpleTransaction = $this->createSimpleTransaction();
$this->assertSame($simpleTransactionFromHelper->getState(), $simpleTransaction->getState());
$this->assertSame($simpleTransactionFromHelper->getFailures(), $simpleTransaction->getFailures());
$fromPlaceDate = $simpleTransactionFromHelper->getThresholdDateTime()->format('d/m/Y H:i:s');
$expectedDate = $simpleTransaction->getThresholdDateTime()->format('d/m/Y H:i:s');
$this->assertSame($fromPlaceDate, $expectedDate);
}
/**
* Returns an instance of SimpleTransaction for tests.
*
* @return SimpleTransaction
*/
private function createSimpleTransaction()
{
$placeStub = $this->createPlaceStub();
return new SimpleTransaction(
'http://some-uri.domain',
0,
$placeStub->getState(),
2
);
}
/**
* Returns an instance of Place with State equals to "FAKE_STATE"
* and threshold equals to 2.
*
* @return PlaceInterface&PHPUnit_Framework_MockObject_MockObject
*/
private function createPlaceStub()
{
$placeStub = $this->createMock(PlaceInterface::class);
$placeStub->expects($this->any())
->method('getState')
->willReturn('FAKE_STATE')
;
$placeStub->expects($this->any())
->method('getThreshold')
->willReturn(2)
;
return $placeStub;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Tests\PrestaShop\CircuitBreaker\Util;
use PHPUnit\Framework\TestCase;
use PrestaShop\CircuitBreaker\Util\Assert;
use stdClass;
class AssertTest extends TestCase
{
/**
* @dataProvider getValues
*
* @param mixed $value
* @param bool $expected
*/
public function testIsPositiveValue($value, $expected)
{
$this->assertSame($expected, Assert::isPositiveValue($value));
}
/**
* @dataProvider getURIs
*
* @param mixed $value
* @param bool $expected
*/
public function testIsURI($value, $expected)
{
$this->assertSame($expected, Assert::isURI($value));
}
/**
* @dataProvider getStrings
*
* @param mixed $value
* @param bool $expected
*/
public function testIsString($value, $expected)
{
$this->assertSame($expected, Assert::isString($value));
}
/**
* @return array
*/
public function getValues()
{
return [
'0' => [0, true],
'str_0' => ['0', false],
'float' => [0.1, true],
'stdclass' => [new stdClass(), false],
'callable' => [
function () {
return 0;
},
false,
],
'negative' => [-1, false],
'bool' => [false, false],
];
}
/**
* @return array
*/
public function getURIs()
{
return [
'valid' => ['http://www.prestashop.com', true],
'int' => [0, false],
'null' => [null, false],
'bool' => [false, false],
'local' => ['http://localhost', true],
'ssh' => ['ssh://git@git.example.com/FOO/my_project.git', true],
];
}
public function getStrings()
{
return [
'valid' => ['foo', true],
'empty' => ['', false],
'null' => [null, false],
'bool' => [false, false],
'stdclass' => [new stdClass(), false],
'valid2' => ['INVALID_STATE', true],
];
}
}