UNPKG

trooba-hystrix-handler

Version:

The handler provides hystrix functionality to trooba request/response pipeline

955 lines (848 loc) 29.6 kB
'use strict'; const Assert = require('assert'); const Trooba = require('trooba'); const Async = require('async'); const Hystrix = require('hystrixjs'); const commandFactory = Hystrix.commandFactory; const metricsFactory = Hystrix.metricsFactory; const circuitFactory = Hystrix.circuitFactory; const HystrixDashboard = require('hystrix-dashboard'); const handler = require('..'); describe(__filename, () => { let metrics = []; before(() => { process.on('trooba:hystrix:data', data => metrics.push(JSON.parse(data))); HystrixDashboard.Utils.toObservable(Hystrix, 2000).subscribe( sseData => process.emit('trooba:hystrix:data', sseData), err => {}, () => {} ); }); after(() => { metricsFactory.resetCache(); circuitFactory.resetCache(); commandFactory.resetCache(); }); it('should handle request', next => { const pipe = Trooba.use(handler, { command: 'foo' }) .use(pipe => { pipe.on('request', request => { pipe.respond(request); }); }) .build(); pipe.create().request('hello', (err, response) => { if (err) { next(err); return; } Assert.ok(!err, err && err.stack); Assert.equal('hello', response); next(); }); }); it('should catch error', next => { const pipe = Trooba.use(handler, { command: 'foo' }) .use(pipe => { pipe.on('request', request => { pipe.throw(new Error('Boom')); }); }) .build(); pipe.create().request('hello', err => { Assert.ok(err); Assert.equal('Boom', err.message); next(); }); }); it('should catch timeout error', next => { const pipe = Trooba.use(handler, { command: 'timeout', timeout: 1 }) .use(pipe => { pipe.on('request', request => { }); }) .build(); pipe.create().request('hello', err => { Assert.ok(err); Assert.equal('CommandTimeOut', err.message); next(); }); }); describe('retry', () => { it('should handle retry logic', next => { var requestCounter = 0; const pipe = Trooba .use(pipe => { var count = 0; var _request; pipe.on('request', (request, next) => { _request = request; next(); }); pipe.on('response', (response, next) => { if (count++ < 1) { pipe.request(_request); return; } next(); }); }) .use(handler, { command: 'foo' }) .use(pipe => { pipe.on('request', request => { requestCounter++; pipe.respond(request); }); }) .build(); pipe.create().request('hello', (err, response) => { if (err) { next(err); return; } Assert.ok(!err, err && err.stack); Assert.equal('hello', response); Assert.equal(2, requestCounter); next(); }); }); }); describe('fallback', () => { it('should return error when no fallback available', next => { const pipe = Trooba.use(handler, { command: 'foo' }) .use(pipe => { pipe.on('request', request => { pipe.throw(new Error('Boom')); }); }) .build(); pipe.create().request('hello', err => { Assert.ok(err); Assert.equal('Boom', err.message); next(); }); }); it('should catch error and do fallback provided via config', next => { const pipe = Trooba.use(handler, { command: 'foo', fallback: (err, request) => { Assert.equal('hello', request); return Promise.resolve('fallback'); } }) .use(pipe => { pipe.on('request', request => { pipe.throw(new Error('Boom')); }); }) .build(); pipe.create().request('hello', (err, res) => { Assert.ok(!err, err && err.stack); Assert.equal('fallback', res); next(); }); }); it('should catch error and do fallback with resolve', next => { const pipe = Trooba.use(handler, { command: 'foo' }) .use(pipe => { pipe.on('request', request => { pipe.throw(new Error('Boom')); }); }) .build({ fallback: (err, request) => { Assert.equal('hello', request); return Promise.resolve('fallback'); } }); pipe.create().request('hello', (err, res) => { Assert.equal('fallback', res); next(); }); }); it('should catch error and do fallback with reject', next => { const pipe = Trooba.use(handler, { command: 'foo' }) .use(pipe => { pipe.on('request', request => { pipe.throw(new Error('Boom')); }); }) .build({ fallback: (err, request) => { Assert.equal('hello', request); return Promise.reject(new Error('Fallback boom')); } }); pipe.create().request('hello', err => { Assert.equal('Fallback boom', err.message); next(); }); }); it('should use fallback from context', next => { const pipe = Trooba.use(handler, { command: 'foo' }) .use(pipe => { pipe.on('request', request => { pipe.throw(new Error('Boom')); }); }) .build(); pipe.create({ fallback: (err, args) => { return Promise.resolve('ctx_fallback'); } }).request('hello', (err, res) => { Assert.equal('ctx_fallback', res); next(); }); }); }); it('should have metrics published', next => { process.once('trooba:hystrix:data', () => { Assert.equal(1, metrics.length); Assert.equal('HystrixCommand', metrics[0].type); Assert.equal(1, metrics[0].errorCount); Assert.equal(2, metrics[0].requestCount); Assert.equal(1, metrics[0].rollingCountFailure); Assert.equal(1, metrics[0].rollingCountSuccess); next(); }); }); describe('command context', () => { after(() => { metricsFactory.resetCache(); circuitFactory.resetCache(); commandFactory.resetCache(); }); it('should fail without command name', () => { Assert.throws(() => { Trooba.use(handler) .use(pipe => { pipe.on('request', request => { pipe.respond(request); }); }) .build() .create() .request(); }, /Command name should be provided/); }); it('should use command name and group from context', next => { Trooba.use(handler) .use(pipe => { pipe.on('request', request => { pipe.respond(request); }); }) .build() .create({ command: 'foo', commandGroup: 'bar' }) .request({}, next); }); }); describe('circuit', () => { let RATE = 5; let counter = 0; let latency = 100; const pipe = Trooba.use(handler, { command: 'op1', circuitBreakerSleepWindowInMilliseconds: 1000 }) .use(pipe => { pipe.on('request', request => { setTimeout(() => { counter++; if (counter % RATE === 0) { pipe.throw(new Error('Boom')); return; } pipe.respond(request); }, latency); }); }) .build(); before(() => { metricsFactory.resetCache(); circuitFactory.resetCache(); commandFactory.resetCache(); metrics = []; }); it('should do a lot of requests, 20% failure rate', next => { counter = 0; Async.times(300, (index, next) => { pipe.create().request('hello', (err, response) => { if (err) { Assert.equal('Boom', err.message); next(); return; } Assert.ok(!err, err && err.stack); Assert.equal('hello', response); next(); }); }, () => next()); }); it('wait for metrics publish', next => process.once('trooba:hystrix:data', () => next())); it('should accumulate stats', next => { Assert.equal(20, metrics[0].errorPercentage); Assert.equal(false, metrics[0].isCircuitBreakerOpen); metrics = []; next(); }); it('should open circuit', next => { RATE = 1; // 100% of failures latency = 100; counter = 0; Async.times(300, (index, next) => { pipe.create().request('hello', (err, response) => { if (err) { Assert.equal('Boom', err.message); next(); return; } Assert.ok(!err, err && err.stack); Assert.equal('hello', response); next(); }); }, next); }); it('wait for metrics publish', next => process.once('trooba:hystrix:data', () => next())); it('validate open circuit', () => { Assert.equal(true, metrics[0].isCircuitBreakerOpen); Assert.equal(60, metrics[0].errorPercentage); metrics = []; }); it('wait for metrics publish', next => process.once('trooba:hystrix:data', () => next())); it('should keep circuit open', () => { Assert.equal(true, metrics[0].isCircuitBreakerOpen); Assert.equal(60, metrics[0].errorPercentage); metrics = []; }); it('should close circuit', next => { RATE = 200; // disable failure counter = 0; Async.times(100, (index, next) => { pipe.create().request('hello', (err, response) => { if (err) { Assert.equal('OpenCircuitError', err.message); next(); return; } Assert.ok(!err, err && err.stack); Assert.equal('hello', response); next(); }); }, next); }); it('wait for metrics publish', next => process.once('trooba:hystrix:data', () => next())); it('validate closed circuit', () => { Assert.equal(false, metrics.pop().isCircuitBreakerOpen); metrics = []; }); it('wait for metrics publish', next => process.once('trooba:hystrix:data', () => next())); it('should accumulate above stats', () => { Assert.ok(metrics[0].latencyExecute['99.5'] > 100); }); it('should force open', next => { RATE = 1000; // 100% of failures counter = 0; circuitFactory.getOrCreate({ commandKey: 'op1' }).circuitBreakerForceOpened = true; pipe.create().request('hello', (err, response) => { if (err) { Assert.equal('OpenCircuitError', err.message); next(); return; } next(new Error('Should have failed')); }); }); it('should be back to regular', next => { RATE = 1000; // 100% of failures counter = 0; const cb = circuitFactory.getOrCreate({ commandKey: 'op1' }); cb.circuitBreakerForceClosed = false; cb.circuitBreakerForceOpened = false; pipe.create().request('hello', (err, response) => { if (err) { next(new Error('Should not have fail')); return; } next(); }); }); it('should force close', next => { RATE = 1000; // 100% of failures counter = 0; const cb = circuitFactory.getOrCreate({ commandKey: 'op1' }); cb.circuitBreakerForceClosed = true; cb.circuitBreakerForceOpened = false; pipe.create().request('hello', (err, response) => { if (err) { next(new Error('Should not fail')); return; } next(); }); }); it('should be back to regular, success', next => { RATE = 1000; // 100% of failures counter = 0; const cb = circuitFactory.getOrCreate({ commandKey: 'op1' }); cb.circuitBreakerForceClosed = false; cb.circuitBreakerForceOpened = false; pipe.create().request('hello', (err, response) => { if (err) { next(new Error('Should not fail')); return; } next(); }); }); it('should force close and never open', next => { RATE = 1; // 100% of failures counter = 0; const cb = circuitFactory.getOrCreate({ commandKey: 'op1' }); cb.circuitBreakerForceClosed = true; cb.circuitBreakerForceOpened = false; Async.times(600, (index, next) => { pipe.create().request('hello', (err, response) => { if (err) { Assert.equal('Boom', err.message); next(); return; } next(new Error('Should have failed')); }); }, next); }); it('should be back to regular, fail', next => { RATE = 1000; // 100% of failures counter = 0; const cb = circuitFactory.getOrCreate({ commandKey: 'op1' }); cb.circuitBreakerForceClosed = false; cb.circuitBreakerForceOpened = false; pipe.create().request('hello', (err, response) => { if (err) { Assert.equal('OpenCircuitError', err.message); next(); return; } next(new Error('Should fail')); }); }); }); describe('circuit, multiple services', () => { let RATE = 5; let counter = 0; let latency = 100; let pipes = []; const transport = pipe => { pipe.on('request', request => { setTimeout(() => { counter++; if (counter % RATE === 0) { pipe.throw(new Error('Boom')); return; } pipe.respond(request); }, latency); }); }; for (var i = 0; i < 10; i++) { const pipe = Trooba.use(handler, { command: 'op' + i, circuitBreakerSleepWindowInMilliseconds: 1000 }) .use(transport) .build(); pipes.push(pipe); } before(() => { metricsFactory.resetCache(); circuitFactory.resetCache(); commandFactory.resetCache(); metrics = []; }); it('should do a lot of requests, 20% failure rate', next => { counter = 0; RATE = 5; Async.times(100, (index, next) => { Async.each(pipes, (pipe, next, index) => { setTimeout(() => { pipe.create().request('hello', (err, response) => { if (err) { Assert.equal('Boom', err.message); next(); return; } Assert.ok(!err, err && err.stack); Assert.equal('hello', response); next(); }); }, Math.round(Math.seededRandom(0, 10))); }, next); }, () => next()); }); it('wait for metrics publish', next => process.once('trooba:hystrix:data', () => next())); it('check stats', () => { // Assert.equal(false, metrics.pop().isCircuitBreakerOpen); Assert.equal(10, metrics.length); metrics.forEach(metric => { Assert.ok(metric.errorPercentage <= 35 && metric.errorPercentage >= 5, `Actual value ${metric.errorPercentage}`); Assert.equal(100, metric.requestCount, `Actual ${metric}`); }); metrics = []; }); }); describe('streaming', () => { it('should handle response stream', next => { const pipe = Trooba.use(handler, { command: 'foo' }) .use(pipe => { pipe.on('request', request => { pipe.streamResponse(request) .write('data1') .write('data2') .end(); }); }) .build(); let _response; const _data = []; pipe.create().request('hello') .on('error', next) .on('response', (response, next) => { _response = response; next(); }) .on('response:data', (data, next) => { _data.push(data); next(); }) .on('response:end', () => { Assert.equal('hello', _response); Assert.deepEqual(['data1', 'data2', undefined], _data); next(); }); }); it('should handle stream data and preserve data order', next => { const pipe = Trooba .use(pipe => { let _response; pipe.on('response', (response, next) => { _response = response; Assert.equal('pong', _response); next(); }); pipe.on('response:data', (data, next) => { Assert.equal('pong', _response); if (data === undefined) { return next(); } Assert.ok(data === 'data1' || data === 'data2'); next(); }); }) .use(handler, { command: 'foo' }) .use(pipe => { pipe.once('request', request => { const stream = pipe.streamResponse('pong'); stream.write('data1'); stream.write('data2'); stream.end(); }); }) .build(); pipe.create().request('ping') .once('error', next) .once('response', response => { Assert.equal('pong', response); next(); }) .on('response:data', (data, next) => { if (data === undefined) { return next(); } Assert.ok(data === 'data1' || data === 'data2'); next(); }); }); it('should catch error', done => { const pipe = Trooba .use(handler, { command: 'foo' }) .use(pipe => { pipe.on('response:data', (data, next) => { if (data === 'data2') { pipe.throw(new Error('Boom')); return; } next(); }); }) .use(pipe => { pipe.on('request', request => { pipe.streamResponse(request) .write('data1') .write('data2') .end(); }); }) .build(); let _response; const _data = []; let _err; pipe.create().request('hello') .on('error', err => { _err = err; Assert.ok(_err); Assert.equal('Boom', _err.message); Assert.equal('hello', _response); Assert.deepEqual(['data1'], _data); done(); }) .on('response', (response, next) => { _response = response; next(); }) .on('response:data', (data, next) => { _data.push(data); next(); }) .on('response:end', () => done(new Error('Should never happen'))); }); it('should catch timeout error', done => { const pipe = Trooba.use(handler, { command: 'foo1', timeout: 1 }) .use(pipe => { pipe.on('request', request => { }); }) .build(); pipe.create().request('hello') .on('error', err => { Assert.ok(err); Assert.equal('CommandTimeOut', err.message); done(); }); }); it('should catch error and stop the pipe execution at this point', done => { const pipe = Trooba .use(handler, { command: 'foo' }) .use(pipe => { pipe.on('response:data', (data, next) => { if (data === 'data1') { return next(); } if (data === 'data2') { pipe.throw(new Error('Boom')); } }); }) .use(pipe => { pipe.on('request', request => { pipe.streamResponse(request) .write('data1') .write('data2') .end(); }); }) .build(); let _response; const _data = []; let _err; pipe.create().request('hello') .on('error', err => { _err = err; Assert.ok(_err); Assert.equal('Boom', _err.message); Assert.equal('hello', _response); Assert.deepEqual(['data1'], _data); done(); }) .on('response', (response, next) => { _response = response; next(); }) .on('response:data', (data, next) => { _data.push(data); next(); }) .on('response:end', () => done(new Error('Should never happen'))); }); it('should catch error and stop the pipe execution at this point and ignore fallback as stream has been flushed already', done => { const pipe = Trooba .use(handler, { command: 'foo' }) .use(pipe => { pipe.on('response:data', (data, next) => { if (data === 'data1') { return next(); } if (data === 'data2') { pipe.throw(new Error('Boom')); } }); }) .use(pipe => { pipe.on('request', request => { pipe.streamResponse(request) .write('data1') .write('data2') .end(); }); }) .build({ fallback: (err, request) => { done(new Error('Should not happen')); } }); let _response; const _data = []; let _err; pipe.create().request('hello') .on('error', err => { _err = err; Assert.ok(_err); Assert.equal('Boom', _err.message); Assert.equal('hello', _response); Assert.deepEqual(['data1'], _data); done(); }) .on('response', (response, next) => { _response = response; next(); }) .on('response:data', (data, next) => { _data.push(data); next(); }) .on('response:end', () => done(new Error('Should never happen'))); }); it('should catch error and do a fallback', done => { const pipe = Trooba .use(handler, { command: 'foo' }) .use(pipe => { pipe.on('response', (data, next) => { pipe.throw(new Error('Boom')); }); }) .use(pipe => { pipe.on('request', request => { pipe.streamResponse(request) .write('data1') .write('data2') .end(); }); }) .build({ fallback: (err, request) => { Assert.ok(err); Assert.equal('Boom', err.message); return Promise.resolve('fallback'); } }); pipe.create().request('hello') .on('error', err => { done(new Error('Should not happen')); }) .on('response', (response, next) => { Assert.equal('fallback', response); done(); next(); }) .on('response:data', (data, next) => { done(new Error('Should not happen')); }) .on('response:end', () => { done(new Error('Should not happen')); }); }); it('should handle request stream', next => { const pipe = Trooba.use(handler, { command: 'foo2' }) .use(pipe => { pipe.on('request', request => { setImmediate(() => { pipe.streamResponse(request) .write('data1') .write('data2') .end(); }); }); }) .build(); let _response; const _data = []; pipe.create().streamRequest('hello') .write('data1') .end() .on('error', next) .on('response', (response, next) => { _response = response; next(); }) .on('response:data', (data, next) => { _data.push(data); next(); }) .on('response:end', () => { Assert.equal('hello', _response); Assert.deepEqual(['data1', 'data2', undefined], _data); next(); }); }); }); }); Math.seed = 6; // in order to work 'Math.seed' must NOT be undefined, // so in any case, you HAVE to provide a Math.seed Math.seededRandom = function(max, min) { max = max || 1; min = min || 0; Math.seed = (Math.seed * 9301 + 49297) % 233280; var rnd = Math.seed / 233280; return min + rnd * (max - min); };