Skip to content

Commit

Permalink
first pass at percentiles, still needs wokr
Browse files Browse the repository at this point in the history
  • Loading branch information
lholmquist committed Apr 12, 2017
1 parent f54822e commit 4c25510
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 29 deletions.
33 changes: 20 additions & 13 deletions lib/circuit.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class CircuitBreaker extends EventEmitter {

if (options.maxFailures) console.error('options.maxFailures is deprecated. Please use options.errorThresholdPercentage');

const increment = property => _ => this[STATUS].increment(property);
const increment = property => (result, runTime) => this[STATUS].increment(property, runTime);

this.on('success', increment('successes'));
this.on('failure', increment('failures'));
Expand Down Expand Up @@ -266,6 +266,9 @@ class CircuitBreaker extends EventEmitter {

let timeout;
let timeoutError = false;

const latencyStartTime = Date.now();

return new this.Promise((resolve, reject) => {
timeout = setTimeout(
() => {
Expand All @@ -275,8 +278,9 @@ class CircuitBreaker extends EventEmitter {
* Emitted when the circuit breaker action takes longer than `options.timeout`
* @event CircuitBreaker#timeout
*/
this.emit('timeout', error);
resolve(fallback(this, error, args) || fail(this, error, args));
const latencyEndTime = Date.now() - latencyStartTime;
this.emit('timeout', error, latencyEndTime);
resolve(fallback(this, error, args, latencyEndTime) || fail(this, error, args, latencyEndTime));
}, this.options.timeout);

try {
Expand All @@ -292,18 +296,21 @@ class CircuitBreaker extends EventEmitter {
* Emitted when the circuit breaker action succeeds
* @event CircuitBreaker#success
*/
this.emit('success', result);
this.emit('success', result, (Date.now() - latencyStartTime));
resolve(result);
if (this.options.cache) {
CACHE.set(this, promise);
}
clearTimeout(timeout);
}
})
.catch((error) =>
handleError(error, this, timeout, args, resolve, reject));
.catch((error) => {
const latencyEndTime = Date.now() - latencyStartTime;
handleError(error, this, timeout, args, latencyEndTime, resolve, reject);
});
} catch (error) {
handleError(error, this, timeout, args, resolve, reject);
const latencyEndTime = Date.now() - latencyStartTime;
handleError(error, this, timeout, args, latencyEndTime, resolve, reject);
}
});
}
Expand All @@ -316,15 +323,15 @@ class CircuitBreaker extends EventEmitter {
}
}

