Skip to main content

Building a Circuit Breaker in Node.js (Part 2)

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 internalproperties (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 failureThresholdsuccessThreshold, 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.

Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...