Welcome to Part 2 in our series on building your own circuit breaker in Node.js. In Part 1, Building a Circuit Breaker in Node.js, we built a starter version that handles the core states of a circuit breaker. In this article, we will add configurability, manual overrides, and fallback request support. You can find the complete code for each example here.
Make it configurable
In part 1, we put all the thresholds and timeouts directly into the breaker. It would be better to make each instance of the circuit breaker configurable. Our result will look like this:
const options = {
failureThreshold: 3,
successThreshold: 2,
timeout: 6000
}
const breaker = new CircuitBreaker(request, options)
To make this happen we need to adjust our constructor in CircuitBreaker
.
class CircuitBreaker {
/* 1 */
constructor(request, options = {}) {
/* 2 */
const defaults = {
failureThreshold: 3,
successThreshold: 2,
timeout: 6000
}
Object.assign(this, defaults, options, {
/* 3 */
request: request,
state: "CLOSED",
failureCount: 0,
successCount: 0,
nextAttempt: Date.now()
})
}
//...
}
Above (1), the constructor now takes an options
argument in addition to the request. Next, we declare some defaults (2) for the user-configurable properties. Object.assign
is then used to add the defaults, the user options, and our internal properties (3) plus the request to this
. Why all the mixing of objects? We want to make sure users cannot override our internal properties. The result is a version of CircuitBreaker
that behaves like our original, but now accepts options for failureThreshold
, successThreshold
, and timeout
on instantiation.
Note: You could also use class private properties instead of the method above, but support is still a bit new or spotty.
The configurable code of our circuit breaker can be found here.
Add manual state overrides
Sometimes it can be useful to offer manual overrides for your circuit breaker. Maybe you're dealing with a finicky API that occasionally needs to be retried out of the flow of the circuit. Perhaps new information comes in from a related source–like a webhook–that makes you want to "break out" of the current state.
To do this, we will add helper methods to our CircuitBreaker
class that swap the state, and reset any properties that affect the logic.
class CircuitBreaker {
//...
open() {
this.state = "OPEN"
this.nextAttempt = Date.now() + this.timeout
}
close() {
this.successCount = 0
this.failureCount = 0
this.state = "CLOSED"
}
half() {
this.state = "HALF"
}
//...
}
You can replace some portions of fail
and success
with these new helpers to reduce some repetition. More importantly, they now give us access to breaker.open()
, breaker.close()
, and breaker.half()
in our instances of the circuit breaker. This way your app can have influence over the state from the outside.
The code with manual overrides for the circuit breaker can be found here.
Fallback functionality
Imagine an API you use or perhaps a regional resource (AWS East vs West) is having problems. You'd like your code to adapt and call an alternate resource. We talk about the power of switching to a fallback in Consuming Webhooks with Node.js and Express.
Let's add a fallback to CircuitBreaker
. First, we will create a new test request. In part 1 we had unstableRequest
in our test.js
file. This is still our main resource, but let's create an additional function to call if a problem happens with our main resource.
function expensiveResource() {
return new Promise((resolve, reject) => {
resolve({ data: "Expensive Fallback Successful" })
})
}
This request is reliable, but more costly than our unstableRequest
. While we're in test.js
, make a change to the instantiation of breaker
:
const breaker = new CircuitBreaker(unstableRequest, expensiveResource)
// Alternately, if you set up the configurability from earlier
const breaker = new CircuitBreaker(unstableRequest, {
fallback: expensiveResource,
failureThreshold: 2
// ...etc
})
Now move back to CircuitBreaker.js
. The first thing we need to do is accept the new argument (or property on the options object).
// Version 1. If using the code without configuration (from Part 1)
class CircuitBreaker {
constructor(request, fallback = null) {
/* ... */
}
/* ... */
}
// Version 2. If using a configurable "options" argument
class CircuitBrekaer {
constructor(request, options) {
const defaults = {
failureThreshold: 3,
successThreshold: 2,
timeout: 6000,
fallback: null
}
Object.assign(this, defaults, options, {
/* ... */
})
}
/* ... */
}
This adds the fallback request just like any other argument. To help with our logic later, we also set it's default value to null
in case it isn't set by the user.
Next, we'll create a method on CircuitBreaker
to attempt the fallback request.
class CircuitBreaker {
/* ... */
async tryFallback() {
// Attempting fallback request
try {
const response = await this.fallback()
return response
} catch (err) {
return err
}
}
}
We will use this method when the original request fails. It won't affect the circuit breaker itself, since the original resource is still having problems. That is why we won't run the fallback response through the success
or fail
flows. Let's call tryFallback
when a request fails.
fail(err) {
this.failureCount++
if (this.failureCount >= this.failureThreshold) {
this.state = "OPEN"
this.nextAttempt = Date.now() + this.timeout
}
this.status("Failure")
if (this.fallback) return this.tryFallback() /* 1 */
return err
}
Everything above is the same as our original code, with the exception of the line at 1. It checks if this.fallback
has been set, and if so it will return our newly created tryFallback
method.
The use of return
in these code blocks is important. It allows us to pass the result back up to the original function that started the request.
The full code for the circuit breaker with fallback functionality can be found here.
Resilient and ready for anything
With everything in place, you now have the foundation to put together a strategy that resilient code patterns to manage the unreliability of the third-party APIs or resources your applications rely on.
While we've gone through the foundations in Part 1, and some advanced features in this article, building your own can still be challenging. Depending on the application you're building and the stack you're working in, using an off the shelf circuit breaker like opossum for Node.js, circuit_breaker for Ruby, go-circuitbreaker for Go, or circuitbreaker for python might be a good choice.
At Bearer we're building a product that handles much of the work required to shield your app from third-party API failures and make it more resilient. This includes features like retries on specific response types, and much more to come in the future. Have a look, and get started using Bearer today.
Like this article and want to see more? Connect with us @BearerSH and check The Bearer Blog for more articles like this from the team at Bearer.
📢 Building a Circuit Breaker in Node.js (Part 2) was originally published on The Bearer blog.
Top comments (0)