Dentro de esta publicación voy a presentar un método para implementar el patrón Circuit Breaker en servicios HTTP remotos utilizando .NET.
Para quienes no lo conozcan, el patrón Circuit Breaker es sencillo de describir, algo más complejo de implementar, pero resulta especialmente útil cuando se trabaja con servicios remotos de cualquier tipo. Este patrón fue introducido por primera vez en el libro Release It! publicado por Michael Nygard, cuya lectura es altamente recomendable.
Dado que aquí nos vamos a centrar en la implementación, puede consultarse más información en el libro mencionado o en la página de Microsoft Learn. A grandes rasgos, el patrón responde a un comportamiento similar al que se muestra en el siguiente diagrama:

Al código!
Nota: debemos tener en cuenta que el siguiente código está orientado a proporcionar una clase base para realizar llamadas a servicios HTTP remotos. Podríamos implementar el patrón Circuit Breaker para otros tipos de servicios no HTTP, como una base de datos o un repositorio de archivos, pero ese no es el objetivo de esta publicación.
Ahora, revisemos el código.
public interface ICircuitBreaker {
HttpStatusCode? LastRequestStatusCode { get; }
CircuitBreakerStatus Status { get; }
TimeSpan OpenToHalfOpenThresold { get; }
int HalfOpenToCloseThresold { get; }
CircuitBreakerThresold CloseToOpenThresold { get; }
void Reset();
}
Esta interfaz define las propiedades y métodos básicos del patrón que todos los servicios deben implementar. Posteriormente, se definirá una clase base abstracta que servirá como implementación común, con el fin de evitar la repetición de código en nuestras aplicaciones.
El método Reset() se recomienda como una funcionalidad útil para forzar manualmente el cambio de estado de los servicios desde Open a Close en caso de que queden bloqueados en dicho estado.
public abstract class CircuitBreakerBase : ICircuitBreaker
{
Semaphore _semaphore = new Semaphore(1, 1);
CircuitBreakerStatusController _currentStatusController;
ILogger? _logger;
public CircuitBreakerBase(TimeSpan openToHalfOpenThresold,
int halfOpenToCloseThresold,
CircuitBreakerThresold closeToOpenThresold,
string serviceName, ILogger? logger = null)
{
OpenToHalfOpenThresold = openToHalfOpenThresold;
HalfOpenToCloseThresold = halfOpenToCloseThresold;
CloseToOpenThresold = closeToOpenThresold;
ServiceName = serviceName;
_logger = logger;
_currentStatusController = new CircuitBreakerStatusController(openToHalfOpenThresold, halfOpenToCloseThresold, closeToOpenThresold, StatusChanged);
}
public HttpStatusCode? LastRequestStatusCode => _currentStatusController.LastRequestStatusCode;
public CircuitBreakerStatus Status => _currentStatusController.Status;
public TimeSpan OpenToHalfOpenThresold { get; private set; }
public int HalfOpenToCloseThresold { get; private set; }
public string ServiceName { get; private set; }
public CircuitBreakerThresold CloseToOpenThresold { get; private set; }
protected async Task<CircuitBreakerResponse> ExecuteInCircuitBreaker(Func<Task<HttpResponseMessage>> functionToExecute)
{
_semaphore.WaitOne();
if (!_currentStatusController.CanBeInvoked())
return CircuitBreakerResponse.Opened();
HttpResponseMessage response;
try
{
response = await functionToExecute();
_currentStatusController.CheckResponse(response);
}
catch (Exception excep)
{
_currentStatusController.AddErrorResponse();
throw excep;
}
finally
{
_semaphore.Release();
}
return new CircuitBreakerResponse(response, _currentStatusController.Status);
}
public void Reset() => _currentStatusController.Reset();
private void StatusChanged(CircuitBreakerStatus fromStatus, CircuitBreakerStatus toStatus) {
_logger?.Log(
toStatus != CircuitBreakerStatus.Open ? LogLevel.Information : LogLevel.Critical,
$"[Circuit Breaker] {ServiceName} service moved from {fromStatus} to {toStatus} status."
);
}
}
La clase base para la implementación de la interfaz ICircuitBreaker simplemente añade métodos que permiten a las clases hijas que hereden de ella ejecutar operaciones asíncronas e implementar las propiedades definidas en su clase padre.
Siguiendo los principios SOLID, vamos a implementar esta clase con la única responsabilidad de encolar las llamadas (únicamente cuando el estado sea distinto de Open), sin ocuparse de la gestión de los diferentes estados que requiere el patrón Circuit Breaker para funcionar.
La clase necesita cinco parámetros para operar:
- openToHalfOpenThreshold: un objeto TimeSpan que define el tiempo durante el cual el servicio permanecerá en estado Open antes de pasar a Half-Open.
- halfOpenToCloseThreshold: número de solicitudes exitosas necesarias para pasar del estado Half-Open al estado Close.
- closeToOpenThreshold: objeto combinado que indica el número de solicitudes que deben provocar error en un intervalo de tiempo específico.
- serviceName: un nombre identificador para registrar los mensajes.
- ILogger: instancia utilizada para registrar los cambios dentro del servicio.
Todos estos parámetros se almacenarán en las propiedades del objeto y se pasarán como argumentos al constructor de CircuitBreakerStatusController.
Dentro del método ExecuteInCircuitBreaker se recibirá como parámetro una función asíncrona que debe devolver un objeto HttpResponseMessage. El servicio invocará esta función encolando las distintas llamadas bajo un enfoque FIFO (First In, First Out).
Este comportamiento puede afectar al rendimiento del servicio, por lo que será necesario decidir cuidadosamente cómo implementar esta clase en nuestras aplicaciones.
internal class CircuitBreakerStatusController(
TimeSpan openToHalfOpenThresold,
int halfOpenToCloseThresold,
CircuitBreakerThresold closeToOpenThresold,
Action<CircuitBreakerStatus, CircuitBreakerStatus>? statusChangedCallback = null)
{
CircuitBreakerStatus _status = CircuitBreakerStatus.Close;
public HttpStatusCode? LastRequestStatusCode { get; private set; }
public CircuitBreakerStatus Status { get { CheckCurrentStatus(); return _status; } private set { _status = value; } }
public DateTimeOffset? SetTime { get; private set; }
public FixedSizeQueue<CircuitBreakerStatusControllerRequest> _lastFailedRequestsQueue =
new FixedSizeQueue<CircuitBreakerStatusControllerRequest>(closeToOpenThresold.NumberOfFailures);
int _halfOpenSuccessfulCalls = 0;
#region Public surface
public bool CanBeInvoked()
{
CheckCurrentStatus();
return _status != CircuitBreakerStatus.Open;
}
public void AddErrorResponse()
{
if (_status == CircuitBreakerStatus.HalfOpen)
SetCurrentStatus(CircuitBreakerStatus.Open);
else
{
_lastFailedRequestsQueue.Enqueue(CircuitBreakerStatusControllerRequest.CreateError());
if (ShouldMoveFromCloseToOpen())
SetCurrentStatus(CircuitBreakerStatus.Open);
}
}
public void CheckResponse(HttpResponseMessage responseMessage)
{
LastRequestStatusCode = responseMessage.StatusCode;
if (responseMessage.IsSuccessStatusCode)
AddSuccessfulResponse();
else
CheckErrorResponse(responseMessage);
}
public void Reset() => SetCurrentStatus(CircuitBreakerStatus.Close);
#endregion
#region Private surface
private void SetCurrentStatus(CircuitBreakerStatus status)
{
statusChangedCallback?.Invoke(_status, status);
Status = status;
SetTime = DateTimeOffset.Now;
}
private void MoveToHalfOpen()
{
SetCurrentStatus(CircuitBreakerStatus.HalfOpen);
_halfOpenSuccessfulCalls = 0;
}
private void CheckCurrentStatus()
{
if (_status == CircuitBreakerStatus.Open &&
SetTime.HasValue && SetTime.Value.Add(openToHalfOpenThresold) < DateTimeOffset.Now)
MoveToHalfOpen();
}
private void AddSuccessfulResponse()
{
if (_status == CircuitBreakerStatus.HalfOpen)
_halfOpenSuccessfulCalls++;
if (_halfOpenSuccessfulCalls >= halfOpenToCloseThresold)
SetCurrentStatus(CircuitBreakerStatus.Close);
}
private bool ShouldMoveFromCloseToOpen() => _lastFailedRequestsQueue.Full &&
_lastFailedRequestsQueue.All(it => !it.Status && it.When.Add(closeToOpenThresold.TimeThresold) >= DateTimeOffset.Now);
private void CheckErrorResponse(HttpResponseMessage responseMessage)
{
if (responseMessage.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
SetCurrentStatus(CircuitBreakerStatus.Open);
else if ((int)responseMessage.StatusCode >= 500)
AddErrorResponse();
}
#endregion
internal class CircuitBreakerStatusControllerRequest(DateTimeOffset when, bool status)
{
public DateTimeOffset When => when;
public bool Status => status;
public static CircuitBreakerStatusControllerRequest CreateSuccessful() => new CircuitBreakerStatusControllerRequest(DateTimeOffset.Now, true);
public static CircuitBreakerStatusControllerRequest CreateError() => new CircuitBreakerStatusControllerRequest(DateTimeOffset.Now, false);
}
}
El CircuitBreakerStatusController tendrá la responsabilidad de gestionar los cambios de estado entre los diferentes estados posibles. La lógica aplicada en su interior será la siguiente:
- Si se recibe un error de servidor (código 5xx) o un timeout, el controlador revisará en qué estado se encuentra el servicio en ese momento.
- Si el servicio está en estado Close y ha alcanzado el número de errores permitidos en un intervalo de tiempo predefinido, el estado cambiará a Open.
- Si el servicio está en estado Half-Open, el estado volverá a Open.
- Si se recibe un error 429 (Too Many Requests), el controlador moverá el estado a Open, independientemente del estado en el que se encuentre.
- Cuando el servicio está en estado Open y se alcanza el umbral de tiempo definido, el servicio permitirá realizar llamadas nuevamente, cambiando el estado a Half-Open. Si el tiempo aún no se ha alcanzado, el método CanBeInvoked() devolverá false.
- Cuando el servicio está en estado Half-Open y se reciben tantas respuestas exitosas consecutivas como se hayan predefinido, el controlador moverá el estado a Close.
- Finalmente, la función Reset() moverá el servicio al estado Close. Es una forma de restablecer manualmente el estado del servicio y solo debería invocarse en situaciones específicas.
Implementación
Es momento de implementar algunos ejemplos con este patrón y comenzar a utilizarlo. El servicio remoto elegido para el ejemplo es el Open Data proporcionado por la página zaragoza.es, una API REST gratuita que permite consultar prácticamente todos los indicadores medibles disponibles en la ciudad.
internal interface IWaterTreatmentPlantService : ICircuitBreaker
{
Task<List<OriginByDate>?> GetOriginsByDates();
}
La interfaz heredará de ICircuitBreaker y definirá los métodos personalizados para este servicio. En este ejemplo, se trata de obtener los diferentes orígenes de agua de la ciudad durante un período de tiempo, desglosados por días.
public class WaterTreatmentPlantService : CircuitBreakerBase, IWaterTreatmentPlantService
{
static HttpClient HTTP_CLIENT = new HttpClient();
public WaterTreatmentPlantService(ILogger<WaterTreatmentPlantService> logger) :
base(TimeSpan.FromMinutes(1), 3,
new CircuitBreakerThresold(3, TimeSpan.FromMinutes(5)),
"WaterTreatmentPlan", logger)
{
}
public async Task<List<OriginByDate>?> GetOriginsByDates()
{
var result = new List<OriginByDate>();
var response = await ExecuteInCircuitBreaker(() =>
{
return HTTP_CLIENT.GetAsync("https://www.zaragoza.es/sede/servicio/potabilizadora/procedencia.json");
});
if (response.IsSuccess)
{
var stringData = await response.ResponseMessage!.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<object[][]>(stringData);
Dictionary<int, string> headers = new();
for (int i = 0; i < data[0].Length; i++)
{
var header = data[0][i];
var stringName = ((JsonElement)header).GetString();
if (stringName != "X")
{
headers.Add(i, stringName);
}
}
data.Skip(1).ToList().ForEach(it =>
{
if (DateTime.TryParseExact(((JsonElement)it[0]).GetString(), "dd-MM-yyyy", null, System.Globalization.DateTimeStyles.None, out var dt))
{
result.Add(new OriginByDate
{
Date = dt,
Distribution = headers.ToDictionary(head => head.Value, head => ((JsonElement)it[head.Key]).GetInt16())
});
}
});
}
return result;
}
}
El método heredará de la clase base CircuitBreakerBase y posteriormente implementará la interfaz IWaterTreatmentPlantService. Como podemos observar, obtendremos la respuesta del servicio utilizando el método protegido ExecuteInCircuitBreaker, definido dentro de la clase base, que será gestionado por el patrón.
El resto del método se comporta como debería hacerlo cualquier llamada a un endpoint HTTP RESTful remoto: obtener la respuesta en formato cadena y deserializarla en un objeto legible por la aplicación.
Nota: Este es un ejemplo pequeño y muy sencillo. En un entorno real (RealLife™) deberíamos implementar estas clases base con mayor cuidado, teniendo en cuenta lo siguiente:
- Cada implementación de la clase base CircuitBreakerBase actúa como una cola, por lo que será necesario dividir nuestra clase entre los distintos métodos, fragmentos (shards) o con la granularidad que requiera la interacción con la API remota.
- Esta implementación ofrece libertad para utilizar HttpClient de la forma que se desee. Sin embargo, existen directrices proporcionadas por Microsoft que conviene conocer y seguir.
- Las clases de implementación deberían actuar como instancias singleton dentro del ciclo de vida de la aplicación, con el fin de compartir el estado entre todos los hilos de ejecución. Para ello, siempre recomiendo utilizar un motor de Inyección de Dependencias (DI) en lugar de crear objetos estáticos dentro de la aplicación.
Reflexiones finales
Este patrón constituye un buen punto de partida cuando trabajamos con servicios remotos en nuestras aplicaciones, pero debería combinarse con otros patrones como Retry, Health Endpoint Monitoring o el Dependency Injection Pattern para lograr una solución más robusta y completa.