@graphql-hive/core
Version:
215 lines (214 loc) • 7.97 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const events_1 = tslib_1.__importDefault(require("events"));
const WINDOW = Symbol('window');
const BUCKETS = Symbol('buckets');
const TIMEOUT = Symbol('timeout');
const PERCENTILES = Symbol('percentiles');
const BUCKET_INTERVAL = Symbol('bucket-interval');
const SNAPSHOT_INTERVAL = Symbol('snapshot-interval');
const ROTATE_EVENT_NAME = Symbol('rotate-event-name');
/**
* Tracks execution status for a given {@link CircuitBreaker}.
* A Status instance is created for every {@link CircuitBreaker}
* and does not typically need to be created by a user.
*
* A Status instance will listen for all events on the {@link CircuitBreaker}
* and track them in a rolling statistical window. The window duration is
* determined by the `rollingCountTimeout` option provided to the
* {@link CircuitBreaker}. The window consists of an array of Objects,
* each representing the counts for a {@link CircuitBreaker}'s events.
*
* The array's length is determined by the {@link CircuitBreaker}'s
* `rollingCountBuckets` option. The duration of each slice of the window
* is determined by dividing the `rollingCountTimeout` by
* `rollingCountBuckets`.
*
* @class Status
* @extends EventEmitter
* @param {Object} options for the status window
* @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
* percentiles
* @param {Object} options.stats object of previous stats
* @example
* // Creates a 1 second window consisting of ten time slices,
* // each 100ms long.
* const circuit = circuitBreaker(fs.readFile,
* { rollingCountBuckets: 10, rollingCountTimeout: 1000});
*
* // get the cumulative statistics for the last second
* circuit.status.stats;
*
* // get the array of 10, 1 second time slices for the last second
* circuit.status.window;
* @fires Status#snapshot
* @see CircuitBreaker#status
*/
class Status extends events_1.default {
constructor(options) {
super();
// Set up our statistical rolling window
this[BUCKETS] = options.rollingCountBuckets || 10;
this[TIMEOUT] = options.rollingCountTimeout || 10000;
this[WINDOW] = new Array(this[BUCKETS]);
this[PERCENTILES] = [0.0, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.995, 1];
this[ROTATE_EVENT_NAME] = 'rotate';
// Default this value to true
this.rollingPercentilesEnabled = options.rollingPercentilesEnabled !== false;
// Default this value to true
this.enableSnapshots = options.enableSnapshots !== false;
// can be undefined
this.rotateBucketController = options.rotateBucketController;
this.rotateBucket = nextBucket(this[WINDOW]);
// prime the window with buckets
for (let i = 0; i < this[BUCKETS]; i++)
this[WINDOW][i] = bucket();
const bucketInterval = Math.floor(this[TIMEOUT] / this[BUCKETS]);
if (this.rotateBucketController) {
// rotate the buckets based on an optional EventEmitter
this.startListeneningForRotateEvent();
}
else {
// or rotate the buckets periodically
this[BUCKET_INTERVAL] = setInterval(this.rotateBucket, bucketInterval);
// No unref() in the browser
if (typeof this[BUCKET_INTERVAL].unref === 'function') {
this[BUCKET_INTERVAL].unref();
}
}
/**
* Emitted at each time-slice. Listeners for this
* event will receive a cumulative snapshot of the current status window.
* @event Status#snapshot
* @type {Object}
*/
if (this.enableSnapshots) {
this[SNAPSHOT_INTERVAL] = setInterval(_ => this.emit('snapshot', this.stats), bucketInterval);
if (typeof this[SNAPSHOT_INTERVAL].unref === 'function') {
this[SNAPSHOT_INTERVAL].unref();
}
}
if (options.stats) {
this[WINDOW][0] = Object.assign(Object.assign({}, bucket()), options.stats);
}
}
/**
* Get the cumulative stats for the current window
* @type {Object}
*/
get stats() {
const totals = this[WINDOW].reduce((acc, val) => {
if (!val) {
return acc;
}
Object.keys(acc).forEach(key => {
if (key !== 'latencyTimes' && key !== 'percentiles') {
acc[key] += val[key] || 0;
}
});
if (this.rollingPercentilesEnabled) {
if (val.latencyTimes) {
acc.latencyTimes = acc.latencyTimes.concat(val.latencyTimes);
}
}
return acc;
}, bucket());
if (this.rollingPercentilesEnabled) {
// Sort the latencyTimes
totals.latencyTimes.sort((a, b) => a - b);
// Get the mean latency
// Mean = sum of all values in the array/length of array
if (totals.latencyTimes.length) {
totals.latencyMean =
totals.latencyTimes.reduce((a, b) => a + b, 0) / totals.latencyTimes.length;
}
else {
totals.latencyMean = 0;
}
// Calculate Percentiles
this[PERCENTILES].forEach(percentile => {
totals.percentiles[percentile] = calculatePercentile(percentile, totals.latencyTimes);
});
}
else {
totals.latencyMean = -1;
this[PERCENTILES].forEach(percentile => {
totals.percentiles[percentile] = -1;
});
}
return totals;
}
/**
* Gets the stats window as an array of time-sliced objects.
* @type {Array}
*/
get window() {
return this[WINDOW].slice();
}
increment(property, latencyRunTime) {
this[WINDOW][0][property]++;
if (property === 'successes' || property === 'failures' || property === 'timeouts') {
this[WINDOW][0].latencyTimes.push(latencyRunTime || 0);
}
}
open() {
this[WINDOW][0].isCircuitBreakerOpen = true;
}
close() {
this[WINDOW][0].isCircuitBreakerOpen = false;
}
shutdown() {
this.removeAllListeners();
// interval is not set if rotateBucketController is provided
if (this.rotateBucketController === undefined) {
clearInterval(this[BUCKET_INTERVAL]);
}
else {
this.removeRotateBucketControllerListener();
}
if (this.enableSnapshots) {
clearInterval(this[SNAPSHOT_INTERVAL]);
}
}
removeRotateBucketControllerListener() {
if (this.rotateBucketController) {
this.rotateBucketController.removeListener(this[ROTATE_EVENT_NAME], this.rotateBucket);
}
}
startListeneningForRotateEvent() {
if (this.rotateBucketController &&
this.rotateBucketController.listenerCount(this[ROTATE_EVENT_NAME], this.rotateBucket) === 0) {
this.rotateBucketController.on(this[ROTATE_EVENT_NAME], this.rotateBucket);
}
}
}
const nextBucket = window => _ => {
window.pop();
window.unshift(bucket());
};
const bucket = _ => ({
failures: 0,
fallbacks: 0,
successes: 0,
rejects: 0,
fires: 0,
timeouts: 0,
cacheHits: 0,
cacheMisses: 0,
coalesceCacheHits: 0,
coalesceCacheMisses: 0,
semaphoreRejections: 0,
percentiles: {},
latencyTimes: [],
});
function calculatePercentile(percentile, arr) {
if (percentile === 0) {
return arr[0] || 0;
}
const idx = Math.ceil(percentile * arr.length);
return arr[idx - 1] || 0;
}
exports.default = Status;