Skip to content

[FEATURE] Add CircuitBreaker class #85

@guibranco

Description

@guibranco

Description

Add a class that handles open/half-open/close state for a simple CircuitBreaker.
This class depends upon #84 to manage state

  • The settings/options should be configured by the constructor or via custom method (failureThreshold, resetTimeout)
  • MemoryCache ([FEATURE] Add MemoryCache class #84) should be a constructor dependency

Tech notes

Simple example

class CircuitBreaker
{
    private $state = 'closed';
    private $failureThreshold = 5;
    private $resetTimeout = 120;
    private $failureCount = 0;
    private $lastFailureTime = null;
    private $memoryCache;

    public function __construct()
    {
        $this->memoryCache = new MemoryCache();
        $this->loadState();
    }

    public function __destruct()
    {
        $this->saveState();
    }

    private function saveState()
    {
        $state = array(
            "state" => $this->state,
            "failureCount" => $this->failureCount,
            "lastFailureTime" => $this->lastFailureTime
        );
        $this->memoryCache->writeJsonInMemory($state);
    }

    private function loadState()
    {
        $state = $this->memoryCache->readJsonInMemory();
        if (!isset($state["state"]) || !isset($state["failureCount"]) || !isset($state["lastFailureTime"])) {
            return;
        }

        $this->state = $state["state"];
        $this->failureCount = $state["failureCount"];
        $this->lastFailureTime = $state["lastFailureTime"];
    }

    public function execute(callable $operation)
    {

        if ($this->state === 'open' && $this->isTimeoutReached()) {
            $this->state = 'half-open';
        }

        if ($this->state === 'closed' || $this->state === 'half-open') {
            try {
                $result = $operation();
                $this->reset();
                return $result;
            } catch (\Exception $e) {
                $this->handleFailure();
                throw $e;
            } finally {
                $this->saveState();
            }
        }

        throw new CircuitBreakerOpenException("Circuit breaker is open until " . date('Y-m-d H:i:s', $this->lastFailureTime + $this->resetTimeout) . " (in " . ($this->lastFailureTime + $this->resetTimeout - time()) . " seconds)");
    }

    private function handleFailure()
    {
        $this->failureCount++;
        $this->lastFailureTime = time();

        if ($this->failureCount >= $this->failureThreshold) {
            $this->state = 'open';
        }
    }

    private function isTimeoutReached()
    {
        return time() - $this->lastFailureTime >= $this->resetTimeout;
    }

    private function reset()
    {
        $this->failureCount = 0;
        $this->lastFailureTime = null;
        $this->state = 'closed';
    }
}

class CircuitBreakerOpenException extends \Exception
{
}

Additional information

⚠️ 🚨 Add documentation and tests

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestgitautoGitAuto label to trigger the app in a issue.good first issueGood for newcomershacktoberfestParticipation in the Hacktoberfest eventhelp wantedExtra attention is needed📝 documentationTasks related to writing or updating documentation🕓 medium effortA task that can be completed in a few hours🛠 WIPWork in progress🧪 testsTasks related to testing

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions