opossum
Version:
A fail-fast circuit breaker for promises and callbacks
1,250 lines (1,180 loc) • 77.7 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["circuitBreaker"] = factory();
else
root["circuitBreaker"] = factory();
})(self, () => {
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/***/ ((module, exports, __webpack_require__) => {
"use strict";
module.exports = exports = __webpack_require__(/*! ./lib/circuit */ "./lib/circuit.js");
/***/ }),
/***/ "./lib/cache.js":
/*!**********************!*\
!*** ./lib/cache.js ***!
\**********************/
/***/ ((module, exports) => {
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/**
* Simple in-memory cache implementation
* @class MemoryCache
* @property {Map} cache Cache map
*/
var MemoryCache = /*#__PURE__*/function () {
function MemoryCache(maxEntries) {
_classCallCheck(this, MemoryCache);
this.cache = new Map();
this.maxEntries = maxEntries !== null && maxEntries !== void 0 ? maxEntries : Math.pow(2, 24) - 1; // Max size for Map is 2^24.
}
/**
* Get cache value by key
* @param {string} key Cache key
* @return {any} Response from cache
*/
return _createClass(MemoryCache, [{
key: "get",
value: function get(key) {
var cached = this.cache.get(key);
if (cached) {
if (cached.expiresAt > Date.now() || cached.expiresAt === 0) {
return cached.value;
}
this.cache["delete"](key);
}
return undefined;
}
/**
* Set cache key with value and ttl
* @param {string} key Cache key
* @param {any} value Value to cache
* @param {number} ttl Time to live in milliseconds
* @return {void}
*/
}, {
key: "set",
value: function set(key, value, ttl) {
// Evict first entry when at capacity - only when it's a new key.
if (this.cache.size === this.maxEntries && this.get(key) === undefined) {
this.cache["delete"](this.cache.keys().next().value);
}
this.cache.set(key, {
expiresAt: ttl,
value: value
});
}
/**
* Delete cache key
* @param {string} key Cache key
* @return {void}
*/
}, {
key: "delete",
value: function _delete(key) {
this.cache["delete"](key);
}
/**
* Clear cache
* @returns {void}
*/
}, {
key: "flush",
value: function flush() {
this.cache.clear();
}
}]);
}();
module.exports = exports = MemoryCache;
/***/ }),
/***/ "./lib/circuit.js":
/*!************************!*\
!*** ./lib/circuit.js ***!
\************************/
/***/ ((module, exports, __webpack_require__) => {
"use strict";
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _readOnlyError(r) { throw new TypeError('"' + r + '" is read-only'); }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); }
function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); }
function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: !0, configurable: !0 } }), Object.defineProperty(t, "prototype", { writable: !1 }), e && _setPrototypeOf(t, e); }
function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); }
var EventEmitter = __webpack_require__(/*! events */ "./node_modules/events/events.js");
var Status = __webpack_require__(/*! ./status */ "./lib/status.js");
var Semaphore = __webpack_require__(/*! ./semaphore */ "./lib/semaphore.js");
var MemoryCache = __webpack_require__(/*! ./cache */ "./lib/cache.js");
var STATE = Symbol('state');
var OPEN = Symbol('open');
var CLOSED = Symbol('closed');
var HALF_OPEN = Symbol('half-open');
var PENDING_CLOSE = Symbol('pending-close');
var SHUTDOWN = Symbol('shutdown');
var FALLBACK_FUNCTION = Symbol('fallback');
var STATUS = Symbol('status');
var NAME = Symbol('name');
var GROUP = Symbol('group');
var ENABLED = Symbol('Enabled');
var WARMING_UP = Symbol('warming-up');
var VOLUME_THRESHOLD = Symbol('volume-threshold');
var OUR_ERROR = Symbol('our-error');
var RESET_TIMEOUT = Symbol('reset-timeout');
var WARMUP_TIMEOUT = Symbol('warmup-timeout');
var LAST_TIMER_AT = Symbol('last-timer-at');
var deprecation = "options.maxFailures is deprecated. Please use options.errorThresholdPercentage";
/**
* Constructs a {@link CircuitBreaker}.
*
* @class CircuitBreaker
* @extends EventEmitter
* @param {Function} action The action to fire for this {@link CircuitBreaker}
* @param {Object} options Options for the {@link CircuitBreaker}
* @param {Status} options.status A {@link Status} object that might
* have pre-prime stats
* @param {Number} options.timeout The time in milliseconds that action should
* be allowed to execute before timing out. Timeout can be disabled by setting
* this to `false`. Default 10000 (10 seconds)
* @param {Number} options.maxFailures (Deprecated) The number of times the
* circuit can fail before opening. Default 10.
* @param {Number} options.resetTimeout The time in milliseconds to wait before
* setting the breaker to `halfOpen` state, and trying the action again.
* Default: 30000 (30 seconds)
* @param {Number} options.rollingCountTimeout Sets the duration of the
* statistical rolling window, in milliseconds. This is how long Opossum keeps
* metrics for the circuit breaker to use and for publishing. Default: 10000
* @param {Number} options.rollingCountBuckets Sets the number of buckets the
* rolling statistical window is divided into. So, if
* options.rollingCountTimeout is 10000, and options.rollingCountBuckets is 10,
* then the statistical window will be 1000/1 second snapshots in the
* statistical window. Default: 10
* @param {String} options.name the circuit name to use when reporting stats.
* Default: the name of the function this circuit controls.
* @param {boolean} options.rollingPercentilesEnabled This property indicates
* whether execution latencies should be tracked and calculated as percentiles.
* If they are disabled, all summary statistics (mean, percentiles) are
* returned as -1. Default: true
* @param {Number} options.capacity the number of concurrent requests allowed.
* If the number currently executing function calls is equal to
* options.capacity, further calls to `fire()` are rejected until at least one
* of the current requests completes. Default: `Number.MAX_SAFE_INTEGER`.
* @param {Number} options.errorThresholdPercentage the error percentage at
* which to open the circuit and start short-circuiting requests to fallback.
* Default: 50
* @param {boolean} options.enabled whether this circuit is enabled upon
* construction. Default: true
* @param {boolean} options.allowWarmUp determines whether to allow failures
* without opening the circuit during a brief warmup period (this is the
* `rollingCountTimeout` property). Default: false
* This can help in situations where no matter what your
* `errorThresholdPercentage` is, if the first execution times out or fails,
* the circuit immediately opens.
* @param {Number} options.volumeThreshold the minimum number of requests within
* the rolling statistical window that must exist before the circuit breaker
* can open. This is similar to `options.allowWarmUp` in that no matter how many
* failures there are, if the number of requests within the statistical window
* does not exceed this threshold, the circuit will remain closed. Default: 0
* @param {Function} options.errorFilter an optional function that will be
* called when the circuit's function fails (returns a rejected Promise). If
* this function returns truthy, the circuit's failPure statistics will not be
* incremented. This is useful, for example, when you don't want HTTP 404 to
* trip the circuit, but still want to handle it as a failure case.
* @param {boolean} options.cache whether the return value of the first
* successful execution of the circuit's function will be cached. Once a value
* has been cached that value will be returned for every subsequent execution:
* the cache can be cleared using `clearCache`. (The metrics `cacheHit` and
* `cacheMiss` reflect cache activity.) Default: false
* @param {Number} options.cacheTTL the time to live for the cache
* in milliseconds. Set 0 for infinity cache. Default: 0 (no TTL)
* @param {Number} options.cacheSize the max amount of entries in the internal
* cache. Only used when cacheTransport is not defined.
* Default: max size of JS map (2^24).
* @param {Function} options.cacheGetKey function that returns the key to use
* when caching the result of the circuit's fire.
* Better to use custom one, because `JSON.stringify` is not good
* from performance perspective.
* Default: `(...args) => JSON.stringify(args)`
* @param {CacheTransport} options.cacheTransport custom cache transport
* should implement `get`, `set` and `flush` methods.
* @param {boolean} options.coalesce If true, this provides coalescing of
* requests to this breaker, in other words: the promise will be cached.
* Only one action (with same cache key) is executed at a time, and the other
* pending actions wait for the result. Performance will improve when rapidly
* firing the circuitbreaker with the same request, especially on a slower
* action (e.g. multiple end-users fetching same data from remote).
* Will use internal cache only. Can be used in combination with options.cache.
* The metrics `coalesceCacheHit` and `coalesceCacheMiss` are available.
* Default: false
* @param {Number} options.coalesceTTL the time to live for the coalescing
* in milliseconds. Set 0 for infinity cache. Default: same as options.timeout
* @param {Number} options.coalesceSize the max amount of entries in the
* coalescing cache. Default: max size of JS map (2^24).
* @param {string[]} options.coalesceResetOn when to reset the coalesce cache.
* Options: `error`, `success`, `timeout`. Default: not set, reset using TTL.
* @param {AbortController} options.abortController this allows Opossum to
* signal upon timeout and properly abort your on going requests instead of
* leaving it in the background
* @param {boolean} options.enableSnapshots whether to enable the rolling
* stats snapshots that opossum emits at the bucketInterval. Disable this
* as an optimization if you don't listen to the 'snapshot' event to reduce
* the number of timers opossum initiates.
* @param {EventEmitter} options.rotateBucketController if you have multiple
* breakers in your app, the number of timers across breakers can get costly.
* This option allows you to provide an EventEmitter that rotates the buckets
* so you can have one global timer in your app. Make sure that you are
* emitting a 'rotate' event from this EventEmitter
* @param {boolean} options.autoRenewAbortController Automatically recreates
* the instance of AbortController whenever the circuit transitions to
* 'halfOpen' or 'closed' state. This ensures that new requests are not
* impacted by previous signals that were triggered when the circuit was 'open'.
* Default: false
*
*
* @fires CircuitBreaker#halfOpen
* @fires CircuitBreaker#close
* @fires CircuitBreaker#open
* @fires CircuitBreaker#fire
* @fires CircuitBreaker#cacheHit
* @fires CircuitBreaker#cacheMiss
* @fires CircuitBreaker#coalesceCacheHit
* @fires CircuitBreaker#coalesceCacheMiss
* @fires CircuitBreaker#reject
* @fires CircuitBreaker#timeout
* @fires CircuitBreaker#success
* @fires CircuitBreaker#semaphoreLocked
* @fires CircuitBreaker#healthCheckFailed
* @fires CircuitBreaker#fallback
* @fires CircuitBreaker#failure
*/
var CircuitBreaker = /*#__PURE__*/function (_EventEmitter) {
function CircuitBreaker(action) {
var _options$timeout, _options$resetTimeout, _options$errorThresho, _options$rollingCount, _options$rollingCount2, _options$cacheTTL, _options$cacheGetKey, _options$coalesceTTL, _options$coalesceRese;
var _this;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, CircuitBreaker);
_this = _callSuper(this, CircuitBreaker);
_this.options = options;
_this.options.timeout = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : 10000;
_this.options.resetTimeout = (_options$resetTimeout = options.resetTimeout) !== null && _options$resetTimeout !== void 0 ? _options$resetTimeout : 30000;
_this.options.errorThresholdPercentage = (_options$errorThresho = options.errorThresholdPercentage) !== null && _options$errorThresho !== void 0 ? _options$errorThresho : 50;
_this.options.rollingCountTimeout = (_options$rollingCount = options.rollingCountTimeout) !== null && _options$rollingCount !== void 0 ? _options$rollingCount : 10000;
_this.options.rollingCountBuckets = (_options$rollingCount2 = options.rollingCountBuckets) !== null && _options$rollingCount2 !== void 0 ? _options$rollingCount2 : 10;
_this.options.rollingPercentilesEnabled = options.rollingPercentilesEnabled !== false;
_this.options.capacity = Number.isInteger(options.capacity) ? options.capacity : Number.MAX_SAFE_INTEGER;
_this.options.errorFilter = options.errorFilter || function (_) {
return false;
};
_this.options.cacheTTL = (_options$cacheTTL = options.cacheTTL) !== null && _options$cacheTTL !== void 0 ? _options$cacheTTL : 0;
_this.options.cacheGetKey = (_options$cacheGetKey = options.cacheGetKey) !== null && _options$cacheGetKey !== void 0 ? _options$cacheGetKey : function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return JSON.stringify(args);
};
_this.options.enableSnapshots = options.enableSnapshots !== false;
_this.options.rotateBucketController = options.rotateBucketController;
_this.options.coalesce = !!options.coalesce;
_this.options.coalesceTTL = (_options$coalesceTTL = options.coalesceTTL) !== null && _options$coalesceTTL !== void 0 ? _options$coalesceTTL : _this.options.timeout;
_this.options.coalesceResetOn = ((_options$coalesceRese = options.coalesceResetOn) === null || _options$coalesceRese === void 0 ? void 0 : _options$coalesceRese.filter(function (o) {
return ['error', 'success', 'timeout'].includes(o);
})) || [];
// Set default cache transport if not provided
if (_this.options.cache) {
if (_this.options.cacheTransport === undefined) {
_this.options.cacheTransport = new MemoryCache(options.cacheSize);
} else if (_typeof(_this.options.cacheTransport) !== 'object' || !_this.options.cacheTransport.get || !_this.options.cacheTransport.set || !_this.options.cacheTransport.flush) {
throw new TypeError('options.cacheTransport should be an object with `get`, `set` and `flush` methods');
}
}
if (_this.options.coalesce) {
_this.options.coalesceCache = new MemoryCache(options.coalesceSize);
}
_this.semaphore = new Semaphore(_this.options.capacity);
// check if action is defined
if (!action) {
throw new TypeError('No action provided. Cannot construct a CircuitBreaker without an invocable action.');
}
if (options.autoRenewAbortController && !options.abortController) {
options.abortController = new AbortController();
}
if (options.abortController && typeof options.abortController.abort !== 'function') {
throw new TypeError('AbortController does not contain `abort()` method');
}
_this[VOLUME_THRESHOLD] = Number.isInteger(options.volumeThreshold) ? options.volumeThreshold : 0;
_this[WARMING_UP] = options.allowWarmUp === true;
// The user can pass in a Status object to initialize the Status/stats
if (_this.options.status) {
// Do a check that this is a Status Object,
if (_this.options.status instanceof Status) {
_this[STATUS] = _this.options.status;
} else {
_this[STATUS] = new Status({
stats: _this.options.status
});
}
} else {
_this[STATUS] = new Status(_this.options);
}
_this[STATE] = CLOSED;
if (options.state) {
_this[ENABLED] = options.state.enabled !== false;
_this[WARMING_UP] = options.state.warmUp || _this[WARMING_UP];
// Closed if nothing is passed in
_this[CLOSED] = options.state.closed !== false;
// These should be in sync
_this[HALF_OPEN] = _this[PENDING_CLOSE] = options.state.halfOpen || false;
// Open should be the opposite of closed,
// but also the opposite of half_open
_this[OPEN] = !_this[CLOSED] && !_this[HALF_OPEN];
_this[SHUTDOWN] = options.state.shutdown || false;
} else {
_this[PENDING_CLOSE] = false;
_this[ENABLED] = options.enabled !== false;
}
_this[FALLBACK_FUNCTION] = null;
_this[NAME] = options.name || action.name || nextName();
_this[GROUP] = options.group || _this[NAME];
if (_this[WARMING_UP]) {
var timer = _this[WARMUP_TIMEOUT] = setTimeout(function (_) {
return _this[WARMING_UP] = false;
}, _this.options.rollingCountTimeout);
if (typeof timer.unref === 'function') {
timer.unref();
}
}
if (typeof action !== 'function') {
_this.action = function (_) {
return Promise.resolve(action);
};
} else _this.action = action;
if (options.maxFailures) console.error(deprecation);
var increment = function increment(property) {
return function (result, runTime) {
return _this[STATUS].increment(property, runTime);
};
};
_this.on('success', increment('successes'));
_this.on('failure', increment('failures'));
_this.on('fallback', increment('fallbacks'));
_this.on('timeout', increment('timeouts'));
_this.on('fire', increment('fires'));
_this.on('reject', increment('rejects'));
_this.on('cacheHit', increment('cacheHits'));
_this.on('cacheMiss', increment('cacheMisses'));
_this.on('coalesceCacheHit', increment('coalesceCacheHits'));
_this.on('coalesceCacheMiss', increment('coalesceCacheMisses'));
_this.on('open', function (_) {
return _this[STATUS].open();
});
_this.on('close', function (_) {
return _this[STATUS].close();
});
_this.on('semaphoreLocked', increment('semaphoreRejections'));
/**
* @param {CircuitBreaker} circuit This current circuit
* @returns {function(): void} A bound reset callback
* @private
*/
function _startTimer(circuit) {
circuit[LAST_TIMER_AT] = Date.now();
return function (_) {
var timer = circuit[RESET_TIMEOUT] = setTimeout(function () {
_halfOpen(circuit);
}, circuit.options.resetTimeout);
if (typeof timer.unref === 'function') {
timer.unref();
}
};
}
/**
* Sets the circuit breaker to half open
* @private
* @param {CircuitBreaker} circuit The current circuit breaker
* @returns {void}
*/
function _halfOpen(circuit) {
circuit[STATE] = HALF_OPEN;
circuit[PENDING_CLOSE] = true;
circuit._renewAbortControllerIfNeeded();
/**
* Emitted after `options.resetTimeout` has elapsed, allowing for
* a single attempt to call the service again. If that attempt is
* successful, the circuit will be closed. Otherwise it remains open.
*
* @event CircuitBreaker#halfOpen
* @type {Number} how long the circuit remained open
*/
circuit.emit('halfOpen', circuit.options.resetTimeout);
}
_this.on('open', _startTimer(_this));
_this.on('success', function (_) {
if (_this.halfOpen) {
_this.close();
}
});
// Prepopulate the State of the Breaker
if (_this[SHUTDOWN]) {
_this[STATE] = SHUTDOWN;
_this.shutdown();
} else if (_this[CLOSED]) {
_this.close();
} else if (_this[OPEN]) {
// If the state being passed in is OPEN but more time has elapsed
// than the resetTimeout, then we should be in halfOpen state
if (_this.options.state.lastTimerAt !== undefined && Date.now() - _this.options.state.lastTimerAt > _this.options.resetTimeout) {
_halfOpen(_this);
} else {
_this.open();
}
} else if (_this[HALF_OPEN]) {
// Not sure if anything needs to be done here
_this[STATE] = HALF_OPEN;
}
return _this;
}
/**
* Renews the abort controller if needed
* @private
* @returns {void}
*/
_inherits(CircuitBreaker, _EventEmitter);
return _createClass(CircuitBreaker, [{
key: "_renewAbortControllerIfNeeded",
value: function _renewAbortControllerIfNeeded() {
if (this.options.autoRenewAbortController && this.options.abortController && this.options.abortController.signal.aborted) {
this.options.abortController = new AbortController();
}
}
/**
* Closes the breaker, allowing the action to execute again
* @fires CircuitBreaker#close
* @returns {void}
*/
}, {
key: "close",
value: function close() {
if (this[STATE] !== CLOSED) {
if (this[RESET_TIMEOUT]) {
clearTimeout(this[RESET_TIMEOUT]);
}
this[STATE] = CLOSED;
this[PENDING_CLOSE] = false;
this._renewAbortControllerIfNeeded();
/**
* Emitted when the breaker is reset allowing the action to execute again
* @event CircuitBreaker#close
*/
this.emit('close');
}
}
/**
* Opens the breaker. Each time the breaker is fired while the circuit is
* opened, a failed Promise is returned, or if any fallback function
* has been provided, it is invoked.
*
* If the breaker is already open this call does nothing.
* @fires CircuitBreaker#open
* @returns {void}
*/
}, {
key: "open",
value: function open() {
if (this[STATE] !== OPEN) {
this[STATE] = OPEN;
this[PENDING_CLOSE] = false;
/**
* Emitted when the breaker opens because the action has
* failure percentage greater than `options.errorThresholdPercentage`.
* @event CircuitBreaker#open
*/
this.emit('open');
}
}
/**
* Shuts down this circuit breaker. All subsequent calls to the
* circuit will fail, returning a rejected promise.
* @returns {void}
*/
}, {
key: "shutdown",
value: function shutdown() {
/**
* Emitted when the circuit breaker has been shut down.
* @event CircuitBreaker#shutdown
*/
this.emit('shutdown');
this.disable();
this.removeAllListeners();
if (this[RESET_TIMEOUT]) {
clearTimeout(this[RESET_TIMEOUT]);
}
if (this[WARMUP_TIMEOUT]) {
clearTimeout(this[WARMUP_TIMEOUT]);
}
this.status.shutdown();
this[STATE] = SHUTDOWN;
// clear cache on shutdown
this.clearCache();
}
/**
* Determines if the circuit has been shutdown.
* @type {Boolean}
*/
}, {
key: "isShutdown",
get: function get() {
return this[STATE] === SHUTDOWN;
}
/**
* Gets the name of this circuit
* @type {String}
*/
}, {
key: "name",
get: function get() {
return this[NAME];
}
/**
* Gets the name of this circuit group
* @type {String}
*/
}, {
key: "group",
get: function get() {
return this[GROUP];
}
/**
* Gets whether this circuit is in the `pendingClosed` state
* @type {Boolean}
*/
}, {
key: "pendingClose",
get: function get() {
return this[PENDING_CLOSE];
}
/**
* True if the circuit is currently closed. False otherwise.
* @type {Boolean}
*/
}, {
key: "closed",
get: function get() {
return this[STATE] === CLOSED;
}
/**
* True if the circuit is currently opened. False otherwise.
* @type {Boolean}
*/
}, {
key: "opened",
get: function get() {
return this[STATE] === OPEN;
}
/**
* True if the circuit is currently half opened. False otherwise.
* @type {Boolean}
*/
}, {
key: "halfOpen",
get: function get() {
return this[STATE] === HALF_OPEN;
}
/**
* The current {@link Status} of this {@link CircuitBreaker}
* @type {Status}
*/
}, {
key: "status",
get: function get() {
return this[STATUS];
}
/**
* Get the current stats for the circuit.
* @see Status#stats
* @type {Object}
*/
}, {
key: "stats",
get: function get() {
return this[STATUS].stats;
}
}, {
key: "toJSON",
value: function toJSON() {
return {
state: {
name: this.name,
enabled: this.enabled,
closed: this.closed,
open: this.opened,
halfOpen: this.halfOpen,
warmUp: this.warmUp,
shutdown: this.isShutdown,
lastTimerAt: this[LAST_TIMER_AT]
},
status: this.status.stats
};
}
/**
* Gets whether the circuit is enabled or not
* @type {Boolean}
*/
}, {
key: "enabled",
get: function get() {
return this[ENABLED];
}
/**
* Gets whether the circuit is currently in warm up phase
* @type {Boolean}
*/
}, {
key: "warmUp",
get: function get() {
return this[WARMING_UP];
}
/**
* Gets the volume threshold for this circuit
* @type {Boolean}
*/
}, {
key: "volumeThreshold",
get: function get() {
return this[VOLUME_THRESHOLD];
}
/**
* Provide a fallback function for this {@link CircuitBreaker}. This
* function will be executed when the circuit is `fire`d and fails.
* It will always be preceded by a `failure` event, and `breaker.fire` returns
* a rejected Promise.
* @param {Function | CircuitBreaker} func the fallback function to execute
* when the breaker has opened or when a timeout or error occurs.
* @return {CircuitBreaker} this
*/
}, {
key: "fallback",
value: function fallback(func) {
var fb = func;
if (func instanceof CircuitBreaker) {
fb = function fb() {
return func.fire.apply(func, arguments);
};
}
this[FALLBACK_FUNCTION] = fb;
return this;
}
/**
* Execute the action for this circuit. If the action fails or times out, the
* returned promise will be rejected. If the action succeeds, the promise will
* resolve with the resolved value from action. If a fallback function was
* provided, it will be invoked in the event of any failure or timeout.
*
* Any parameters passed to this function will be proxied to the circuit
* function.
*
* @return {Promise<any>} promise resolves with the circuit function's return
* value on success or is rejected on failure of the action. Use isOurError()
* to determine if a rejection was a result of the circuit breaker or the
* action.
*
* @fires CircuitBreaker#failure
* @fires CircuitBreaker#fallback
* @fires CircuitBreaker#fire
* @fires CircuitBreaker#reject
* @fires CircuitBreaker#success
* @fires CircuitBreaker#timeout
* @fires CircuitBreaker#semaphoreLocked
*/
}, {
key: "fire",
value: function fire() {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return this.call.apply(this, [this.action].concat(args));
}
/**
* Execute the action for this circuit using `context` as `this`.
* If the action fails or times out, the
* returned promise will be rejected. If the action succeeds, the promise will
* resolve with the resolved value from action. If a fallback function was
* provided, it will be invoked in the event of any failure or timeout.
*
* Any parameters in addition to `context will be passed to the
* circuit function.
*
* @param {any} context the `this` context used for function execution
* @param {any} rest the arguments passed to the action
*
* @return {Promise<any>} promise resolves with the circuit function's return
* value on success or is rejected on failure of the action.
*
* @fires CircuitBreaker#failure
* @fires CircuitBreaker#fallback
* @fires CircuitBreaker#fire
* @fires CircuitBreaker#reject
* @fires CircuitBreaker#success
* @fires CircuitBreaker#timeout
* @fires CircuitBreaker#semaphoreLocked
*/
}, {
key: "call",
value: function call(context) {
var _this2 = this;
if (this.isShutdown) {
var err = buildError('The circuit has been shutdown.', 'ESHUTDOWN');
return Promise.reject(err);
}
for (var _len3 = arguments.length, rest = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
rest[_key3 - 1] = arguments[_key3];
}
var args = rest.slice();
/**
* Emitted when the circuit breaker action is executed
* @event CircuitBreaker#fire
* @type {any} the arguments passed to the fired function
*/
this.emit('fire', args);
// Protection, caches and coalesce disabled.
if (!this[ENABLED]) {
var result = this.action.apply(context, args);
return typeof result.then === 'function' ? result : Promise.resolve(result);
}
// Generate cachekey only when cache and/or coalesce is enabled.
var cacheKey = this.options.cache || this.options.coalesce ? this.options.cacheGetKey.apply(this, rest) : '';
// If cache is enabled, check if we have a cached value
if (this.options.cache) {
var cached = this.options.cacheTransport.get(cacheKey);
if (cached) {
/**
* Emitted when the circuit breaker is using the cache
* and finds a value.
* @event CircuitBreaker#cacheHit
*/
this.emit('cacheHit');
return cached;
}
/**
* Emitted when the circuit breaker does not find a value in
* the cache, but the cache option is enabled.
* @event CircuitBreaker#cacheMiss
*/
this.emit('cacheMiss');
}
/* When coalesce is enabled, check coalesce cache and return
promise, if any. */
if (this.options.coalesce) {
var cachedCall = this.options.coalesceCache.get(cacheKey);
if (cachedCall) {
/**
* Emitted when the circuit breaker is using coalesce cache
* and finds a cached promise.
* @event CircuitBreaker#coalesceCacheHit
*/
this.emit('coalesceCacheHit');
return cachedCall;
}
/**
* Emitted when the circuit breaker does not find a value in
* coalesce cache, but the coalesce option is enabled.
* @event CircuitBreaker#coalesceCacheMiss
*/
this.emit('coalesceCacheMiss');
}
if (!this.closed && !this.pendingClose) {
/**
* Emitted when the circuit breaker is open and failing fast
* @event CircuitBreaker#reject
* @type {Error}
*/
var error = buildError('Breaker is open', 'EOPENBREAKER');
this.emit('reject', error);
return fallback(this, error, args) || Promise.reject(error);
}
this[PENDING_CLOSE] = false;
var timeout;
var timeoutError = false;
var call = new Promise(function (resolve, reject) {
var latencyStartTime = Date.now();
if (_this2.semaphore.test()) {
if (_this2.options.timeout) {
timeout = setTimeout(function () {
timeoutError = true;
var error = buildError("Timed out after ".concat(_this2.options.timeout, "ms"), 'ETIMEDOUT');
var latency = Date.now() - latencyStartTime;
_this2.semaphore.release();
/**
* Emitted when the circuit breaker action takes longer than
* `options.timeout`
* @event CircuitBreaker#timeout
* @type {Error}
*/
_this2.emit('timeout', error, latency, args);
handleError(error, _this2, timeout, args, latency, resolve, reject);
resetCoalesce(_this2, cacheKey, 'timeout');
if (_this2.options.abortController) {
_this2.options.abortController.abort();
}
}, _this2.options.timeout);
}
try {
var _result = _this2.action.apply(context, args);
var promise = typeof _result.then === 'function' ? _result : Promise.resolve(_result);
promise.then(function (result) {
if (!timeoutError) {
clearTimeout(timeout);
/**
* Emitted when the circuit breaker action succeeds
* @event CircuitBreaker#success
* @type {any} the return value from the circuit
*/
_this2.emit('success', result, Date.now() - latencyStartTime);
resetCoalesce(_this2, cacheKey, 'success');
_this2.semaphore.release();
resolve(result);
if (_this2.options.cache) {
_this2.options.cacheTransport.set(cacheKey, promise, _this2.options.cacheTTL > 0 ? Date.now() + _this2.options.cacheTTL : 0);
}
}
})["catch"](function (error) {
if (!timeoutError) {
_this2.semaphore.release();
var latencyEndTime = Date.now() - latencyStartTime;
handleError(error, _this2, timeout, args, latencyEndTime, resolve, reject);
resetCoalesce(_this2, cacheKey, 'error');
}
});
} catch (error) {
_this2.semaphore.release();
var latency = Date.now() - latencyStartTime;
handleError(error, _this2, timeout, args, latency, resolve, reject);
resetCoalesce(_this2, cacheKey, 'error');
}
} else {
var _latency = Date.now() - latencyStartTime;
var _err = buildError('Semaphore locked', 'ESEMLOCKED');
/**
* Emitted when the rate limit has been reached and there
* are no more locks to be obtained.
* @event CircuitBreaker#semaphoreLocked
* @type {Error}
*/
_this2.emit('semaphoreLocked', _err, _latency);
handleError(_err, _this2, timeout, args, _latency, resolve, reject);
resetCoalesce(_this2, cacheKey);
}
});
/* When coalesce is enabled, store promise in coalesceCache */
if (this.options.coalesce) {
this.options.coalesceCache.set(cacheKey, call, this.options.coalesceTTL > 0 ? Date.now() + this.options.coalesceTTL : 0);
}
return call;
}
/**
* Clears the cache of this {@link CircuitBreaker}
* @returns {void}
*/
}, {
key: "clearCache",
value: function clearCache() {
if (this.options.cache) {
this.options.cacheTransport.flush();
}
if (this.options.coalesceCache) {
this.options.coalesceCache.flush();
}
}
/**
* Provide a health check function to be called periodically. The function
* should return a Promise. If the promise is rejected the circuit will open.
* This is in addition to the existing circuit behavior as defined by
* `options.errorThresholdPercentage` in the constructor. For example, if the
* health check function provided here always returns a resolved promise, the
* circuit can still trip and open if there are failures exceeding the
* configured threshold. The health check function is executed within the
* circuit breaker's execution context, so `this` within the function is the
* circuit breaker itself.
*
* @param {Function} func a health check function which returns a promise.
* @param {Number} [interval] the amount of time between calls to the health
* check function. Default: 5000 (5 seconds)
*
* @returns {void}
*
* @fires CircuitBreaker#healthCheckFailed
* @throws {TypeError} if `interval` is supplied but not a number
*/
}, {
key: "healthCheck",
value: function healthCheck(func, interval) {
var _this3 = this;
interval = interval || 5000;
if (typeof func !== 'function') {
throw new TypeError('Health check function must be a function');
}
if (isNaN(interval)) {
throw new TypeError('Health check interval must be a number');
}
var check = function check(_) {
func.apply(_this3)["catch"](function (e) {
/**
* Emitted with the user-supplied health check function
* returns a rejected promise.
* @event CircuitBreaker#healthCheckFailed
* @type {Error}
*/
_this3.emit('healthCheckFailed', e);
_this3.open();
});
};
var timer = setInterval(check, interval);
if (typeof timer.unref === 'function') {
timer.unref();
}
check();
}
/**
* Enables this circuit. If the circuit is the disabled
* state, it will be re-enabled. If not, this is essentially
* a noop.
* @returns {void}
*/
}, {
key: "enable",
value: function enable() {
this[ENABLED] = true;
this.status.startListeneningForRotateEvent();
}
/**
* Disables this circuit, causing all calls to the circuit's function
* to be executed without circuit or fallback protection.
* @returns {void}
*/
}, {
key: "disable",
value: function disable() {
this[ENABLED] = false;
this.status.removeRotateBucketControllerListener();
}
/**
* Retrieves the current AbortSignal from the abortController, if available.
* This signal can be used to monitor ongoing requests.
* @returns {AbortSignal|undefined} The AbortSignal if present,
* otherwise undefined.
*/
}, {
key: "getSignal",
value: function getSignal() {
if (this.options.abortController && this.options.abortController.signal) {
return this.options.abortController.signal;
}
return undefined;
}
/**
* Retrieves the current AbortController instance.
* This controller can be used to manually abort ongoing requests or create
* a new signal.
* @returns {AbortController|undefined} The AbortController if present,
* otherwise undefined.
*/
}, {
key: "getAbortController",
value: function getAbortController() {
return this.options.abortController;
}
}], [{
key: "isOurError",
value:
/**
* Returns true if the provided error was generated here. It will be false
* if the error came from the action itself.
* @param {Error} error The Error to check.
* @returns {Boolean} true if the error was generated here
*/
function isOurError(error) {
return !!error[OUR_ERROR];
}
/**
* Create a new Status object,
* helpful when you need to prime a breaker with stats
* @param {Object} options -
* @param {Number} options.rollingCountBuckets number of buckets in the window
* @param {Number} options.rollingCountTimeout the duration of the window
* @param {Boolean} options.rollingPercentilesEnabled whether to calculate
* @param {Object} options.stats user supplied stats
* @returns {Status} a new {@link Status} object
*/
}, {
key: "newStatus",
value: function newStatus(options) {
return new Status(options);
}
}]);
}(EventEmitter);
function handleError(error, circuit, timeout, args, latency, resolve, reject) {
var _circuit$options;
clearTimeout(timeout);
if ((_circuit$options = circuit.options).errorFilter.apply(_circuit$options, [error].concat(_toConsumableArray(args)))) {
// The error was filtered, so emit 'success'
circuit.emit('success', error, latency);
} else {
// Error was not filtered, so emit 'failure'
fail(circuit, error, args, latency);
// Only call the fallback function if errorFilter doesn't succeed
// If the fallback function succeeds, resolve
var fb = fallback(circuit, error, args);
if (fb) return resolve(fb);
}
// In all other cases, reject
reject(error);
}
function fallback(circuit, err, args) {
if (circuit[FALLBACK_FUNCTION]) {
try {
var result = circuit[FALLBACK_FUNCTION].apply(circuit[FALLBACK_FUNCTION], [].concat(_toConsumableArray(args), [err]));
/**
* Emitted when the circuit breaker executes a fallback function
* @event CircuitBreaker#fallback
* @type {any} the return value of the fallback function
*/
circuit.emit('fallback', result, err);
if (result instanceof Promise) return result;
return Promise.resolve(result);
} catch (e) {
return Promise.reject(e);
}
}
}
function fail(circuit, err, args, latency) {
/**
* Emitted when the circuit breaker action fails
* @event CircuitBreaker#failure
* @type {Error}
*/
circuit.emit('failure', err, latency, args);
if (circuit.warmUp) return;
// check stats to see if the circuit should be opened
var stats = circuit.stats;
if (stats.fires < circuit.volumeThreshold && !circuit.halfOpen) return;
var errorRate = stats.failures / stats.fires * 100;
if (errorRate > circuit.options.errorThresholdPercentage || circuit.halfOpen) {
circuit.open();
}
}
function resetCoalesce(circuit, cacheKey, event) {
/**
* Reset coalesce cache for this cacheKey, depending on
* options.coalesceResetOn set.
* @param {@link CircuitBreaker} circuit what circuit is to be cleared
* @param {string} cacheKey cache key to clear.
* @param {string} event optional, can be `error`, `success`, `timeout`
* @returns {void}
*/
if (!event || circuit.options.coalesceResetOn.includes(event)) {
var _circuit$options$coal;
(_circuit$options$coal = circuit.options.coalesceCache) === null || _circuit$options$coal === void 0 || _circuit$options$coal["delete"](cacheKey);
}
}
function buildError(msg, code) {
var error = new Error(msg);
error.code = code;
error[OUR_ERROR] = true;
return error;
}
// http://stackoverflow.com/a/2117523
var nextName = function nextName() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0;
var v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
};
module.exports = exports = CircuitBreaker;
/***/ }),
/***/ "./lib/semaphore.js":
/*!**************************!*\
!*** ./lib/semaphore.js ***!
\**************************/
/***/ ((module, exports) => {
"use strict";
module.exports = exports = semaphore;
function semaphore(count) {
var resolvers = [];
var counter = count;
var sem = {
take: take,
release: release,
test: test
};
Object.defineProperty(sem, 'count', {
get: function get(_) {
return counter;
},
enumerable: true
});
return sem;
function take(timeout) {
if (counter > 0) {
--counter;
return Promise.resolve(release);
}
return new Promise(function (resolve, reject) {
resolvers.push(function (_) {
--counter;
resolve(release);
});
if (timeout) {
setTimeout(function (_) {
resolvers.shift();
var err = new Error("Timed out after ".concat(timeout, "ms"));
err.code = 'ETIMEDOUT';
reject(err);
}, timeout);
}
});
}
function release() {
counter++;
if (resolvers.length > 0) {
resolvers.shift()();
}
}
function test() {
if (counter < 1) return false;
return take() && true;
}
}
/***/ }),
/***/ "./lib/status.js":
/*!***********************!*\
!*** ./lib/status.js ***!
\***********************/
/***/ ((module, exports, __webpack_require__) => {
"use strict";
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _d