Skip to content

Commit

Permalink
Merge 632cb75 into 7887b10
Browse files Browse the repository at this point in the history
  • Loading branch information
ackintosh committed Mar 31, 2020
2 parents 7887b10 + 632cb75 commit 67ec416
Show file tree
Hide file tree
Showing 25 changed files with 554 additions and 275 deletions.
83 changes: 43 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ $ganesha->failure($service);
```

```php
$ganesha = Ackintosh\Ganesha\Builder::build([
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy([
'failureRateThreshold' => 50,
'adapter' => new Ackintosh\Ganesha\Storage\Adapter\Redis($redis),
]);
Expand Down Expand Up @@ -131,7 +131,7 @@ If disabled, Ganesha keeps to record success/failure statistics, but Ganesha doe

```php
// Ganesha with Count strategy(threshold `3`).
// $ganesha = ...
// $ganesha = Ackintosh\Ganesha\Builder::withCountStrategy() ...

// Disable
Ackintosh\Ganesha::disable();
Expand All @@ -151,7 +151,7 @@ var_dump($ganesha->isAvailable($service));
Resets the statistics saved in a storage.

```php
$ganesha = Ackintosh\Ganesha\Builder::build([
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy([
// ...
]);

Expand All @@ -163,23 +163,23 @@ $ganesha->reset();

Ganesha has two strategies which avoids cascading failures.

### Rate (default)
### Rate

```php
$ganesha = Ackintosh\Ganesha\Builder::build([
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
// The interval in time (seconds) that evaluate the thresholds.
'timeWindow' => 30,
->timeWindow(30)
// The failure rate threshold in percentage that changes CircuitBreaker's state to `OPEN`.
'failureRateThreshold' => 50,
->failureRateThreshold(50)
// The minimum number of requests to detect failures.
// Even if `failureRateThreshold` exceeds the threshold,
// CircuitBreaker remains in `CLOSED` if `minimumRequests` is below this threshold.
'minimumRequests' => 10,
->minimumRequests(10)
// The interval (seconds) to change CircuitBreaker's state from `OPEN` to `HALF_OPEN`.
'intervalToHalfOpen' => 5,
->intervalToHalfOpen(5)
// The storage adapter instance to store various statistics to detect failures.
'adapter' => new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached),
]);
->adapter(new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached))
->build();
```

Note about "time window": The Storage Adapter implements either [SlidingTimeWindow](https://github.com/ackintosh/ganesha/blob/master/src/Ganesha/Storage/Adapter/SlidingTimeWindowInterface.php) or [TumblingTimeWindow](https://github.com/ackintosh/ganesha/blob/master/src/Ganesha/Storage/Adapter/TumblingTimeWindowInterface.php). The difference of the implementation comes from constraints of the storage functionalities.
Expand Down Expand Up @@ -209,16 +209,16 @@ The details to help us understand visually is shown below:
If you want use the Count strategy use `Builder::buildWithCountStrategy()` to build an instance.

```php
$ganesha = Ackintosh\Ganesha\Builder::buildWithCountStrategy([
$ganesha = Ackintosh\Ganesha\Builder::withCountStrategy()
// The failure count threshold that changes CircuitBreaker's state to `OPEN`.
// The count will be increased if `$ganesha->failure()` is called,
// or will be decreased if `$ganesha->success()` is called.
'failureCountThreshold' => 100,
->failureCountThreshold(100)
// The interval (seconds) to change CircuitBreaker's state from `OPEN` to `HALF_OPEN`.
'intervalToHalfOpen' => 5,
->intervalToHalfOpen(5)
// The storage adapter instance to store various statistics to detect failures.
'adapter' => new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached),
]);
->adapter(new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached))
->build();
```

## [Adapters](#table-of-contents)
Expand All @@ -232,9 +232,10 @@ $redis = new \Redis();
$redis->connect('localhost');
$adapter = new Ackintosh\Ganesha\Storage\Adapter\Redis($redis);

$ganesha = Ackintosh\Ganesha\Builder::build([
'adapter' => $adapter,
]);
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter($adapter)
// ... (omitted) ...
->build();
```

### Memcached
Expand All @@ -246,9 +247,10 @@ $memcached = new \Memcached();
$memcached->addServer('localhost', 11211);
$adapter = new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached);

$ganesha = Ackintosh\Ganesha\Builder::build([
'adapter' => $adapter,
]);
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter($adapter)
// ... (omitted) ...
->build();
```

### MongoDB
Expand All @@ -257,13 +259,12 @@ MongoDB adapter requires [mongodb](https://github.com/mongodb/mongo-php-library)

```php
$manager = new \MongoDB\Driver\Manager('mongodb://localhost:27017/');
$adapter = new Ackintosh\Ganesha\Storage\Adapter\MongoDB($manager);
$configuration = new Configuration(['dbName' => 'ganesha', 'collectionName' => 'ganeshaCollection']);
$adapter = new Ackintosh\Ganesha\Storage\Adapter\MongoDB($manager, 'dbName', 'collectionName');

$adapter->setConfiguration($configuration);
$ganesha = Ackintosh\Ganesha\Builder::build([
'adapter' => $adapter,
]);
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter($adapter)
// ... (omitted) ...
->build();
```

## [Customizing storage keys](#table-of-contents)
Expand All @@ -278,14 +279,15 @@ class YourStorageKeys implements StorageKeysInterface
return 'your_prefix_';
}

// (ommitted)
// ... (omitted) ...
}

$ganesha = Ackintosh\Ganesha\Builder::build([
$ganesha = Ackintosh\Ganesha\Builder::withRateStrategy()
// The keys which will stored by Ganesha to the storage you specified via `adapter`
// will be prefixed with `your_prefix_`.
'storageKeys' => new YourStorageKeys(),
]);
->storageKeys(new YourStorageKeys())
// ... (omitted) ...
->build();
```

## [Ganesha :heart: Guzzle](#table-of-contents)
Expand All @@ -299,13 +301,14 @@ use Ackintosh\Ganesha\Exception\RejectedException;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

$ganesha = Builder::build([
'timeWindow' => 30,
'failureRateThreshold' => 50,
'minimumRequests' => 10,
'intervalToHalfOpen' => 5,
'adapter' => $adapter,
]);
$ganesha = Builder::withRateStrategy()
->timeWindow(30)
->failureRateThreshold(50)
->minimumRequests(10)
->intervalToHalfOpen(5)
->adapter($adapter)
->build();

$middleware = new GuzzleMiddleware($ganesha);

$handlers = HandlerStack::create();
Expand Down Expand Up @@ -382,7 +385,7 @@ class SampleExtractor implements ServiceNameExtractorInterface

// ---

$ganesha = Builder::build([
$ganesha = Builder::withRateStrategy([
// ...
]);
$middleware = new GuzzleMiddleware(
Expand Down
14 changes: 7 additions & 7 deletions examples/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ function buildGanesha($storage)
break;
}

$ganesha = Builder::build([
'adapter' => $adapter,
'timeWindow' => TIME_WINDOW,
'failureRateThreshold' => FAILURE_RATE,
'minimumRequests' => MINIMUM_REQUESTS,
'intervalToHalfOpen' => INTERVAL_TO_HALF_OPEN,
]);
$ganesha = Builder::withRateStrategy()
->adapter($adapter)
->timeWindow(TIME_WINDOW)
->failureRateThreshold(FAILURE_RATE)
->minimumRequests(MINIMUM_REQUESTS)
->intervalToHalfOpen(INTERVAL_TO_HALF_OPEN)
->build();

$messageOnTripped = <<<__EOS__
!!!!!!!!!!!!!!!!!!!!!!!
Expand Down
44 changes: 12 additions & 32 deletions src/Ganesha/Builder.php
Original file line number Diff line number Diff line change
@@ -1,48 +1,28 @@
<?php
namespace Ackintosh\Ganesha;

use Ackintosh\Ganesha;
use InvalidArgumentException;
use Ackintosh\Ganesha\Strategy;

/**
* A front end of the strategy specific builders
*
* @package Ackintosh\Ganesha
*/
class Builder
{
/**
* @param array $params
* @return Ganesha
* @return Strategy\Rate\Builder
*/
public static function build(array $params): Ganesha
public static function withRateStrategy(): Strategy\Rate\Builder
{
$params['strategyClass'] = '\Ackintosh\Ganesha\Strategy\Rate';
return self::perform($params);
return new Strategy\Rate\Builder();
}

/**
* @param array $params
* @return Ganesha
* @return Strategy\Count\Builder
*/
public static function buildWithCountStrategy(array $params): Ganesha
public static function withCountStrategy(): Strategy\Count\Builder
{
$params['strategyClass'] = '\Ackintosh\Ganesha\Strategy\Count';
return self::perform($params);
}

/**
* @param array $params
* @return Ganesha
* @throws InvalidArgumentException
*/
private static function perform(array $params): Ganesha
{
call_user_func([$params['strategyClass'], 'validate'], $params);

$configuration = new Configuration($params);
$ganesha = new Ganesha(
call_user_func(
[$configuration['strategyClass'], 'create'],
$configuration
)
);

return $ganesha;
return new Strategy\Count\Builder();
}
}
81 changes: 70 additions & 11 deletions src/Ganesha/Configuration.php
Original file line number Diff line number Diff line change
@@ -1,40 +1,99 @@
<?php
namespace Ackintosh\Ganesha;

use Ackintosh\Ganesha\Storage\AdapterInterface;
use Ackintosh\Ganesha\Storage\StorageKeys;
use Ackintosh\Ganesha\Storage\StorageKeysInterface;

class Configuration implements \ArrayAccess
class Configuration
{
// Configuration keys
const ADAPTER = 'adapter';
const TIME_WINDOW = 'timeWindow';
const FAILURE_RATE_THRESHOLD = 'failureRateThreshold';
const FAILURE_COUNT_THRESHOLD = 'failureCountThreshold';
const MINIMUM_REQUESTS = 'minimumRequests';
const INTERVAL_TO_HALF_OPEN = 'intervalToHalfOpen';
const STORAGE_KEYS = 'storageKeys';

/**
* @var array
*/
private $params;

public function __construct($params)
{
if (!isset($params['storageKeys'])) {
$params['storageKeys'] = new StorageKeys();
if (!isset($params[self::STORAGE_KEYS])) {
$params[self::STORAGE_KEYS] = new StorageKeys();
}
$this->params = $params;
}

public function offsetSet($offset, $value)
public function adapter(): AdapterInterface
{
return $this->params[self::ADAPTER];
}

public function timeWindow(): int
{
return $this->params[self::TIME_WINDOW];
}

public function failureRateThreshold(): int
{
return $this->params[self::FAILURE_RATE_THRESHOLD];
}

public function failureCountThreshold(): int
{
return $this->params[self::FAILURE_COUNT_THRESHOLD];
}

public function minimumRequests(): int
{
$this->params[$offset] = $value;
return $this->params[self::MINIMUM_REQUESTS];
}

public function offsetExists($offset)
public function intervalToHalfOpen(): int
{
return isset($this->params[$offset]);
return $this->params[self::INTERVAL_TO_HALF_OPEN];
}

public function offsetUnset($offset)
public function storageKeys(): StorageKeysInterface
{
unset($this->params[$offset]);
return $this->params[self::STORAGE_KEYS];
}

public function offsetGet($offset)
/**
* @throws \InvalidArgumentException
*/
public function validate(): void
{
return isset($this->params[$offset]) ? $this->params[$offset] : null;
if (isset($this->params[self::ADAPTER]) && !$this->params[self::ADAPTER] instanceof AdapterInterface) {
throw new \InvalidArgumentException(get_class($this->params[self::ADAPTER]) . ' should be an instance of AdapterInterface');
}

if (isset($this->params[self::STORAGE_KEYS]) && !$this->params[self::STORAGE_KEYS] instanceof StorageKeysInterface) {
throw new \InvalidArgumentException(get_class($this->params[self::STORAGE_KEYS]) . ' should be an instance of StorageKeysInterface');
}

foreach ([
self::TIME_WINDOW,
self::FAILURE_RATE_THRESHOLD,
self::FAILURE_COUNT_THRESHOLD,
self::MINIMUM_REQUESTS,
self::INTERVAL_TO_HALF_OPEN
] as $name) {
if (isset($this->params[$name])) {
$v = $this->params[$name];
if (!is_int($v) || $v < 1) {
throw new \InvalidArgumentException($name . ' should be an positive integer');
}
}
}

if (isset($this->params[self::FAILURE_RATE_THRESHOLD]) && $this->params[self::FAILURE_RATE_THRESHOLD] > 100) {
throw new \InvalidArgumentException(self::FAILURE_RATE_THRESHOLD . ' should be equal or less than 100');
}
}
}
8 changes: 2 additions & 6 deletions src/Ganesha/Storage/Adapter/Memcached.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ class Memcached implements AdapterInterface, TumblingTimeWindowInterface
*/
private $memcached;

/**
* @var Configuration
*/
private $configuration;

/**
* Memcached constructor.
* @param \Memcached $memcached
Expand Down Expand Up @@ -50,10 +45,11 @@ public function supportRateStrategy(): bool
/**
* @param Configuration $configuration
* @return void
* @codeCoverageIgnore
*/
public function setConfiguration(Configuration $configuration): void
{
$this->configuration = $configuration;
// This adapter doesn't use the configuration.
}

/**
Expand Down

0 comments on commit 67ec416

Please sign in to comment.