miércoles, 25 de junio de 2025

Implementando Circuit Breaker con Redis en PHP para Microservicios

Implementando Circuit Breaker con Redis en PHP para Microservicios

En arquitecturas de microservicios, la resiliencia es crucial. Un fallo en un servicio no debe propagarse y derribar toda la aplicación. El patrón Circuit Breaker ayuda a prevenir esto, actuando como un interruptor que impide llamar a un servicio que está fallando, dando tiempo a que se recupere. Este post muestra cómo implementar un Circuit Breaker en PHP, utilizando Redis para el almacenamiento del estado.

La idea principal es almacenar el estado (cerrado, abierto, medio-abierto) del circuito en Redis. Cada vez que intentamos llamar al servicio, verificamos el estado en Redis. Si está cerrado, la llamada se realiza. Si está abierto, la llamada se evita y se devuelve una respuesta predeterminada o un error. Si está medio-abierto, se permite una llamada para verificar si el servicio se ha recuperado.


<?php

use Predis\Client;

class CircuitBreaker {
    private Client $redis;
    private string $serviceName;
    private int $failureThreshold;
    private int $retryTimeout;

    public function __construct(string $serviceName, int $failureThreshold = 5, int $retryTimeout = 10) {
        $this->redis = new Client(['host' => '127.0.0.1', 'port' => 6379]); // Reemplazar con tu config de Redis
        $this->serviceName = $serviceName;
        $this->failureThreshold = $failureThreshold;
        $this->retryTimeout = $retryTimeout;
    }

    private function getStateKey(): string {
        return "circuit_breaker:{$this->serviceName}:state";
    }

    private function getFailureCountKey(): string {
        return "circuit_breaker:{$this->serviceName}:failure_count";
    }

    private function getLastFailureTimeKey(): string {
        return "circuit_breaker:{$this->serviceName}:last_failure_time";
    }

    public function execute(callable $operation, callable $fallback) {
        $state = $this->redis->get($this->getStateKey()) ?? 'closed';

        if ($state === 'open') {
            $lastFailureTime = (int)$this->redis->get($this->getLastFailureTimeKey());
            if (time() - $lastFailureTime < $this->retryTimeout) {
                return $fallback(); // Cortocircuito: No llamar al servicio
            } else {
                $this->redis->set($this->getStateKey(), 'half_open');
            }
        }

        try {
            $result = $operation(); // Llamar al servicio
            $this->reset(); // Si la llamada fue exitosa, resetear el circuito
            return $result;
        } catch (\Exception $e) {
            $this->recordFailure();
            return $fallback(); // Cortocircuito: Devolver fallback
        }
    }

    private function recordFailure() {
        $failureCount = (int)$this->redis->incr($this->getFailureCountKey());
        $this->redis->set($this->getLastFailureTimeKey(), time());

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

    private function open() {
        $this->redis->set($this->getStateKey(), 'open');
        $this->redis->set($this->getLastFailureTimeKey(), time());
    }

    private function reset() {
        $this->redis->set($this->getStateKey(), 'closed');
        $this->redis->del($this->getFailureCountKey());
        $this->redis->del($this->getLastFailureTimeKey());
    }
}

Este código define la clase CircuitBreaker. El constructor recibe el nombre del servicio, el número máximo de fallos permitidos antes de abrir el circuito (failureThreshold) y el tiempo que el circuito debe permanecer abierto antes de permitir una nueva prueba (retryTimeout). Los métodos getStateKey, getFailureCountKey y getLastFailureTimeKey generan claves únicas para Redis basadas en el nombre del servicio.


<?php
// Ejemplo de uso:
require_once 'vendor/autoload.php'; // Asegúrate de tener predis/predis instalado

$breaker = new CircuitBreaker('UserService', 3, 5); // 3 fallos, reintentar en 5 segundos

$result = $breaker->execute(
    function() {
        // Aquí iría la llamada al microservicio UserService
        // Simulación de un fallo:
        if (rand(0, 5) < 2) {
            throw new Exception("Error al llamar a UserService!");
        }
        return "Respuesta del UserService";
    },
    function() {
        return "Respuesta fallback (UserService no disponible)";
    }
);

echo $result . PHP_EOL;
    

El ejemplo de uso muestra cómo instanciar la clase CircuitBreaker y utilizar el método execute. Este método recibe dos funciones anónimas: la primera es la operación que intenta llamar al microservicio, y la segunda es la función de "fallback" que se ejecuta si el circuito está abierto o si la llamada al microservicio falla.

Con esta implementación, podemos proteger nuestra aplicación PHP de fallos en microservicios, mejorando la resiliencia y la experiencia del usuario.

No hay comentarios:

Publicar un comentario