diff --git a/README.md b/README.md index cb024949..080d66fc 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,8 @@ const stats = breaker.stats; timeouts: 0, cacheHits: 0, cacheMisses: 0, + coalesceCacheHits: 0, + coalesceCacheMisses: 0, semaphoreRejections: 0, percentiles: { '0': 0, @@ -417,6 +419,9 @@ The code that is summing the stats samples is here: }, bucket()); ``` +### Coalesce calls + +Circuitbreaker offers coalescing your calls. If options.coalesce is set, multiple calls to the circuitbreaker will be handled as one, within the given timeframe (options.coalesceTTL). Performance will improve when rapidly firing the circuitbreaker with the same request, especially on a slower action. This is especially useful if multiple events can trigger the same functions at the same time. Of course, caching has the same function, but will only be effective when the call has been executed once to store the return value. Coalescing and cache can be used at the same time, coalescing calls will always use the internal cache. ### Typings diff --git a/lib/cache.js b/lib/cache.js index b48a2733..5c7019eb 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -4,8 +4,9 @@ * @property {Map} cache Cache map */ class MemoryCache { - constructor () { + constructor (maxEntries) { this.cache = new Map(); + this.maxEntries = maxEntries ?? 2 ^ 24 - 1; // Max size for Map is 2 ^ 24. } /** @@ -32,6 +33,11 @@ class MemoryCache { * @return {void} */ set (key, value, ttl) { + // Evict oldest entry when at capacity. + if (this.cache.size === this.maxEntries) { + this.cache.delete(this.cache.keys().next().value); + } + this.cache.set(key, { expiresAt: ttl, value diff --git a/lib/circuit.js b/lib/circuit.js index 64d19c3b..2a541ded 100644 --- a/lib/circuit.js +++ b/lib/circuit.js @@ -88,6 +88,9 @@ Please use options.errorThresholdPercentage`; * `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 @@ -95,6 +98,19 @@ Please use options.errorThresholdPercentage`; * 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 {AbortController} options.abortController this allows Opossum to * signal upon timeout and properly abort your on going requests instead of * leaving it in the background @@ -115,6 +131,8 @@ Please use options.errorThresholdPercentage`; * @fires CircuitBreaker#fire * @fires CircuitBreaker#cacheHit * @fires CircuitBreaker#cacheMiss + * @fires CircuitBreaker#coalesceCacheHit + * @fires CircuitBreaker#coalesceCacheMiss * @fires CircuitBreaker#reject * @fires CircuitBreaker#timeout * @fires CircuitBreaker#success @@ -168,11 +186,13 @@ class CircuitBreaker extends EventEmitter { ((...args) => JSON.stringify(args)); this.options.enableSnapshots = options.enableSnapshots !== false; this.options.rotateBucketController = options.rotateBucketController; + this.options.coalesce = !!options.coalesce; + this.options.coalesceTTL = options.coalesceTTL ?? this.options.timeout; // Set default cache transport if not provided if (this.options.cache) { if (this.options.cacheTransport === undefined) { - this.options.cacheTransport = new MemoryCache(); + this.options.cacheTransport = new MemoryCache(options.cacheSize); } else if (typeof this.options.cacheTransport !== 'object' || !this.options.cacheTransport.get || !this.options.cacheTransport.set || @@ -184,6 +204,10 @@ class CircuitBreaker extends EventEmitter { } } + if (this.options.coalesce) { + this.options.coalesceCache = new MemoryCache(options.coalesceSize); + } + this.semaphore = new Semaphore(this.options.capacity); // check if action is defined @@ -265,6 +289,8 @@ class CircuitBreaker extends EventEmitter { 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', _ => this[STATUS].open()); this.on('close', _ => this[STATUS].close()); this.on('semaphoreLocked', increment('semaphoreRejections')); @@ -593,6 +619,14 @@ class CircuitBreaker extends EventEmitter { const args = rest.slice(); + // Protection, caches and coalesce disabled. + if (!this[ENABLED]) { + const result = this.action.apply(context, args); + return (typeof result.then === 'function') + ? result + : Promise.resolve(result); + } + // Need to create variable here to prevent extra calls if cache is disabled let cacheKey = ''; @@ -624,11 +658,26 @@ class CircuitBreaker extends EventEmitter { this.emit('cacheMiss'); } - if (!this[ENABLED]) { - const result = this.action.apply(context, args); - return (typeof result.then === 'function') - ? result - : Promise.resolve(result); + /* When coalesce is enabled, check coalesce cache and return + promise, if any. */ + if (this.options.coalesce) { + const 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) { @@ -648,7 +697,8 @@ class CircuitBreaker extends EventEmitter { let timeout; let timeoutError = false; - return new Promise((resolve, reject) => { + + const call = new Promise((resolve, reject) => { const latencyStartTime = Date.now(); if (this.semaphore.test()) { if (this.options.timeout) { @@ -728,6 +778,19 @@ class CircuitBreaker extends EventEmitter { handleError(err, this, timeout, args, latency, resolve, reject); } }); + + /* 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; } /** diff --git a/lib/status.js b/lib/status.js index ab0a386c..d7a8c8e8 100644 --- a/lib/status.js +++ b/lib/status.js @@ -229,6 +229,8 @@ const bucket = _ => ({ timeouts: 0, cacheHits: 0, cacheMisses: 0, + coalesceCacheHits: 0, + coalesceCacheMisses: 0, semaphoreRejections: 0, percentiles: {}, latencyTimes: [] diff --git a/package-lock.json b/package-lock.json index 4b4c935b..75061e5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "eslint-plugin-standard": "^5.0.0", "faucet": "^0.0.3", "husky": "^8.0.3", - "nyc": "^17.0.0", + "nyc": "~17.0.0", "opener": "1.5.2", "serve": "^14.2.0", "stream-browserify": "^3.0.0", @@ -2545,9 +2545,9 @@ "dev": true }, "node_modules/@zeit/schemas": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.29.0.tgz", - "integrity": "sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==", + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", "dev": true }, "node_modules/accepts": { @@ -3256,12 +3256,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3340,13 +3340,10 @@ } }, "node_modules/builtins/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -4060,9 +4057,9 @@ } }, "node_modules/documentation": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.1.tgz", - "integrity": "sha512-Y/brACCE3sNnDJPFiWlhXrqGY+NelLYVZShLGse5bT1KdohP4JkPf5T2KNq1YWhIEbDYl/1tebRLC0WYbPQxVw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.3.tgz", + "integrity": "sha512-B7cAviVKN9Rw7Ofd+9grhVuxiHwly6Ieh+d/ceMw8UdBOv/irkuwnDEJP8tq0wgdLJDUVuIkovV+AX9mTrZFxg==", "dev": true, "dependencies": { "@babel/core": "^7.18.10", @@ -4729,13 +4726,10 @@ } }, "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -5406,9 +5400,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -6356,6 +6350,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -7986,10 +7989,16 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "optional": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -8054,13 +8063,10 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -8574,11 +8580,10 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true, - "license": "ISC" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", @@ -8687,9 +8692,9 @@ } }, "node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -8699,13 +8704,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "optional": true, "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -9404,13 +9413,13 @@ } }, "node_modules/serve": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.0.tgz", - "integrity": "sha512-+HOw/XK1bW8tw5iBilBz/mJLWRzM8XM6MPxL4J/dKzdxq1vfdEWSwhaR7/yS8EJp5wzvP92p1qirysJvnEtjXg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.3.tgz", + "integrity": "sha512-VqUFMC7K3LDGeGnJM9h56D3XGKb6KGgOw0cVNtA26yYXHCcpxf3xwCTUaQoWlVS7i8Jdh3GjQkOB23qsXyjoyQ==", "dev": true, "dependencies": { - "@zeit/schemas": "2.29.0", - "ajv": "8.11.0", + "@zeit/schemas": "2.36.0", + "ajv": "8.12.0", "arg": "5.0.2", "boxen": "7.0.0", "chalk": "5.0.1", @@ -9466,9 +9475,9 @@ } }, "node_modules/serve/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", @@ -9597,9 +9606,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "optional": true, "engines": { @@ -10030,15 +10039,6 @@ "node": ">=8.0" } }, - "node_modules/to-regex-range/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -10072,9 +10072,9 @@ } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -10605,9 +10605,9 @@ } }, "node_modules/vue-template-compiler": { - "version": "2.7.9", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.9.tgz", - "integrity": "sha512-NPJxt6OjVlzmkixYg0SdIN2Lw/rMryQskObY89uAMcM9flS/HrmLK5LaN1ReBTuhBgnYuagZZEkSS6FESATQUQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", "dev": true, "optional": true, "dependencies": { @@ -13035,9 +13035,9 @@ "dev": true }, "@zeit/schemas": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.29.0.tgz", - "integrity": "sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==", + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", "dev": true }, "accepts": { @@ -13519,12 +13519,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -13565,13 +13565,10 @@ }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true } } }, @@ -14098,9 +14095,9 @@ } }, "documentation": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.1.tgz", - "integrity": "sha512-Y/brACCE3sNnDJPFiWlhXrqGY+NelLYVZShLGse5bT1KdohP4JkPf5T2KNq1YWhIEbDYl/1tebRLC0WYbPQxVw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.3.tgz", + "integrity": "sha512-B7cAviVKN9Rw7Ofd+9grhVuxiHwly6Ieh+d/ceMw8UdBOv/irkuwnDEJP8tq0wgdLJDUVuIkovV+AX9mTrZFxg==", "dev": true, "requires": { "@babel/core": "^7.18.10", @@ -14775,13 +14772,10 @@ }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true } } }, @@ -15106,9 +15100,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -15743,6 +15737,12 @@ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -16864,9 +16864,9 @@ "dev": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "optional": true }, @@ -16916,13 +16916,10 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true } } }, @@ -17305,9 +17302,9 @@ "dev": true }, "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true }, "picomatch": { @@ -17383,15 +17380,15 @@ } }, "postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "optional": true, "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" } }, "prelude-ls": { @@ -17904,13 +17901,13 @@ } }, "serve": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.0.tgz", - "integrity": "sha512-+HOw/XK1bW8tw5iBilBz/mJLWRzM8XM6MPxL4J/dKzdxq1vfdEWSwhaR7/yS8EJp5wzvP92p1qirysJvnEtjXg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.3.tgz", + "integrity": "sha512-VqUFMC7K3LDGeGnJM9h56D3XGKb6KGgOw0cVNtA26yYXHCcpxf3xwCTUaQoWlVS7i8Jdh3GjQkOB23qsXyjoyQ==", "dev": true, "requires": { - "@zeit/schemas": "2.29.0", - "ajv": "8.11.0", + "@zeit/schemas": "2.36.0", + "ajv": "8.12.0", "arg": "5.0.2", "boxen": "7.0.0", "chalk": "5.0.1", @@ -17923,9 +17920,9 @@ }, "dependencies": { "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -18058,9 +18055,9 @@ "dev": true }, "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "optional": true }, @@ -18400,14 +18397,6 @@ "dev": true, "requires": { "is-number": "^7.0.0" - }, - "dependencies": { - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - } } }, "trim-lines": { @@ -18435,9 +18424,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" @@ -18814,9 +18803,9 @@ } }, "vue-template-compiler": { - "version": "2.7.9", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.9.tgz", - "integrity": "sha512-NPJxt6OjVlzmkixYg0SdIN2Lw/rMryQskObY89uAMcM9flS/HrmLK5LaN1ReBTuhBgnYuagZZEkSS6FESATQUQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", "dev": true, "optional": true, "requires": { diff --git a/test/browser/index.js b/test/browser/index.js index 0d0446cf..ce04afc9 100644 --- a/test/browser/index.js +++ b/test/browser/index.js @@ -1,6 +1,7 @@ // This is a generated file. Do not modify by hand 'use strict'; +require('../cache.js'); require('../circuit-shutdown-test.js'); require('../closed-test.js'); require('../common.js'); @@ -9,6 +10,7 @@ require('../enable-disable-test.js'); require('../error-filter-test.js'); require('../half-open-test.js'); require('../health-check-test.js'); +require('../options-test.js'); require('../semaphore-test.js'); require('../state-test.js'); require('../status-test.js'); diff --git a/test/cache.js b/test/cache.js index 92ee385e..be9fa8a4 100644 --- a/test/cache.js +++ b/test/cache.js @@ -3,12 +3,12 @@ const test = require('tape'); const CircuitBreaker = require('../'); const common = require('./common'); - const passFail = common.passFail; +const expected = 34; test('Using cache', t => { t.plan(9); - const expected = 34; + const options = { cache: true }; @@ -33,6 +33,7 @@ test('Using cache', t => { `cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); breaker.clearCache(); }) + .then(() => breaker.fire(expected)) .then(arg => { const stats = breaker.status.stats; @@ -44,9 +45,31 @@ test('Using cache', t => { .catch(t.fail); }); +test('Using cache max size', t => { + t.plan(2); + + const options = { + cache: true, + cacheSize: 2 + }; + const breaker = new CircuitBreaker(passFail, options); + + Promise.all([ + breaker.fire(1), + breaker.fire(2), + breaker.fire(3), + breaker.fire(4) + ]).then(() => { + const stats = breaker.status.stats; + t.equals(stats.cacheHits, 0, 'does not hit the cache'); + t.equals(breaker.options.cacheTransport.cache.size, options.cacheSize, 'respects max size'); + }).then(t.end) + .catch(t.fail); +}); + test('Using cache with TTL', t => { t.plan(12); - const expected = 34; + const options = { cache: true, cacheTTL: 100 @@ -86,9 +109,75 @@ test('Using cache with TTL', t => { .catch(t.fail); }); +test('Using coalesce cache + regular cache.', t => { + t.plan(10); + + const options = { + cache: true, + cacheTTL: 200, + coalesce: true, + coalesceTTL: 200 + }; + + const breaker = new CircuitBreaker(passFail, options); + + // fire breaker three times in rapid succession, expect execution once. + Promise.all([ + breaker.fire(expected), + breaker.fire(expected), + breaker.fire(expected) + ]).then(results => { + const stats = breaker.status.stats; + t.equals(stats.cacheHits, 0, 'does not hit the cache'); + t.equals(stats.coalesceCacheHits, 2, 'hits coalesce cache twice'); + t.equals(stats.fires, 3, 'fired thrice'); + t.equals(stats.successes, 1, 'success once'); + t.equals(results.length, 3, 'executed 3'); + t.deepEqual(results, [expected, expected, expected], + `cache coalesceCacheHits:coalesceCacheMisses` + + `${stats.coalesceCacheHits}:${stats.coalesceCacheMisses}`); + }) + // Re-fire breaker, expect cache hit as cache takes preference. + .then(() => new Promise(resolve => setTimeout(resolve, 0))) + .then(() => breaker.fire(expected)).then(arg => { + const stats = breaker.status.stats; + + t.equals(stats.cacheHits, 1, 'hits the cache'); + t.equals(stats.coalesceCacheHits, 2, 'not further hits to coalesce cache, it is now expired'); + t.equals(stats.successes, 1, 'success once'); + t.equals(arg, expected, + `cache hits:misses ${stats.cacheHits}:${stats.cacheMisses}`); + }) + .then(t.end) + .catch(t.fail); +}); + +test('No coalesce cache.', t => { + t.plan(5); + const breaker = new CircuitBreaker(passFail); + + // fire breaker three times, expect execution three times. + Promise.all([ + breaker.fire(expected), + breaker.fire(expected), + breaker.fire(expected) + ]).then(results => { + const stats = breaker.status.stats; + t.equals(stats.cacheHits, 0, 'does not hit the cache'); + t.equals(stats.coalesceCacheHits, 0, 'does not hit coalesce cache'); + t.equals(stats.fires, 3, 'fired thrice'); + t.equals(stats.successes, 3, 'success thrice'); + t.deepEqual(results, [expected, expected, expected], + `cache coalesceCacheHits:coalesceCacheMisses` + + `${stats.coalesceCacheHits}:${stats.coalesceCacheMisses}`); + }) + .then(t.end) + .catch(t.fail); +}); + test('Using cache with custom get cache key', t => { t.plan(11); - const expected = 34; + const options = { cache: true, cacheGetKey: x => `key-${x}` @@ -131,7 +220,7 @@ test('Using cache with custom get cache key', t => { test('Using cache with custom transport', t => { t.plan(15); - const expected = 34; + const cache = new Map(); const options = { cache: true,