scatter-gather-circuit-breaker

Circuit Breaker module for the scatter-gather library.

Usage no npm install needed!

<script type="module">
  import scatterGatherCircuitBreaker from 'https://cdn.skypack.dev/scatter-gather-circuit-breaker';
</script>

README

scatter-gather-circuit-breaker

Circuit Breaker module for the scatter-gather library.

This Circuit Breaker package provides a module that can be plugged into the scatter-gather package. It allows you to specify a sequential list of function calls that will each be wrapped in a circuit breaker.

You can also optionally configure a cache that will be used to cache the responses from your function outputs.

Usage

Basic usage

You can instantiate a new CircuitBreaker in the following manner:

const CircuitBreaker = require('scatter-gather-circuit-breaker').CircuitBreaker;
const cb = new CircuitBreaker({
    phases: [{
        name: 'myApiCall',
        impl: async (message, prevResults) => {
            return 'I made some API call and here is the results'
        }
    }]
})

You can specify more than one function to be invoked:

const CircuitBreaker = require('scatter-gather-circuit-breaker').CircuitBreaker;
const cb = new CircuitBreaker({
    phases: [
        {
            name: 'myApiCall',
            impl: async (message, prevResults) => {
                return 'I made some API call and here is the results'
            }
        },
        {
            name: 'myApiCall2',
            impl: async (message, prevResults) => {
                const firstCallResult = prevResults.myApiCall
                // Do something with the firstCallResult
                return 'Here are some more results'
            }
        }
    ]
})

The prevResults object contains the results from the phases that were sequentially executed before the current phase.

Once you have instantiated the circuit breaker object, you can call the processMessage function to execute the process of calling each phase:

const requestMessage = {
    id: 'FakeId',
    type: 'SomeMessageType',
    key: 'ByuId',
    body: {
        some: 'param'
    }
}
cb.processMessage(requestMessage)
    .then(responseMessage => {
        // Do something with the response
    })

The returned object from processMessage looks like the following:

{
    id: 'FakeId',
    type: 'SomeMessageType',
    key: 'ByuId',
    response: {
        item: 'Whatever was returned from the last phase of your circuit breaker'
        cachedLastUpdated: new Date() // If the returned result came from the cache, this tells when the result was cached. If it did not come from the cache, it will be undefined.
    },
    error: new CircuitBreakerError // If there were any errors during the process, this will be set, otherwise it will be undefined
}

The CircuitBreakerError object wraps the possible errors that could occur in both the cache and the api calls. You can access the original errors like this:

error.apiError  // The error returned by the API call (if any)
error.cacheError // The error returned by the cache (if any)

Using a cache

You can optionally specify a cache that the module should use to cache your responses. You can currently use one of two cache types:

  • Redis
  • DynamoDB

Redis

Here is an example of using a Redis cache:

const CircuitBreaker = require('scatter-gather-circuit-breaker').CircuitBreaker;
const cb = new CircuitBreaker({
    cache: {
        redis: {
            host: 'localhost',
            port: 6379
        }
    },
    phases: [
        {
            name: 'myApiCall',
            cacheTtl: 0, // Cache forever
            impl: async (message, prevResults) => {
                return 'I made some API call and here is the results'
            }
        },
        {
            name: 'myApiCall2',
            cacheTtl: 60, // Cache for 60 secons
            impl: async (message, prevResults) => {
                const firstCallResult = prevResults.myApiCall
                // Do something with the firstCallResult
                return 'Here are some more results'
            }
        }
    ]
})

Note that the cache property is where you specify the Redis configuration. This will be used as a look-aside cache by default.

Note that the output of each phase will be cached separately, so each phase allows you to specify a cacheTtl parameter that specifies how long you want to cache the output for that phase.

DynamoDB

You can also use DynamoDB as a cache backend:

const CircuitBreaker = require('scatter-gather-circuit-breaker').CircuitBreaker;
const cb = new CircuitBreaker({
    cache: {
        dynamo: {
            tableName: 'dsw88-test-cache-table',
            partitionKey: 'byuid',
            sortKey: 'phase',
            region: 'us-west-2'
        }  
    },
    phases: [
        {
            name: 'myApiCall',
            cacheTtl: 0, // Cache forever
            impl: async (message, prevResults) => {
                return 'I made some API call and here is the results'
            }
        },
        {
            name: 'myApiCall2',
            cacheTtl: 60, // Cache for 60 secons
            impl: async (message, prevResults) => {
                const firstCallResult = prevResults.myApiCall
                // Do something with the firstCallResult
                return 'Here are some more results'
            }
        }
    ]
})

Note that when using DyanmoDB as a cache, you must provide the table name and region where the table is located, as well as the paritition and range key attributes configured on the table.

Optional parameters

  • cachingStrategy: choose between 'look-aside', and 'read-through'. 'look-aside' is the default.
    • 'look-aside' calls both the cache and the api simultaneously and returns whichever returns first.
    • 'read-through' calls the cache first and then the api if necessary.
  • passThrough: if you want to only cache some responses and not others, specify a parameter that if found in the response, the response will not be cached.
const CircuitBreaker = require('scatter-gather-circuit-breaker').CircuitBreaker;
const cb = new CircuitBreaker({
    cache: {
        cachingStrategy: 'read-through', // prefer the cache over the api
        passThrough: 'validation_response',
        dynamo: {
            tableName: 'dsw88-test-cache-table',
            partitionKey: 'byuid',
            sortKey: 'phase',
            region: 'us-west-2'
        }
    },
    phases: [
        {
            name: 'myApiCall',
            cacheTtl: 0, // Cache forever
            impl: async (message, prevResults) => {
                if (message.type === 'someMessageType') {
                  return 'This response will be cached'
                }
                return {'validation_response': 'This response will not be cached'}
            }
        },
        {
            name: 'myApiCall2',
            cacheTtl: 60, // Cache for 60 secons
            impl: async (message, prevResults) => {
                const firstCallResult = prevResults.myApiCall
                // Check the firstCallResult for the passThrough variable
                if (firstCallResult.validation_response) {
                  return "validation_response found"
                }
                return "Here are some more results"
            }
        }
    ]
})

Configuring the circuit breaker

This module takes care of wrapping your functions in a circuit breaker for you. You can change the behavior of this circuit breaker when creating your instance:

const CircuitBreaker = require('scatter-gather-circuit-breaker').CircuitBreaker;
const cb = new CircuitBreaker({
    circuitBreaker: {
        timeout: 10000, // The time in milliseconds to wait for responses before throwing a timeout error
        errorThresholdPercentage: 50, // The percent of responses that need to fail before the circuit breaker fails open.
        resetTimeout: 30000 // The time in milliseconds to wait when a circuit breaker fails open before trying again to talk to the API.
    },
    phases: [{
        name: 'myApiCall',
        impl: async (message, prevResults) => {
            return 'I made some API call and here is the results'
        }
    }]
})

Only handling certain messages

By default the CircuitBreaker will process all messages sent to it by the scatter-gather framework. You can configure a set of message types that you only want to handle, and all others will be ignored by the CircuitBreaker:

const CircuitBreaker = require('scatter-gather-circuit-breaker').CircuitBreaker;
const cb = new CircuitBreaker({
    processedMessageTypes: [
        'someMessageType1',
        'someMessageType2'
    ],
    phases: [{
        name: 'myApiCall',
        impl: async (message, prevResults) => {
            return 'I made some API call and here is the results'
        }
    }]
})