Skip to content

Commit

Permalink
Project kickstart
Browse files Browse the repository at this point in the history
  • Loading branch information
Ricardo Fiorani committed Jul 18, 2019
0 parents commit 9c18782
Show file tree
Hide file tree
Showing 20 changed files with 856 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
composer.lock
vendor
.idea
.phpunit.result.cache
coverage
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# pirate-circuit-breaker

[![Latest Version on Packagist][ico-version]][link-packagist]
[![Software License][ico-license]](LICENSE.md)
[![Build Status][ico-travis]][link-travis]
[![Coverage Status][ico-scrutinizer]][link-scrutinizer]
[![Quality Score][ico-code-quality]][link-code-quality]
[![Total Downloads][ico-downloads]][link-downloads]

This is an Implementation of the 2-state (Open and Closed) CircuitBreaker pattern that we use at HolidayPirates.
Unlike the [3-state CircuitBreaker proposed by Fowler](https://martinfowler.com/bliki/CircuitBreaker.html), this implementation has only two states, "Open" and "Closed".

## Install

Via Composer

```bash
$ composer require holidaypirates/pirate-circuit-breaker
```

## Requirements
- PHP 7.3
- An implementation of the `\Psr\SimpleCache\CacheInterface` to store the services failures and circuit state OR your own storage implementation of `\HolidayPirates\CircuitBreaker\Storage\StorageInterface`
- For development only : Docker and Docker-Compose
## Usage

```php
<?php declare(strict_types=1);

use HolidayPirates\CircuitBreaker\CircuitBreaker;
use HolidayPirates\CircuitBreaker\Service\DummyService;
use HolidayPirates\CircuitBreaker\Storage\Adapter\SimpleCacheAdapter;

// Setup:
$pool = new YourCachePool(); // Any implementation of \Psr\SimpleCache\CacheInterface
$storageAdapter = new SimpleCacheAdapter($pool);
$circuitBreaker = new CircuitBreaker($storageAdapter);

$service = new DummyService(5, 60); //After 5 failed attempts it will wait 60 seconds before allowing more requests.

$circuitBreaker->registerService($service);

// Usage:
$dummyApiClient = new DummyApiClient(); // This will be any service you want to protect with the CB

if (false == $circuitBreaker->isServiceAvailable(DummyService::class)) {
throw new \Exception('Service unavailable');
}

try {
$response = $dummyApiClient->sendRequest();
$circuitBreaker->reportSuccess(DummyService::class);
} catch (Exception $exception) {
$circuitBreaker->reportFailure(DummyService::class);

throw new \Exception('Service unavailable',0, $exception);
}

```
> Please note that `HolidayPirates\CircuitBreaker\Service\DummyService` is just an implementation of `\HolidayPirates\CircuitBreaker\Service\ServiceInterface`.
> You must create your own implementations of `\HolidayPirates\CircuitBreaker\Service\ServiceInterface` for each service that you want the CircuitBreaker to operate in.
For more examples of usage please see `\HolidayPirates\Tests\Integration\CircuitBreaker\CircuitBreakerTest`
## Testing

```bash
$ docker-compose run php vendor/bin/phpunit
```

## Credits

- [Ricardo Fiorani][link-author]
- [All Contributors][link-contributors]

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

[ico-version]: https://img.shields.io/packagist/v/holidaypirates/pirate-circuit-breaker.svg?style=flat-square
[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
[ico-travis]: https://img.shields.io/travis/holidaypirates/pirate-circuit-breaker/master.svg?style=flat-square
[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/holidaypirates/pirate-circuit-breaker.svg?style=flat-square
[ico-code-quality]: https://img.shields.io/scrutinizer/g/holidaypirates/pirate-circuit-breaker.svg?style=flat-square
[ico-downloads]: https://img.shields.io/packagist/dt/holidaypirates/pirate-circuit-breaker.svg?style=flat-square

[link-packagist]: https://packagist.org/packages/holidaypirates/pirate-circuit-breaker
[link-travis]: https://travis-ci.org/holidaypirates/pirate-circuit-breaker
[link-scrutinizer]: https://scrutinizer-ci.com/g/holidaypirates/pirate-circuit-breaker/code-structure
[link-code-quality]: https://scrutinizer-ci.com/g/holidaypirates/pirate-circuit-breaker
[link-downloads]: https://packagist.org/packages/holidaypirates/pirate-circuit-breaker
[link-author]: https://github.com/ricardofiorani
[link-contributors]: ../../contributors
40 changes: 40 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "holidaypirates/pirate-circuit-breaker",
"description": "Implementation of the 2-state CircuitBreaker pattern that we use at HolidayPirates",
"homepage": "https://www.holidaypirates.group",
"type": "library",
"keywords": [
"circuit-breaker",
"service",
"protection"
],
"license": "MIT",
"prefer-stable": true,
"authors": [
{
"name": "Ricardo Fiorani",
"email": "[email protected]"
}
],
"autoload": {
"psr-4": {
"HolidayPirates\\CircuitBreaker\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HolidayPirates\\Tests\\Integration\\CircuitBreaker\\": "tests/Integration",
"HolidayPirates\\Tests\\Unit\\CircuitBreaker\\": "tests/Unit"
}
},
"require": {
"php": ">=7.3",
"psr/simple-cache": "^1.0"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"phpunit/phpunit": "^8.2",
"cache/array-adapter": "^1.0",
"cache/redis-adapter": "^1.0"
}
}
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3'
services:
php:
build: docker/php
volumes:
- .:/code
working_dir: "/code"
links:
- redis

redis:
image: redis:5.0.4-alpine
5 changes: 5 additions & 0 deletions docker/php/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM php:7.3-fpm
RUN pecl install xdebug-2.7.1 && pecl install redis-4.0.1
RUN docker-php-ext-enable xdebug redis

WORKDIR "/code"
19 changes: 19 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Integration">
<directory>./tests/Integration</directory>
</testsuite>
<testsuite name="Unit">
<directory>./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>
10 changes: 10 additions & 0 deletions src/Awareness/CircuitBreakerAwareInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types=1);

namespace HolidayPirates\CircuitBreaker\Awareness;

use HolidayPirates\CircuitBreaker\CircuitBreakerInterface;

interface CircuitBreakerAwareInterface
{
public function setCircuitBreaker(CircuitBreakerInterface $circuitBreaker): void;
}
41 changes: 41 additions & 0 deletions src/Awareness/CircuitBreakerAwareTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types=1);

