View Source

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{zone-template-instance:ZFPROP:Proposal Zone Template}

{zone-data:component-name}
Zend_CircuitBreaker
{zone-data}

{zone-data:proposer-list}
[Artur Ejsmont|mailto:a.e@inne.pl]
{zone-data}

{zone-data:liaison}
TBD
{zone-data}

{zone-data:revision}
1.0 - 20 November 2010: Initial Draft.
{zone-data}

{zone-data:overview}
Circuit Breaker is a component that helps discovering externals resources failures and adapting application behavior.

If my database is overloaded and times out after 30s or a web service is broken i do not want to waste time connecting to it. I prefer to use long lived cache or even show a user friendly message instead.

Circuit breaker helps by encapsulating this logic and allowing developers to simply ask "is my resource available?" before using it. CB manages statistical data to give the appropriate answer.

Statistical data is stored using persistence adapter as an internal data structure. This information is gathered after every request to the external service so that CB would know is the external service healthy or not. Storage and retrieval of the data has to be very fast (apc/memcached preferred).
{zone-data}

{zone-data:references}
* [Wikipedia Entry|http://en.wikipedia.org/wiki/Circuit_breaker_design_pattern]
* [More details on my site|http://artur.ejsmont.org/blog/PHP-Circuit-Breaker-initial-Zend-Framework-proposal]
{zone-data}

{zone-data:requirements}
* This component *will* allow configuration of thresholds per service name.
* This component *will* allow configuration via array/config object.
* This component *will* allow replacement of persistence storage adapters.
* This component *will* provide persistence storage adapters for zend cache, dummy and database.
* This component *will* keep very simple API.
* This component *will* be ported to ZF2 as soon as zend cache 2 is fully implemented.

* This component *will not* be an singleton
* This component *will not* depend on other components unless specified explicitly by user - to allow standalone usage.

{zone-data}

{zone-data:dependencies}
* Zend_Exception
* Zend_Cache - optional, you can provide your own Zend_CircuitBreaker_Storage_Interface implementations.
{zone-data}

{zone-data:operation}
This component should be used close to the service call implementation. To decrease code duplication and end user envolvment it would be beneficial to create decorators (or some extensions) to database and web service components. This way calls to Circuit breaker could be handed by Zend Framework. It is not required for a standalone use though.

Sequence:
* User calls Circuit breaker to ask is service named 'ABC' available?
* Circuit breaker responds true if service is available or false if its considered unavailable
* User calls service based on the advice
* After service call user reports back to circuit breaker to let it know was service call successful or not.

!SequenceDiagram5_A_0.png|thumbnail!
!SequenceDiagram5_A_0.png|thumbnail!
!SequenceDiagram5_C_0.png|thumbnail!


This way CB can set thresholds of amount of failed requests in period of time. If requests were failing lately CB can start advising that service is unavailable. Therefor user code can skip service call and branch execution straight to error handling. User can also decide to use long lived cache in this scenario or take other actions necessary.


Diagram of class dependencies can be seen below. Circuit breaker delegates persistence to selected adapter type. It can also use array decorator to group all service statistics into one array - to reduce persistence storage load/save calls.

!php_circuit_breaker_zend.png|thumbnail!


Service back online discovery.

Circuit breaker comes with an important feature of probing for service avability. Once service is recognized as unavailable CB will allow single thread to try to use it based on configuration. This way you can configure retry to happen after 1,5,60 minutes or whatever value in seconds. CB will make sure just one thread will try to connect, if it succeeds it will allow rest of the threads to connect too. Otherwise it will keep service locked till next retry timeout.


{zone-data}

{zone-data:milestones}
Component is in usable state available (for now) [circuit breaker code on my website|http://artur.ejsmont.org/blog/sites/default/files/zend_cb_release_0.1_2010_06_20.zip]. There are full implementations of core classes and some test coverage.

* Milestone 1: Decide should exceptions be disabled (should CB thorw store exception) or maybe add store exception capturing decorator and config option for it?
* Milestone 2: Decide if APC handler could be included in component to have no dependency on zend cache - even that some (minimal) duplication occurs.
* Milestone 3: Initial documentation exists but more is needed
* Milestone 4: Check component somewhere into git (i dont know where should it be availalbe yet)
{zone-data}

{zone-data:class-list}
* Zend_CircuitBreaker_Interface
* Zend_CircuitBreaker
* Zend_CircuitBreaker_Storage_Interface
* Zend_CircuitBreaker_Storage_Exception
* Zend_CircuitBreaker_Storage_Adapter_Dummy
* Zend_CircuitBreaker_Storage_Adapter_ZendCacheBackend
* Zend_CircuitBreaker_Storage_Decorator_Array

* Zend_CircuitBreaker_Storage_Adapter_Apc (optionally for performance and simplicity)
{zone-data}

{zone-data:use-cases}
{composition-setup}
{deck:id=UseCases1}

{card:label="CB creation"}
I would like to provide both easy and flexible ways of instantination. I would like to support both config objects, arrays and manual dependency injection. Creation of CB object can be acheved manually or using factory. Based on parameters factory creates Zend Cache Adapter wrapper or simply creates instance of CB. Config can be passed but its optional. If no thresholds are defined defaults would apply to all services.
{code}
// providing storage interface implementation
$cb = Zend_CircuitBreaker::factory($storeInstance);
$cb = Zend_CircuitBreaker::factory($storeInstance, $config);

// providing zend cache backend or cache core instance
$cb = Zend_CircuitBreaker::factory($zendCacheBackendInstance);
$cb = Zend_CircuitBreaker::factory($zendCacheBackendInstance, $config);
$cb = Zend_CircuitBreaker::factory($zendCacheCoreInstance);

// i wish to provide a few default configuration based on most common use cases
$cb = Zend_CircuitBreaker::factory(Zend_CircuitBreaker::DUMMY_ADAPTER);
{code}
{card}

{card:label="Manual store creation"}
Store can be created manually and injected into factory. CB is not a singleton.
{code}
// Manual Adapter creation examples:
$store = new Zend_CircuitBreaker_Storage_Decorator_Array(
new Zend_CircuitBreaker_Storage_Adapter_Apc(600, 'customPrefix'));
$store = new Zend_CircuitBreaker_Storage_Decorator_Array(
new Zend_CircuitBreaker_Storage_Adapter_ZendCacheBackend(
new Zend_Cache_Backend_File(array(
"cache_dir" => "tmp",
"file_name_prefix"=>"tmp",
))));

// then use store to create CB with this persistence store
$cb = Zend_CircuitBreaker::factory($store);
{code}
{card}


{card:label="Full Manual Use"}
Use any Adapter, set thresholds, use service and CB deciding what to do based on CB advice.
{code}
$conf = array(
"UserProfileService"=>array('maxFailures'=>5, 'retryTimeout'=>30),
);

// create some adapter and then CB instance
$adapter = new Zend_CircuitBreaker_Storage_Adapter_Dummy();
$cb = Zend_CircuitBreaker::factory($adapter, $conf);

$userProfile = null;
if( $cb->isAvailable("UserProfileService") ){
try{
// do the actual call - userProfileService is your external dependency
$userProfile = $userProfileService->loadProfileOrWhatever();
// let CB know it went ok
$cb->reportSuccess("UserProfileService");
}catch( UserProfileServiceConnectionException $e ){
// if service failed also let CB know
$cb->reportFailure("UserProfileService");
}catch( Zend_CircuitBreaker_Storage_Exception $e ){
// CB could not save it's stats, maybe cache is down?
}catch( Exception $e ){
// do stuff that should happen if service is ok but query was incorrect
}
}
if( $profile === null ){
// hnadle error in a gracefull way
// for example show 'System maintenance, you cant login now.' message
// or load some static fixed data or long lived cache
}
{code}
{card}

{deck}
{zone-data}

{zone-data:skeletons}
{deck:id=Skeletons1}
{card:label=Zend_CircuitBreaker_Interface}
{code}
interface Zend_CircuitBreaker_Interface
{
/*
* Check if service is available (according to CB knowledge)
*
* @param string $serviceName - arbitrary service name
* @return boolean true if service is available, false if service is down
*/
public function isAvailable($serviceName);

/*
* Use this method to let CB know that you failed to connect to the
* service of particular name.
*
* Allows CB to update its stats accordingly for future HTTP requests.
*
* @param string $serviceName - arbitrary service name
* @return void
*/
public function reportFailure($serviceName);

/*
* Use this method to let CB know that you successfully connected to the
* service of particular name.
*
* Allows CB to update its stats accordingly for future HTTP requests.
*
* @param string $serviceName - arbitrary service name
* @return void
*/
public function reportSuccess($serviceName);
}
{code}
{card}

{card:label=Zend_CircuitBreaker_Storage_Interface}
{code}
interface Zend_CircuitBreaker_Storage_Interface
{
/*
* Loads circuit breaker service status value.
* For example failures count or last retry time.
* Method does not care what are the attribute names. They are not inspected.
* Any string can be passed as service name and attribute name.
*
* @param string $serviceName name of service to load stats for
* @param string $attributeName name of attribute to load
* @return string value stored or '' if value was not found
*
* @throws Zend_CircuitBreaker_Storage_Exception if storage error occurs, handler can not be used
*/
public function loadStatus( $serviceName, $attributeName );

/*
* Saves circuit breaker service status value.
* Method does not care what are the attribute names. They are not inspected.
* Any string can be passed as service name and attribute name, value can be int/string.
*
* Saving in storage is not guaranteed unless flush is set to true.
* Use calls without flush if you know you will update more than one value and you want to
* improve performance of the calls.
*
* @param string $serviceName name of service to load stats for
* @param string $attributeName name of the attribute to load
* @param string $value string value loaded or '' if nothing found
* @param boolean $flush set to true will force immediate save, false does not guaranteed saving at all.
* @return void
*
* @throws Zend_CircuitBreaker_Storage_Exception if storage error occurs, handler can not be used
*/
public function saveStatus( $serviceName, $attributeName, $value, $flush = false );
}
{code}
{card}
{deck}
{zone-data}

{zone-template-instance}]]></ac:plain-text-body></ac:macro>
<p>!classes|thumbnail!</p>