Skip to content
This repository has been archived by the owner on Aug 17, 2022. It is now read-only.

Commit

Permalink
Merge pull request #9 from loveOSS/introduced-manual-controls
Browse files Browse the repository at this point in the history
Introduced manual controls
  • Loading branch information
mickaelandrieu committed May 24, 2019
2 parents 6900934 + fcfae84 commit 543d886
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 1 deletion.
24 changes: 24 additions & 0 deletions src/Contracts/CircuitBreaker.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,28 @@ public function isHalfOpened(): bool;
* @return bool checks if the circuit breaker is closed
*/
public function isClosed(): bool;

/**
* @return bool checks if the circuit breaker is isolated
*/
public function isIsolated(): bool;

/**
* Manually open (and hold open) the Circuit Breaker
* This can be used for example to take it offline for maintenance.
*
* @param string $service the service to call
*
* @return self
*/
public function isolate(string $service): self;

/**
* Reset the breaker to closed state to start accepting actions again.
*
* @param string $service the service to call
*
* @return self
*/
public function reset(string $service): self;
}
36 changes: 36 additions & 0 deletions src/MainCircuitBreaker.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public function call(string $service, callable $fallback, array $serviceParamete
{
$transaction = $this->initTransaction($service, $serviceParameters);

if ($this->isIsolated()) {
return (string) $fallback();
}

if ($this->isOpened()) {
if (!$this->canAccessService($transaction)) {
return (string) $fallback();
Expand Down Expand Up @@ -140,6 +144,38 @@ public function isClosed(): bool
return States::CLOSED_STATE === $this->currentPlace->getState();
}

/**
* {@inheritdoc}
*/
public function isIsolated(): bool
{
return States::ISOLATED_STATE === $this->currentPlace->getState();
}

/**
* {@inheritdoc}
*/
public function isolate(string $service): CircuitBreaker
{
$this->currentPlace = $this->places[States::ISOLATED_STATE];

$this->dispatch(Transitions::ISOLATING_TRANSITION, $service, []);

return $this;
}

/**
* {@inheritdoc}
*/
public function reset(string $service): CircuitBreaker
{
$this->currentPlace = $this->places[States::CLOSED_STATE];

$this->dispatch(Transitions::RESETTING_TRANSITION, $service, []);

return $this;
}

/**
* @param string $state the Place state
* @param string $service the service URI
Expand Down
6 changes: 6 additions & 0 deletions src/Places/ClosedPlace.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

use Resiliency\States;

/**
* The circuit initially starts closed. When the circuit is closed:
*
* The circuit-breaker executes actions placed through it, measuring the failures and successes of those actions.
* If the failures exceed a certain threshold, the circuit will break (open).
*/
final class ClosedPlace extends AbstractPlace
{
/**
Expand Down
10 changes: 10 additions & 0 deletions src/Places/HalfOpenPlace.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

use Resiliency\States;

/**
* When the circuit is half-open:
*
* the next action will be treated as a trial, to determine the circuit's health.
*
* If this call throws a handled exception, that exception is rethrown,
* and the circuit transitions immediately back to open, and remains open again for the configured timespan.
*
* If the call throws no exception, the circuit transitions back to closed.
*/
final class HalfOpenPlace extends AbstractPlace
{
/**
Expand Down
25 changes: 25 additions & 0 deletions src/Places/IsolatedPlace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Resiliency\Places;

use Resiliency\States;

/**
* This state is manually triggered to ensure the Circuit Breaker
* remains open until we reset it.
*/
class IsolatedPlace extends AbstractPlace
{
public function __construct()
{
parent::__construct(0, 0.0, 0.0);
}

/**
* {@inheritdoc}
*/
public function getState(): string
{
return States::ISOLATED_STATE;
}
}
4 changes: 4 additions & 0 deletions src/Places/OpenPlace.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

use Resiliency\States;

/**
* While the circuit is in an open state: every call to the service
* won't be executed and the fallback callback is executed.
*/
final class OpenPlace extends AbstractPlace
{
/**
Expand Down
6 changes: 6 additions & 0 deletions src/States.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ final class States
* to evaluate is done and not the alternative call.
*/
const CLOSED_STATE = 'CLOSED';

/**
* Once isolated, the circuit breaker stays in OPEN state and
* won't accept any requests, even when the threshold is reached.
*/
const ISOLATED_STATE = 'ISOLATED';
}
3 changes: 3 additions & 0 deletions src/Systems/MainSystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Resiliency\Systems;

use Resiliency\Places\IsolatedPlace;
use Resiliency\States;
use Resiliency\Contracts\Place;
use Resiliency\Contracts\System;
Expand Down Expand Up @@ -39,11 +40,13 @@ public function __construct(
$closedPlace = new ClosedPlace($failures, $timeout);
$halfOpenPlace = new HalfOpenPlace($strippedTimeout);
$openPlace = new OpenPlace($threshold);
$isolatedPlace = new IsolatedPlace();

$this->places = [
$closedPlace->getState() => $closedPlace,
$halfOpenPlace->getState() => $halfOpenPlace,
$openPlace->getState() => $openPlace,
$isolatedPlace->getState() => $isolatedPlace,
];
}

Expand Down
10 changes: 10 additions & 0 deletions src/Transitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,14 @@ final class Transitions
* Happened on each try to call the service.
*/
const TRIAL_TRANSITION = 'TRIAL';

/**
* Happened when the Circuit Breaker is isolated.
*/
const ISOLATING_TRANSITION = 'ISOLATING';

/**
* Happened when the Circuit Breaker is reset.
*/
const RESETTING_TRANSITION = 'RESETTING';
}
27 changes: 27 additions & 0 deletions tests/CircuitBreakerWorkflowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,33 @@ public function testOnceInHalfOpenModeServiceIsFinallyReachable(CircuitBreaker $
$this->assertTrue($circuitBreaker->isClosed());
}

/**
* The Circuit Breaker can be isolated, once its done it remains
* Open and so on only fallback responses will be sent.
*
* @dataProvider getCircuitBreakers
*/
public function testOnceCircuitBreakerIsIsolatedNoTrialsAreDone(CircuitBreaker $circuitBreaker): void
{
$circuitBreaker->isolate('https://httpbin.org/get/foo');

$response = $circuitBreaker->call('https://httpbin.org/get/foo', $this->createFallbackResponse());
$this->assertSame('{}', $response);
$this->assertTrue($circuitBreaker->isIsolated());

// Let's do 10 calls!

for ($i = 0; $i < 10; ++$i) {
$circuitBreaker->call('https://httpbin.org/get/foo', $this->createFallbackResponse());
$this->assertSame('{}', $response);
$this->assertTrue($circuitBreaker->isIsolated());
}

$circuitBreaker->reset('https://httpbin.org/get/foo');

$this->assertTrue($circuitBreaker->isClosed());
}

/**
* Return the list of supported circuit breakers
*
Expand Down
32 changes: 32 additions & 0 deletions tests/SymfonyCircuitBreakerEventsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,38 @@ function () {
$this->assertSame('resiliency.opening', $invocations[3]->getParameters()[0]);
}

public function testCircuitBreakerEventsOnIsolationAndResetActions(): void
{
$service = 'https://httpbin.org/get/foobaz';
$circuitBreaker = $this->createCircuitBreaker();

$circuitBreaker->call(
$service,
function () {
return '{}';
}
);

$circuitBreaker->isolate($service);

/**
* The circuit breaker is now isolated and
* the related event has been dispatched
*/
$invocations = $this->spy->getInvocations();
$this->assertCount(5, $invocations);
$this->assertSame('resiliency.isolating', $invocations[4]->getParameters()[0]);

/*
* And now we reset the circuit breaker!
* The related event must be dispatched
*/
$circuitBreaker->reset($service);
$invocations = $this->spy->getInvocations();
$this->assertCount(6, $invocations);
$this->assertSame('resiliency.resetting', $invocations[5]->getParameters()[0]);
}

private function createCircuitBreaker(): CircuitBreaker
{
$system = $this->getSystem();
Expand Down
2 changes: 1 addition & 1 deletion tests/System/MainSystemTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function testGetPlaces(): void
$places = $mainSystem->getPlaces();

$this->assertIsArray($places);
$this->assertCount(3, $places);
$this->assertCount(4, $places);

foreach ($places as $place) {
$this->assertInstanceOf(Place::class, $place);
Expand Down

0 comments on commit 543d886

Please sign in to comment.