@fastify/circuit-breaker
Version:
A low overhead circuit breaker for your routes
158 lines (134 loc) • 4.29 kB
JavaScript
'use strict'
const fp = require('fastify-plugin')
const { FifoObject } = require('toad-cache')
const createError = require('@fastify/error')
const OPEN = Symbol('open')
const HALFOPEN = Symbol('half-open')
const CLOSE = Symbol('close')
function fastifyCircuitBreaker (fastify, opts, next) {
opts = opts || {}
const timeout = opts.timeout || 1000 * 10
const resetTimeout = opts.resetTimeout || 1000 * 10
const threshold = opts.threshold || 5
const timeoutErrorMessage = opts.timeoutErrorMessage || 'Timeout'
const circuitOpenErrorMessage = opts.circuitOpenErrorMessage || 'Circuit open'
const onCircuitOpen = opts.onCircuitOpen
const onTimeout = opts.onTimeout
const cache = new FifoObject(opts.cache || 500)
let routeId = 0
fastify.decorateRequest('_cbTime', 0)
fastify.decorateRequest('_cbIsOpen', false)
fastify.decorateRequest('_cbRouteId', 0)
fastify.decorate('circuitBreaker', circuitBreaker)
fastify.addHook('onSend', onSend)
next()
function circuitBreaker (opts) {
opts = opts || {}
const thisRouteId = ++routeId
cache.set(thisRouteId, {
status: CLOSE,
failures: 0,
currentlyRunningRequest: 0,
isResetTimerRunning: false,
threshold: opts.threshold || threshold,
timeout: opts.timeout || timeout,
resetTimeout: opts.resetTimeout || resetTimeout,
onCircuitOpen: opts.onCircuitOpen || onCircuitOpen,
onTimeout: opts.onTimeout || onTimeout
})
return async function beforeHandler (req, reply) {
const route = cache.get(thisRouteId)
if (route.status === OPEN) {
req._cbIsOpen = true
if (route.onCircuitOpen) {
const errorPayload = await route.onCircuitOpen(req, reply)
return reply.send(errorPayload)
}
return reply.send(new CircuitOpenError())
}
if (route.status === HALFOPEN && route.currentlyRunningRequest >= 1) {
req._cbIsOpen = true
if (route.onCircuitOpen) {
const errorPayload = await route.onCircuitOpen(req, reply)
return reply.send(errorPayload)
}
throw new CircuitOpenError()
}
route.currentlyRunningRequest++
req._cbRouteId = thisRouteId
req._cbTime = getTime()
}
}
async function onSend (req, reply, _payload) {
if (req._cbRouteId === 0 || req._cbIsOpen === true) {
return
}
const route = cache.get(req._cbRouteId)
route.currentlyRunningRequest--
if (getTime() - req._cbTime > route.timeout) {
route.failures++
if (route.failures >= route.threshold) {
route.status = OPEN
runTimer(req._cbRouteId)
}
if (route.onTimeout) {
const errorPayload = await route.onTimeout(req, reply)
return errorPayload
}
const err = new TimeoutError()
reply.code(err.statusCode)
throw err
}
if (reply.raw.statusCode < 500) {
route.status = CLOSE
route.failures = 0
return
}
route.failures++
if (route.status === HALFOPEN) {
route.status = OPEN
runTimer(req._cbRouteId)
return
}
if (route.failures >= route.threshold) {
route.status = OPEN
runTimer(req._cbRouteId)
if (route.onCircuitOpen) {
const errorPayload = await route.onCircuitOpen(req, reply)
return errorPayload
}
const err = new CircuitOpenError()
reply.code(err.statusCode)
throw err
}
}
function runTimer (routeId) {
const route = cache.get(routeId)
if (route.isResetTimerRunning === true) return
route.isResetTimerRunning = true
setTimeout(() => {
route.isResetTimerRunning = false
route.status = HALFOPEN
}, route.resetTimeout)
}
const TimeoutError = createError(
'FST_ERR_CIRCUIT_BREAKER_TIMEOUT',
timeoutErrorMessage,
503
)
const CircuitOpenError = createError(
'FST_ERR_CIRCUIT_BREAKER_OPEN',
circuitOpenErrorMessage,
503
)
function getTime () {
const ts = process.hrtime()
return (ts[0] * 1e3) + (ts[1] / 1e6)
}
}
module.exports = fp(fastifyCircuitBreaker, {
fastify: '5.x',
name: '@fastify/circuit-breaker'
})
module.exports.default = fastifyCircuitBreaker
module.exports.fastifyCircuitBreaker = fastifyCircuitBreaker