namespace HolidayPirates\CircuitBreaker\Awareness;

use HolidayPirates\CircuitBreaker\CircuitBreakerInterface;
use HolidayPirates\CircuitBreaker\Exception\UnavailableServiceException;

trait CircuitBreakerAwareTrait
{
private $circuitBreaker;

public function setCircuitBreaker(CircuitBreakerInterface $circuitBreaker): void
{
$this->circuitBreaker = $circuitBreaker;
}

public function getCircuitBreaker(): CircuitBreakerInterface
{
return $this->circuitBreaker;
}

/**
* @throws UnavailableServiceException
*/
public function throwExceptionIfServiceUnavailable(string $serviceName): void
{
if (false === $this->getCircuitBreaker()->isServiceAvailable($serviceName)) {
throw new UnavailableServiceException("Service {$serviceName} is not available right now.");
}
}

public function reportServiceSuccess(string $serviceName): void
{
$this->getCircuitBreaker()->reportSuccess($serviceName);
}

public function reportServiceFailure(string $serviceName): void
{
$this->getCircuitBreaker()->reportFailure($serviceName);
}
}
100 changes: 100 additions & 0 deletions src/CircuitBreaker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php declare(strict_types=1);

namespace HolidayPirates\CircuitBreaker;

use HolidayPirates\CircuitBreaker\Service\ServiceInterface;
use HolidayPirates\CircuitBreaker\Storage\StorageInterface;

class CircuitBreaker implements CircuitBreakerInterface
{
private $storage;
private $services = [];

public function __construct(StorageInterface $storage)
{
$this->storage = $storage;
}

public function registerService(ServiceInterface $service): void
{
$this->services[$service->getIdentifier()] = $service;
}

public function isServiceAvailable(string $serviceName): bool
{
$service = $this->getService($serviceName);

if ($this->isCircuitOpen($service)) {
return false;
}

/**
* If we wanted a full 3-state CircuitBreaker implementation as described by Fowler, it is in here
* that we can add the logic for the "Half-open circuit", which would allow a smaller number of requests
* to go through before we really close the circuit.
*/

return true;
}

public function reportFailure(string $serviceName): void
{
$service = $this->getService($serviceName);
$amountOfFailures = $this->storage->getAmountOfFailures($service);

if ($amountOfFailures >= $service->getMaxFailures()) {
$this->setOpenCircuit($service);

return;
}

$this->storage->incrementAmountOfFailures($service);
}

public function reportSuccess(string $serviceName): void
{
$service = $this->getService($serviceName);

$this->storage->incrementAmountOfSuccess($service);
}

public function areAllServicesAvailable(): bool
{
foreach ($this->getRegisteredServiceNames() as $serviceName) {
if (!$this->isServiceAvailable($serviceName)) {
return false;
}
}

return true;
}

public function getRegisteredServiceNames(): array
{
return array_keys($this->services);
}

private function getService(string $serviceName): ServiceInterface
{
if (false === isset($this->services[$serviceName])) {
throw new \LogicException(
sprintf(
'Service not found. Did you forgot to call registerService(%s) ?',
$serviceName
)
);
}

return $this->services[$serviceName];
}

private function setOpenCircuit(ServiceInterface $service): void
{
$this->storage->setOpenCircuit($service);
}

private function isCircuitOpen(ServiceInterface $service): bool
{
return $this->storage->isCircuitOpen($service);
}
}
37 changes: 37 additions & 0 deletions src/CircuitBreakerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types=1);

namespace HolidayPirates\CircuitBreaker;

use HolidayPirates\CircuitBreaker\Service\ServiceInterface;
use HolidayPirates\CircuitBreaker\Storage\Adapter\Exception\StorageAdapterException;

interface CircuitBreakerInterface
{
public function registerService(ServiceInterface $service): void;

/**
* @throws StorageAdapterException
*/
public function isServiceAvailable(string $serviceName): bool;

/**
* @throws StorageAdapterException
*/
public function areAllServicesAvailable(): bool;

/**
* @throws StorageAdapterException
*/
public function getRegisteredServiceNames(): array;

/**
* @throws StorageAdapterException
*/

public function reportFailure(string $serviceName): void;

/**
* @throws StorageAdapterException
*/
public function reportSuccess(string $serviceName): void;
}
10 changes: 10 additions & 0 deletions src/Exception/UnavailableServiceException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types=1);

namespace HolidayPirates\CircuitBreaker\Exception;

use Exception;

class UnavailableServiceException extends Exception
{

}
Loading

0 comments on commit 9c18782

Please sign in to comment.