Skip to content

🐘 A Circuit Breaker pattern implementation for PHP applications.

License

Notifications You must be signed in to change notification settings

ackintosh/ganesha

Repository files navigation

Ganesha

Ganesha is PHP implementation ofCircuit Breaker patternwhich has multi strategies to avoid cascading failures and supports various storages to record statistics.

ganesha

Latest Stable VersionTestsCoverage StatusScrutinizer Code QualityMinimum PHP Version

If Ganesha is saving your service from system failures, please considersupportingto this project's author,Akihito Nakano,to show your ❤️ and support. Thank you!

Sponsor @ackintosh on GitHub Sponsors


This is one of theCircuit Breakerimplementation in PHP which has been actively developed and production ready - well-tested and well-documented. 💪 You can integrate Ganesha to your existing code base easily as Ganesha provides just simple interfaces andGuzzle Middlewarebehaves transparency.

If you have an idea about enhancement, bugfix..., please let me know viaIssues.✨

Table of contents

Hereis an example which shows you how Ganesha behaves when a failure occurs.
It is easily executable. All you need is Docker.

#Install Composer
$ curl -sS https://getcomposer.org/installer|php

#Run the Composer command to install the latest version of Ganesha
$ php composer.phar require ackintosh/ganesha

Ganesha provides following simple interfaces. Each method receives a string (named$servicein example) to identify the service.$servicewill be the service name of the API, the endpoint name, etc. Please remember that Ganesha detects system failure for each$service.

$ganesha->isAvailable($service);
$ganesha->success($service);
$ganesha->failure($service);
// For further details about builder options, please see the `Strategy` section.
$ganesha=Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter(newAckintosh\Ganesha\Storage\Adapter\Redis($redis))
->failureRateThreshold(50)
->intervalToHalfOpen(10)
->minimumRequests(10)
->timeWindow(30)
->build();

$service='external_api';

if(!$ganesha->isAvailable($service)) {
die('external api is not available');
}

try{
echo\Api::send($request)->getBody();
$ganesha->success($service);
}catch(\Api\RequestTimedOutException$e) {
// If an error occurred, it must be recorded as failure.
$ganesha->failure($service);
die($e->getMessage());
}

Three states of circuit breaker

(martinfowler: CircuitBreaker)

Ganesha follows the states and transitions described in the article faithfully.$ganesha->isAvailable()returnstrueif the circuit states onClosed,otherwise it returnsfalse.

Subscribe to events in ganesha

  • When the circuit state transitions toOpenthe eventGanesha::EVENT_TRIPPEDis triggered
  • When the state back toClosedthe eventGanesha::EVENT_CALMED_DOWNis triggered
$ganesha->subscribe(function($event,$service,$message) {
switch($event) {
caseGanesha::EVENT_TRIPPED:
\YourMonitoringSystem::warn(
"Ganesha has tripped! It seems that a failure has occurred in{$service}.{$message}."
);
break;
caseGanesha::EVENT_CALMED_DOWN:
\YourMonitoringSystem::info(
"The failure in{$service}seems to have calmed down:).{$message}."
);
break;
caseGanesha::EVENT_STORAGE_ERROR:
\YourMonitoringSystem::error($message);
break;
default:
break;
}
});

Disable

If disabled, Ganesha keeps to record success/failure statistics, but Ganesha doesn't trip even if the failure count reached to a threshold.

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

// Disable
Ackintosh\Ganesha::disable();

// Although the failure is recorded to storage,
$ganesha->failure($service);
$ganesha->failure($service);
$ganesha->failure($service);

// Ganesha does not trip and Ganesha::isAvailable() returns true.
var_dump($ganesha->isAvailable($service));
// bool(true)

Reset

Resets the statistics saved in a storage.

$ganesha=Ackintosh\Ganesha\Builder::withRateStrategy()
//...
->build();

$ganesha->reset();

Ganesha has two strategies which avoids cascading failures.

Rate

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

Note about "time window": The Storage Adapter implements eitherSlidingTimeWindoworTumblingTimeWindow.The difference of the implementation comes from constraints of the storage functionalities.

[SlidingTimeWindow]

  • SlidingTimeWindowimplements a time period that stretches back in time from the present. For instance, a SlidingTimeWindow of 30 seconds includes any events that have occurred in the past 30 seconds.
  • Redis adapterandMongoDB adapterimplements SlidingTimeWindow.

