Implementando un Circuit Breaker con Redis en PHP
El patrón Circuit Breaker es esencial para la construcción de sistemas resilientes, especialmente en microservicios y arquitecturas distribuidas. Permite detener la cascada de fallos cuando un servicio dependiente falla, evitando la degradación del sistema en su conjunto. En este post, exploraremos cómo implementar un Circuit Breaker robusto utilizando Redis como almacenamiento para la lógica de estado en PHP.
<?php
use Predis\Client;
class CircuitBreaker {
private Client $redis;
private string $serviceName;
private int $failureThreshold;
private int $recoveryTimeout;
public function __construct(string $serviceName, int $failureThreshold, int $recoveryTimeout, array $redisConfig = []) {
$this->serviceName = $serviceName;
$this->failureThreshold = $failureThreshold;
$this->recoveryTimeout = $recoveryTimeout;
$this->redis = new Client($redisConfig);
}
private function getKey(string $suffix): string {
return "circuit_breaker:{$this->serviceName}:{$suffix}";
}
public function isAvailable(): bool {
$state = $this->redis->get($this->getKey('state')) ?? 'closed';
if ($state === 'open') {
$retryAfter = $this->redis->get($this->getKey('retry_after'));
if ($retryAfter === null || time() >= (int)$retryAfter) {
$this->redis->set($this->getKey('state'), 'half_open'); // Intentamos recovery
return true;
}
return false;
}
return true; // El circuito está cerrado o medio abierto
}
public function recordSuccess(): void {
$failures = (int)($this->redis->get($this->getKey('failures')) ?? 0);
if ($failures > 0) {
$this->redis->set($this->getKey('failures'), 0);
}
if ($this->redis->get($this->getKey('state')) === 'half_open') {
$this->redis->set($this->getKey('state'), 'closed'); // Éxito en half_open
}
}
public function recordFailure(): void {
$failures = (int)($this->redis->incr($this->getKey('failures')));
if ($failures >= $this->failureThreshold) {
$this->redis->set($this->getKey('state'), 'open');
$this->redis->set($this->getKey('retry_after'), time() + $this->recoveryTimeout);
}
}
}
// Ejemplo de uso:
// $breaker = new CircuitBreaker('my_service', 5, 30, ['host' => '127.0.0.1']);
Este código define una clase CircuitBreaker
que utiliza Redis para persistir su estado. El constructor recibe el nombre del servicio, el umbral de fallos (failureThreshold
), el tiempo de espera para la recuperación (recoveryTimeout
) y opcionalmente, la configuración de Redis.
<?php
// Continua del ejemplo anterior
// Función para interactuar con el servicio protegido
function callService(CircuitBreaker $breaker): string {
if ($breaker->isAvailable()) {
try {
// Lógica para llamar al servicio externo
$response = simulateExternalService(); // Simulamos una llamada al servicio
$breaker->recordSuccess();
return $response;
} catch (\Exception $e) {
$breaker->recordFailure();
throw $e; // Re-lanzamos la excepción
}
} else {
return "Service unavailable (Circuit Breaker Open)";
}
}
function simulateExternalService(): string {
// Simula un fallo aleatorio
if (rand(0, 5) === 0) {
throw new \Exception("Simulated Service Failure");
}
return "Service Response";
}
// Ejemplo de uso completo:
try {
$breaker = new CircuitBreaker('my_service', 3, 10, ['host' => '127.0.0.1']);
echo callService($breaker) . PHP_EOL;
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . PHP_EOL;
}
La función isAvailable()
verifica el estado del circuito. Si está "cerrado" o "medio abierto", permite la llamada al servicio. Si está "abierto", comprueba si ha pasado el tiempo de espera para la recuperación (recoveryTimeout
). Si el intento de recuperación en estado "medio abierto" tiene éxito (recordSuccess()
), el circuito vuelve a estar "cerrado". Si falla (recordFailure()
), el circuito permanece "abierto" y se actualiza el tiempo de reintento.
Este enfoque centraliza la lógica del Circuit Breaker y ofrece una forma consistente y eficiente de proteger las dependencias en una aplicación PHP, utilizando Redis para el almacenamiento distribuido del estado.
No hay comentarios:
Publicar un comentario