Circuit Breaker
Why Are They Used?
A circuit breaker is used to provide stability and prevent cascading failures in distributed systems. These should be used in conjunction with judicious timeouts at the interfaces between remote systems to prevent the failure of a single component from bringing down all components.
As an example, we have a web application interacting with a remote third party web service. Let's say the third party has oversold their capacity and their database melts down under load. Assume that the database fails in such a way that it takes a very long time to hand back an error to the third party web service. This in turn makes calls fail after a long period of time. Back to our web application, the users have noticed that their form submissions take much longer seeming to hang. Well the users do what they know to do which is use the refresh button, adding more requests to their already running requests. This eventually causes the failure of the web application due to resource exhaustion. This will affect all users, even those who are not using functionality dependent on this third party web service.
Introducing circuit breakers on the web service call would cause the requests to begin to fail-fast, letting the user know that something is wrong and that they need not refresh their request. This also confines the failure behavior to only those users that are using functionality dependent on the third party, other users are no longer affected as there is no resource exhaustion. Circuit breakers can also allow savvy developers to mark portions of the site that use the functionality unavailable, or perhaps show some cached content as appropriate while the breaker is open.
The Akka.NET library provides an implementation of a circuit breaker called Akka.Pattern.CircuitBreaker
which has the behavior described below.
What Do They Do?
- During normal operation, a circuit breaker is in the
Closed
state:- Exceptions or calls exceeding the configured
СallTimeout
increment a failure counter - Successes reset the failure count to zero
- When the failure counter reaches a
MaxFailures
count, the breaker is tripped intoOpen
state
- Exceptions or calls exceeding the configured
- While in
Open
state:- All calls fail-fast with a
OpenCircuitException
- After the configured
ResetTimeout
, the circuit breaker enters aHalf-Open
state
- All calls fail-fast with a
- In
Half-Open
state:- The first call attempted is allowed through without failing fast
- All other calls fail-fast with an exception just as in
Open
state - If the first call succeeds, the breaker is reset back to
Closed
state and theResetTimeout
is reset - If the first call fails, the breaker is tripped again into the
Open
state (as for exponential backoff circuit breaker, theResetTimeout
is multiplied by the exponential backoff factor)
- State transition listeners:
- Callbacks can be provided for every state entry via
OnOpen
,OnClose
, andOnHalfOpen
- These are executed in the
ExecutionContext
provided.
- Callbacks can be provided for every state entry via
Examples
Initialization
Here's how a CircuitBreaker
would be configured for:
- 5 maximum failures
- a call timeout of 10 seconds
- a reset timeout of 1 minute
public class DangerousActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public DangerousActor()
{
var breaker = new CircuitBreaker(
Context.System.Scheduler,
maxFailures: 5,
callTimeout: TimeSpan.FromSeconds(10),
resetTimeout: TimeSpan.FromMinutes(1)).OnOpen(NotifyMeOnOpen);
}
private void NotifyMeOnOpen()
{
_log.Warning("My CircuitBreaker is now open, and will not close for one minute");
}
}
Call Protection
Here's how the CircuitBreaker
would be used to protect an asynchronous
call as well as a synchronous one:
public class DangerousActorCallProtection : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public DangerousActorCallProtection()
{
var breaker = new CircuitBreaker(
Context.System.Scheduler,
maxFailures: 5,
callTimeout: TimeSpan.FromSeconds(10),
resetTimeout: TimeSpan.FromMinutes(1)).OnOpen(NotifyMeOnOpen);
var dangerousCall = "This really isn't that dangerous of a call after all";
Receive<string>(str => str.Equals("is my middle name"), _ =>
{
var sender = this.Sender;
breaker.WithCircuitBreaker(_ => Task.FromResult(dangerousCall)).PipeTo(sender);
});
Receive<string>(str => str.Equals("block for me"), _ =>
{
Sender.Tell(breaker.WithSyncCircuitBreaker(() => dangerousCall));
});
}
private void NotifyMeOnOpen()
{
_log.Warning("My CircuitBreaker is now open, and will not close for one minute");
}
}
dangerousActor.Tell("is my middle name");
// This really isn't that dangerous of a call after all
dangerousActor.Tell("block for me");
dangerousActor.Tell("block for me");
dangerousActor.Tell("block for me");
dangerousActor.Tell("block for me");
dangerousActor.Tell("block for me");
// My CircuitBreaker is now open, and will not close for one minute
// My CircuitBreaker is now half-open
dangerousActor.Tell("is my middle name");
// My CircuitBreaker is now closed
// This really isn't that dangerous of a call after all
Tell Pattern
The above Call Protection
pattern works well when the return from a remote call is wrapped in a Future
. However, when a remote call sends back a message or timeout to the caller Actor
, the Call Protection
pattern is awkward. CircuitBreaker doesn't support it natively at the moment, so you need to use below low-level power-user APIs, succeed
and fail
methods, as well as isClose
, isOpen
, isHalfOpen
.
Note
The below examples doesn't make a remote call when the state is HalfOpen
. Using the power-user APIs, it is your responsibility to judge when to make remote calls in HalfOpen
.
protected override void OnReceive(object message)
{
switch (message)
{
case "call" when _breaker.IsClosed:
_recipient.Tell("message");
break;
case "response":
_breaker.Succeed();
break;
case Exception _:
_breaker.Fail();
break;
case ReceiveTimeout _:
_breaker.Fail();
break;
}
}