The details to help us understand visually is shown below:
(quoted fromIntroduction to Stream Analytics windowing functions - Microsoft Azure)

[TumblingTimeWindow]

The details to help us understand visually is shown below:
(quoted fromIntroduction to Stream Analytics windowing functions - Microsoft Azure)

Count

If you prefer the Count strategy useBuilder::buildWithCountStrategy()to build an instance.

$ganesha=Ackintosh\Ganesha\Builder::withCountStrategy()
//Thefailure count threshold that changesCircuitBreaker'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)
//Theinterval(seconds) to changeCircuitBreaker's state from `OPEN` to `HALF_OPEN`.
->intervalToHalfOpen(5)
// The storage adapter instance to store various statistics to detect failures.
->adapter(new Ackintosh\Ganesha\Storage\Adapter\Memcached($memcached))
->build();

APCu

The APCu adapter requires theAPCuextension.

$adapter=newAckintosh\Ganesha\Storage\Adapter\Apcu();

$ganesha=Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter($adapter)
//...(omitted)...
->build();

Note: APCu is internal to each server/instance, not pooled like most Memcache and Redis setups. Each worker's circuit breaker will activate or reset individually, and failure thresholds should be set lower to compensate.

Redis

Redis adapter requiresphpredisorPredisclient instance. The example below is usingphpredis.

$redis=new\Redis();
$redis->connect('localhost');
$adapter=newAckintosh\Ganesha\Storage\Adapter\Redis($redis);

$ganesha=Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter($adapter)
//...(omitted)...
->build();

Memcached

Memcached adapter requiresmemcached(NOT memcache) extension.

$memcached=new\Memcached();
$memcached->addServer('localhost',11211);
$adapter=newAckintosh\Ganesha\Storage\Adapter\Memcached($memcached);

$ganesha=Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter($adapter)
//...(omitted)...
->build();

MongoDB

MongoDB adapter requiresmongodbextension.

$manager=new\MongoDB\Driver\Manager('mongodb://localhost:27017/');
$adapter=newAckintosh\Ganesha\Storage\Adapter\MongoDB($manager,'dbName','collectionName');

$ganesha=Ackintosh\Ganesha\Builder::withRateStrategy()
->adapter($adapter)
//...(omitted)...
->build();

If you want to customize the keys to be used when storing circuit breaker information, set an instance which implementsStorageKeysInterface.