function handleError (error, circuit, timeout, args, resolve, reject) {
function handleError (error, circuit, timeout, args, latencyEndTime, resolve, reject) {
clearTimeout(timeout);
fail(circuit, error, args);
const fb = fallback(circuit, error, args);
fail(circuit, error, args, latencyEndTime);
const fb = fallback(circuit, error, args, latencyEndTime);
if (fb) resolve(fb);
else reject(error);
}

function fallback (circuit, err, args) {
function fallback (circuit, err, args, latencyEndTime) {
if (circuit[FALLBACK_FUNCTION]) {
return new circuit.Promise((resolve, reject) => {
const result = circuit[FALLBACK_FUNCTION].apply(circuit[FALLBACK_FUNCTION], args);
Expand All @@ -338,12 +345,12 @@ function fallback (circuit, err, args) {
}
}

function fail (circuit, err, args) {
function fail (circuit, err, args, latencyEndTime) {
/**
* Emitted when the circuit breaker action fails
* @event CircuitBreaker#failure
*/
circuit.emit('failure', err);
circuit.emit('failure', err, latencyEndTime);

// check stats to see if the circuit should be opened
const stats = circuit.stats;
Expand Down
35 changes: 23 additions & 12 deletions lib/hystrix-formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,31 @@ function hystrixFormatter (stats) {
json.currentConcurrentExecutionCount = 0;
json.rollingMaxConcurrentExecutionCount = 0;
// TODO: caluclate these latency values
json.latencyExecute_mean = 0;
json.latencyExecute_mean = stats.latencyMean || 0;
json.latencyExecute = {
'0': 0,
'25': 0,
'50': 0,
'75': 0,
'90': 0,
'95': 0,
'99': 0,
'99.5': 0,
'100': 0
0: stats.percentiles['0'],
25: stats.percentiles['0.25'],
50: stats.percentiles['0.5'],
75: stats.percentiles['0.75'],
90: stats.percentiles['0.9'],
95: stats.percentiles['0.95'],
99: stats.percentiles['0.99'],
99.5: stats.percentiles['0.995'],
100: stats.percentiles['1']
};
// Whats the difference between execute and total?
json.latencyTotal_mean = stats.latencyMean;
json.latencyTotal = {
0: stats.percentiles['0'],
25: stats.percentiles['0.25'],
50: stats.percentiles['0.5'],
75: stats.percentiles['0.75'],
90: stats.percentiles['0.9'],
95: stats.percentiles['0.95'],
99: stats.percentiles['0.99'],
99.5: stats.percentiles['0.995'],
100: stats.percentiles['1']
};
json.latencyTotal_mean = 0;
json.latencyTotal = { '0': 0, '25': 0, '50': 0, '75': 0, '90': 0, '95': 0, '99': 0, '99.5': 0, '100': 0 };
json.propertyValue_circuitBreakerRequestVolumeThreshold = 5;
json.propertyValue_circuitBreakerSleepWindowInMilliseconds = stats.options.resetTimeout;
json.propertyValue_circuitBreakerErrorThresholdPercentage = stats.options.errorThresholdPercentage;
Expand Down
43 changes: 39 additions & 4 deletions lib/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const WINDOW = Symbol('window');
const BUCKETS = Symbol('buckets');
const TIMEOUT = Symbol('timeout');
const PERCENTILES = Symbol('percentiles');

const EventEmitter = require('events').EventEmitter;

Expand Down Expand Up @@ -60,6 +61,7 @@ class Status extends EventEmitter {
this[BUCKETS] = options.rollingCountBuckets;
this[TIMEOUT] = options.rollingCountTimeout;
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];

// prime the window with buckets
for (let i = 0; i < this[BUCKETS]; i++) this[WINDOW][i] = bucket();
Expand All @@ -81,12 +83,32 @@ class Status extends EventEmitter {
* Get the cumulative stats for the current window
*/
get stats () {
return this[WINDOW].reduce((acc, val) => {
const totals = this[WINDOW].reduce((acc, val) => {
// the window starts with all but one bucket undefined
if (!val) return acc;
Object.keys(acc).forEach(key => (acc[key] += val[key] || 0));
Object.keys(acc).forEach(key => {
if (key !== 'latencyTimes' && key !== 'percentiles') {
(acc[key] += val[key] || 0);
}
});

acc.latencyTimes.push.apply(acc.latencyTimes, val.latencyTimes || []);
return acc;
}, bucket());

// Sort the latencyTimess
totals.latencyTimes.sort((a, b) => a - b);

// Get the mean latency
// Mean = sum of all values in the array/length of array
totals.latencyMean = (totals.latencyTimes.reduce((a, b) => a + b, 0)) / totals.latencyTimes.length;

// Calculate Percentiles
this[PERCENTILES].forEach(percentile => {
totals.percentiles[percentile] = calculatePercentile(percentile, totals.latencyTimes);
});

return totals;
}

/**
Expand All @@ -96,8 +118,11 @@ class Status extends EventEmitter {
return this[WINDOW].slice();
}

increment (property) {
increment (property, latencyRunTime) {
this[WINDOW][0][property]++;
if (property === 'successes' || property === 'failures' || property === 'timeouts') {
this[WINDOW][0].latencyTimes.push(latencyRunTime || 0);
}
}

open () {
Expand All @@ -122,7 +147,17 @@ const bucket = _ => ({
fires: 0,
timeouts: 0,
cacheHits: 0,
cacheMisses: 0
cacheMisses: 0,
percentiles: {},
latencyTimes: []
});

function calculatePercentile (percentile, arr) {
if (percentile === 0) {
return arr[0];
}
const idx = Math.ceil(percentile * arr.length);
return arr[idx - 1];
}

module.exports = exports = Status;

0 comments on commit 4c25510

Please sign in to comment.