UNPKG

opossum

Version:

A fail-fast circuit breaker for promises and callbacks

1,250 lines (1,180 loc) 77.7 kB
(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