classYourStorageKeysimplementsStorageKeysInterface
{
publicfunctionprefix()
{
return'your_prefix_';
}

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

$ganesha=Ackintosh\Ganesha\Builder::withRateStrategy()
//Thekeys which will stored byGaneshato the storage you specified via `adapter`
// will be prefixed with `your_prefix_`.
->storageKeys(newYourStorageKeys())
//...(omitted)...
->build();

If you are usingGuzzle(v6 or higher),Guzzle Middlewarepowered by Ganesha makes it easy to integrate Circuit Breaker to your existing code base.

useAckintosh\Ganesha\Builder;
useAckintosh\Ganesha\GuzzleMiddleware;
useAckintosh\Ganesha\Exception\RejectedException;
useGuzzleHttp\Client;
useGuzzleHttp\HandlerStack;

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

$middleware=newGuzzleMiddleware($ganesha);

$handlers=HandlerStack::create();
$handlers->push($middleware);

$client=newClient(['handler'=>$handlers]);

try{
$client->get('http://api.example /awesome_resource');
}catch(RejectedException$e) {
// If the circuit breaker is open, RejectedException will be thrown.
}

How does Guzzle Middleware determine the$service?

As documented inUsage,Ganesha detects failures for each$service.Below, We will show you how Guzzle Middleware determine$serviceand how we specify$serviceexplicitly.

By default, the host name is used as$service.

// In the example above, `api.example ` is used as `$service`.
$client->get('http://api.example /awesome_resource');

You can also specify$servicevia a option passed to client, or request header. If both are specified, the option value takes precedence.

// via constructor argument
$client=newClient([
'handler'=>$handlers,
// 'ganesha.service_name' is defined as ServiceNameExtractor::OPTION_KEY
'ganesha.service_name'=>'specified_service_name',
]);

// via request method argument
$client->get(
'http://api.example /awesome_resource',
[
'ganesha.service_name'=>'specified_service_name',
]
);

// via request header
$request=newRequest(
'GET',
'http://api.example /awesome_resource',
[
// 'X-Ganesha-Service-Name' is defined as ServiceNameExtractor::HEADER_NAME
'X-Ganesha-Service-Name'=>'specified_service_name'
]
);
$client->send($request);

Alternatively, you can apply your own rules by implementing a class that implements theServiceNameExtractorInterface.

useAckintosh\Ganesha\GuzzleMiddleware\ServiceNameExtractorInterface;
usePsr\Http\Message\RequestInterface;

classSampleExtractorimplementsServiceNameExtractorInterface
{
/**
* @override
*/
publicfunctionextract(RequestInterface$request,array$requestOptions)
{
// We treat the combination of host name and HTTP method name as $service.
return$request->getUri()->getHost().'_'.$request->getMethod();
}
}

// ---

$ganesha=Builder::withRateStrategy()
//...
->build();
$middleware=newGuzzleMiddleware(
$ganesha,
// Pass the extractor as an argument of GuzzleMiddleware constructor.
newSampleExtractor()
);

How does Guzzle Middleware determine the failure?

By default, if the next handler promise is fulfilled ganesha will consider it a success, and a failure if it is rejected.

You can implement your own rules on fulfilled response by passing an implementation ofFailureDetectorInterfaceto the middleware.

useAckintosh\Ganesha\GuzzleMiddleware\FailureDetectorInterface;
usePsr\Http\Message\ResponseInterface;

classHttpStatusFailureDetectorimplementsFailureDetectorInterface
{
publicfunctionisFailureResponse(ResponseInterface$response):bool
{
returnin_array($response->getStatusCode(), [503,504],true);
}
}

// ---
$ganesha=Builder::withRateStrategy()
//...
->build();
$middleware=newGuzzleMiddleware(
$ganesha,
// Pass the failure detector to the GuzzleMiddleware constructor.
failureDetector:newHttpStatusFailureDetector()
);

PHP client generated byOpenAPI Generatoris using Guzzle as HTTP client and as we mentioned asGanesha ❤️ Guzzle,Guzzle Middleware powered by Ganesha is ready. So it is easily possible to integrate Ganesha and the PHP client generated by OpenAPI Generator in a smart way as below.

// For details on how to build middleware please see https://github /ackintosh/ganesha#ganesha-heart-guzzle
$middleware=newGuzzleMiddleware($ganesha);

// Set the middleware to HTTP client.
$handlers=HandlerStack::create();
$handlers->push($middleware);
$client=newClient(['handler'=>$handlers]);

// Just pass the HTTP client to the constructor of API class.
$api=newPetApi($client);

try{
// Ganesha is working in the shadows! The result of api call is monitored by Ganesha.
$api->getPetById(123);
}catch(RejectedException$e) {
awesomeErrorHandling($e);
}

If you are usingSymfony HttpClient,GaneshaHttpClient makes it easy to integrate Circuit Breaker to your existing code base.

useAckintosh\Ganesha\Builder;
useAckintosh\Ganesha\GaneshaHttpClient;
useAckintosh\Ganesha\Exception\RejectedException;

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

$client=HttpClient::create();
$ganeshaClient=newGaneshaHttpClient($client,$ganesha);

try{
$ganeshaClient->request('GET','http://api.example /awesome_resource');
}catch(RejectedException$e) {
// If the circuit breaker is open, RejectedException will be thrown.
}

How does GaneshaHttpClient determine the$service?

As documented inUsage,Ganesha detects failures for each$service.Below, We will show you how GaneshaHttpClient determine$serviceand how we specify$serviceexplicitly.

By default, the host name is used as$service.

// In the example above, `api.example ` is used as `$service`.
$ganeshaClient->request('GET','http://api.example /awesome_resource');

You can also specify$servicevia a option passed to client, or request header. If both are specified, the option value takes precedence.

// via constructor argument
$ganeshaClient=newGaneshaHttpClient($client,$ganesha,[
// 'ganesha.service_name' is defined as ServiceNameExtractor::OPTION_KEY
'ganesha.service_name'=>'specified_service_name',
]);

// via request method argument
$ganeshaClient->request(
'GET',
'http://api.example /awesome_resource',
[
'ganesha.service_name'=>'specified_service_name',
]
);

// via request header
$ganeshaClient->request('GET','',['headers'=> [
// 'X-Ganesha-Service-Name' is defined as ServiceNameExtractor::HEADER_NAME
'X-Ganesha-Service-Name'=>'specified_service_name'
]]);

Alternatively, you can apply your own rules by implementing a class that implements theServiceNameExtractorInterface.

useAckintosh\Ganesha\HttpClient\HostTrait;
useAckintosh\Ganesha\HttpClient\ServiceNameExtractorInterface;

finalclassSampleExtractorimplementsServiceNameExtractorInterface
{
useHostTrait;

/**
* @override
*/
publicfunctionextract(string$method,string$url,array$requestOptions):string
{
// We treat the combination of host name and HTTP method name as $service.
returnself::extractHostFromUrl($url).'_'.$method;
}
}

// ---

$ganesha=Builder::withRateStrategy()
//...
->build();
$ganeshaClient=newGaneshaHttpClient(
$client,
$ganesha,
// Pass the extractor as an argument of GaneshaHttpClient constructor.
newSampleExtractor()
);

How does GaneshaHttpClient determine the failure?

As documented inUsage,Ganesha detects failures for each$service. Below, We will show you how GaneshaHttpClient specify failure explicitly.

By default, Ganesha considers a request is successful as soon as the server responded, whatever the HTTP status code.

Alternatively, you can use theRestFailureDetectorimplementation ofFailureDetectorInterfaceto specify a list of HTTP Status Code to be considered as failure via an option passed to client.
This implementation will consider failure when these HTTP status codes are returned by the server:

  • 500 (Internal Server Error)
  • 502 (Bad Gateway or Proxy Error)
  • 503 (Service Unavailable)
  • 504 (Gateway Time-out)
  • 505 (HTTP Version not supported)
// via constructor argument
$ganeshaClient=newGaneshaHttpClient(
$client,$ganesha,null,
newRestFailureDetector([503])
);

// via request method argument
$ganeshaClient->request(
'GET',
'http://api.example /awesome_resource',
[
// 'ganesha.failure_status_codes' is defined as RestFailureDetector::OPTION_KEY
'ganesha.failure_status_codes'=> [503],
]
);

Alternatively, you can apply your own rules by implementing a class that implements theFailureDetectorInterface.

useAckintosh\Ganesha\HttpClient\FailureDetectorInterface;
useSymfony\Contracts\HttpClient\Exception\ExceptionInterface;
useSymfony\Contracts\HttpClient\ResponseInterface;

finalclassSampleFailureDetectorimplementsFailureDetectorInterface
{
/**
* @override
*/
publicfunctionisFailureResponse(ResponseInterface$response,array$requestOptions):bool
{
try{
$jsonData=$response->toArray();
}catch(ExceptionInterface$e) {
returntrue;
}

// Server is not RestFull and always returns HTTP 200 Status Code, but set an error flag in the JSON payload.
returntrue=== ($jsonData['error']??false);
}

/**
* @override
*/
publicfunctiongetOptionKeys():array
{
// No option is defined for this implementation
return[];
}
}

// ---

$ganesha=Builder::withRateStrategy()
//...
->build();
$ganeshaClient=newGaneshaHttpClient(
$client,
$ganesha,
null,
// Pass the failure detector as an argument of GaneshaHttpClient constructor.
newSampleFailureDetector()
);

Here are some companies using Ganesha in production! We are proud of them. 🐘

To add your company to the list, please visitREADME.mdand click on the icon to edit the page or let me know viaissues/twitter.

( Alpha betical order)

Here are some articles/videos introduce Ganesha! All of them are really shining like a jewel for us. ✨

Articles

Videos

We can run unit tests on a Docker container, so it is not necessary to install the dependencies in your machine.

#Start data stores (Redis, Memcached, etc)
$ docker-compose up

#Run `composer install`
$ docker-compose run --rm -w /tmp/ganesha -u ganesha client composer install

#Run tests in container
$ docker-compose run --rm -w /tmp/ganesha -u ganesha client vendor/bin/phpunit
  • An extension or client library which is used bythe storage adapteryou've choice will be required. Please check theAdapterssection for details.

Version Guidance

Version PHP Version
3.x >=8.0
2.x >=7.3
1.x >=7.1
0.x >=5.6

Ganesha© ackintosh, Released under theMITLicense.
Authored and maintained by ackintosh

GitHub@ackintosh/ Twitter@NAKANO_Akihito/Blog (ja)