UNPKG

brakes

Version:

Node.js Circuit Breaker Pattern

227 lines (196 loc) 5.87 kB
'use strict'; const EventEmitter = require('events').EventEmitter; const Bucket = require('./Bucket'); /* Example Default Options */ const defaultOptions = { bucketSpan: 1000, bucketNum: 60, percentiles: [0.0, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.995, 1], statInterval: 1200 }; class Stats extends EventEmitter { constructor(opts) { super(); this._opts = Object.assign({}, defaultOptions, opts); this._activePosition = this._opts.bucketNum - 1; this._cummulative = { // Total count of requests, failures, etc. countTotal: 0, countSuccess: 0, countFailure: 0, countTimeout: 0, countShortCircuited: 0, // Derivate in between two measurements to support counters that only increase (e.g., prom-client) countTotalDeriv: 0, countSuccessDeriv: 0, countFailureDeriv: 0, countTimeoutDeriv: 0, countShortCircuitedDeriv: 0, }; // initialize buckets this._buckets = []; for (let i = 0; i < this._opts.bucketNum; i++) { this._buckets.push(new Bucket(this._cummulative)); } this._activeBucket = this._buckets[this._activePosition]; this._startBucketSpinning(); this._totals = this._generateStats(this._buckets, true); } getCummulateiveSatistics() { return this._cummulative; } reset() { for (let i = 0; i < this._opts.bucketNum; i++) { this._shiftAndPush(this._buckets, new Bucket(this._cummulative)); } this._activeBucket = this._buckets[this._activePosition]; this._update(); } /* Starts cycling through buckets */ _startBucketSpinning() { this._spinningInterval = setInterval(() => { this._shiftAndPush(this._buckets, new Bucket(this._cummulative)); this._activeBucket = this._buckets[this._activePosition]; }, this._opts.bucketSpan); this._spinningInterval.unref(); } /* Stop Bucket from spinning */ _stopBucketSpinning() { if (this._spinningInterval) { clearInterval(this._spinningInterval); this._spinningInterval = undefined; return true; } return false; } /* start generating snapshots */ startSnapshots(interval) { this._snapshotInterval = setInterval( () => { this._snapshot(); }, interval || this._opts.statInterval ); this._snapshotInterval.unref(); } /* stop generating snapshots */ stopSnapshots() { if (this._snapshotInterval) { clearInterval(this._snapshotInterval); this._snapshotInterval = undefined; return true; } return false; } /* Generate new totals `includeLatencyStats` flag determines whether or not to calculate a new round of percentiles. If `includeLatencyStats` is set to false or undefined, the existing calculated percentiles will be preserved. */ _generateStats(buckets, includeLatencyStats) { // reduce buckets const tempTotals = buckets.reduce((prev, cur) => { if (!cur) return prev; // aggregate incremented stats prev.total += cur.total || 0; prev.failed += cur.failed || 0; prev.timedOut += cur.timedOut || 0; prev.successful += cur.successful || 0; prev.shortCircuited += cur.shortCircuited || 0; // concat `requestTimes` Arrays if (includeLatencyStats) { prev.requestTimes.push.apply(prev.requestTimes, cur.requestTimes || []); } return prev; }, { failed: 0, timedOut: 0, total: 0, shortCircuited: 0, latencyMean: 0, successful: 0, requestTimes: [], percentiles: {} }); // calculate percentiles if (includeLatencyStats) { tempTotals.requestTimes.sort((a, b) => a - b); tempTotals.latencyMean = this._calculateMean(tempTotals.requestTimes) || 0; this._opts.percentiles.forEach(p => { tempTotals.percentiles[p] = this._calculatePercentile(p, tempTotals.requestTimes) || 0; }); } else { // pass through previous percentile and mean tempTotals.latencyMean = this._totals.latencyMean; tempTotals.percentiles = this._totals.percentiles; } // remove large totals Arrays delete tempTotals.requestTimes; this._totals = tempTotals; this._totals = Object.assign(this._totals, this._cummulative); return this._totals; } _resetDerivs() { this._cummulative.countTotalDeriv = 0; this._cummulative.countSuccessDeriv = 0; this._cummulative.countFailureDeriv = 0; this._cummulative.countTimeoutDeriv = 0; this._cummulative.countShortCircuitedDeriv = 0; } /* Calculate percentile. This function assumes the list you are giving it is already ordered. */ _calculatePercentile(percentile, array) { if (percentile === 0) { return array[0]; } const idx = Math.ceil(percentile * array.length); return array[idx - 1]; } /* Calculate mean. */ _calculateMean(array) { const sum = array.reduce((a, b) => a + b, 0); return Math.round(sum / array.length); } /* Update totals and send updated event */ _update() { this.emit('update', this._generateStats(this._buckets)); } _shiftAndPush(arr, item) { arr.push(item); arr.shift(); return arr; } /* Send snapshot stats event */ _snapshot() { this.emit('snapshot', this._generateStats(this._buckets, true)); this._resetDerivs(); } /* Register a failure */ failure(runTime) { this._activeBucket.failure(runTime); this._update(); } /* Register a success */ success(runTime) { this._activeBucket.success(runTime); this._update(); } /* Register a short circuit */ shortCircuit() { this._activeBucket.shortCircuit(); this._update(); } /* Register a timeout */ timeout(runTime) { this._activeBucket.timeout(runTime); this._update(); } } module.exports = Stats;