En esta publicación, voy a compartir una forma de implementar un patrón de Circuit Breaker para servicios HTTP remotos utilizando .NET.
Si no lo conoces, el patrón de Circuit Breaker es un patrón muy simple de describir, un poco más difícil de implementar, pero útil cuando trabajas con servicios remotos (de cualquier tipo). Este patrón fue descrito por primera vez en el libro Release It! publicado por Michael Nygard, una lectura altamente recomendada.
Como vamos a centrarnos en la implementación aquí, puedes leer más sobre ello en el libro que mencioné anteriormente o en la página de Microsoft Learn. Pero en resumen, el patrón debería responder a un comportamiento como el que se muestra en el siguiente diagrama:

Let’s code
Nota: Debemos tener en cuenta que el siguiente código se centra en proporcionar una clase base para llamar a servicios HTTP remotos. Podríamos implementar un patrón de Circuit Breaker para servicios no HTTP, como una base de datos o un repositorio de archivos, pero ese no es el objetivo de esta publicación.
Echemos un vistazo al 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 para el patrón que todos los servicios deberían implementar. Más adelante, definiremos una clase base abstracta que servirá como base de implementación para evitar repetir el mismo código en nuestras aplicaciones. El método Reset()
es un método recomendado y útil para forzar manualmente el cambio de nuestros servicios del estado Open
al estado Close
si se quedan atascados en él.
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 una interfaz ICircuitBreaker
simplemente añade métodos para permitir que aquellos que la hereden, ejecuten operaciones asíncronas e implementen las propiedades definidas en su padre.
Como estamos tratando de seguir los principios SOLID, vamos a implementar esta clase con la única responsabilidad de encolar las llamadas (solo cuando el estado es diferente de Open), sin preocuparnos por la gestión de los diferentes estados que necesita el patrón Circuit Breaker para operar.
La clase necesita cinco parámetros para funcionar:
openToHalfOpenThresold
: unTimeSpan
con la cantidad de tiempo que el servicio estará en el estado Open antes de pasar al estado Half-Open.halfOpenToCloseThresold
: número de solicitudes exitosas antes de pasar del estado Half-Open al estado Close.closeToOpenThresold
: objeto combinado con el número de solicitudes que deberían causar error en el tiempo especificado.serviceName
: un nombre identificador para registrar los mensajes.ILogger
: instancia para registrar los cambios dentro del servicio.
Todos estos parámetros se guardarán dentro de las propiedades del objeto y se pasarán por parámetro al constructor de CircuitBreakerStatusController
.
Dentro del método ExecuteInCircuitBreaker
, pasaremos una función asíncrona como parámetro que debería devolver un objeto HttpResponseMessage
. El servicio llamará a esta función encolando las diferentes llamadas de manera FIFO (First In, First Out). Esto podría afectar al rendimiento de nuestro servicio, por lo que debemos 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 él será la siguiente:
- Si recibimos 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 posibles errores en una cantidad de tiempo predefinida, el estado se cambiará aOpen
. - Si el servicio está en estado
Half-Open
, el estado se cambiará nuevamente aOpen
.
- Si el servicio está en estado
- Si recibimos un error 429 (Too Many Requests), el controlador moverá el estado a
Open
, sin importar en qué estado se encuentre. - Cuando el servicio está en estado
Open
y se alcanza el umbral de tiempo, el servicio permitirá realizar llamadas nuevamente cambiando el estado aHalf-Open
. Si no se ha alcanzado el tiempo, el métodoCanBeInvoked()
devolveráfalse
. - Cuando el servicio está en estado
Half-Open
y recibimos tantas respuestas exitosas consecutivas como hemos predefinido, el controlador mueve el estado aClose
. - Finalmente, el método
Reset()
moverá el servicio al estadoClose
. Es una forma de restablecer manualmente el estado del servicio y solo debe invocarse en ciertas situaciones.
Implementación
Es hora de implementar algunos ejemplos con este patrón y comenzar a usarlo. El servicio remoto que elegí para mi ejemplo es el Open Data proporcionado por la página Una API REST gratuita para conocer casi todos los indicadores medibles que tenemos en mi ciudad.
internal interface IWaterTreatmentPlantService : ICircuitBreaker
{
Task<List<OriginByDate>?> GetOriginsByDates();
}
La interfaz heredará de la interfaz ICircuitBreaker
y definirá los métodos personalizados para este servicio. En este ejemplo, obtendremos los diferentes orígenes del agua para la ciudad en un período de tiempo, dividido 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 luego implementará la interfaz IWaterTreatmentPlantService
. Como podemos ver, obtendremos la respuesta del servicio utilizando el método protegido ExecuteInCircuitBreaker
definido en la clase base, que será gestionado por el patrón.
El resto del método actúa de la misma manera que lo haríamos al llamar a un endpoint HTTP RESTful remoto, obteniendo la respuesta en formato de cadena y serializándola en un objeto legible por la aplicación.
Nota: Este es un ejemplo pequeño y muy simple. En la vida real, deberíamos implementar estas clases base cuidadosamente, teniendo en cuenta lo siguiente:
- Cada implementación de la clase base
CircuitBreakerBase
actúa como una cola, por lo que necesitaremos dividir nuestra clase entre los diferentes métodos, fragmentos o con la granularidad que necesitemos para llamar a nuestra API remota. - Esta implementación te da la libertad de usar la implementación de
HttpClient
como desees. Pero hay algunas proporcionadas por Microsoft que deberías conocer y seguir. - Las clases de implementación deben actuar como instancias singleton dentro del ciclo de vida de nuestra aplicación para compartir el estado entre todos los hilos de la aplicación. Siempre recomiendo para eso usar un motor de inyección de dependencias (DI) en lugar de crear objetos estáticos dentro de la aplicación.
Conclusiones
Este patrón es un buen punto de partida cuando trabajamos con servicios remotos en nuestras aplicaciones, pero debe combinarse con otros patrones como Retry, Health Endpoint Monitoring o el patrón de inyección de dependencias.
The full example and the project could be viewed here in Github.