UNPKG

@bennadel/circuit-breaker

Version:

A flexible circuit breaker for Node.js (requires ES6 class modules).

809 lines (570 loc) 16 kB
// Require the core node modules. var expect = require( "chai" ).expect; // Require the application modules. var CircuitBreaker = require( "../lib/CircuitBreaker" ); var InMemoryMonitor = require( "../lib/monitor/InMemoryMonitor" ); var Metrics = require( "../lib/metrics/Metrics" ); var OpenError = require( "../lib/error/OpenError" ); var State = require( "../lib/state/State" ); var TimeoutError = require( "../lib/error/TimeoutError" ); // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // var bucketCount = 2; var bucketDuration = 100; var metrics = null; var monitor = new InMemoryMonitor(); var state = null; var circuitBreaker = null; var error = new Error( "testing" ); var invalidBranchError = new Error( "test should not reach this branch." ); var entireWindow = ( bucketCount * bucketDuration ); var almostEntireWindow = ( entireWindow - 30 ); var context = { value: Date.now(), valueMethod: function() { return( this.value ); }, echoMethod: function( value ) { return( value ); }, throwMethod: function() { throw( error ); }, rejectMethod: function() { return( Promise.reject( error ) ); } }; describe( "Testing lib.CircuitBreaker", function() { describe( "with no global fallback", function() { var requestTimeout = 30; var volumeThreshold = 10; var failureThreshold = 5; // 5 percent. var activeThreshold = 20; beforeEach( function() { monitor.clearEvents(); metrics = new Metrics( bucketCount, bucketDuration ); state = new State({ id: "testingState", requestTimeout: requestTimeout, volumeThreshold: volumeThreshold, failureThreshold: failureThreshold, activeThreshold: activeThreshold, monitor: monitor, metrics: metrics }); circuitBreaker = new CircuitBreaker( state ); } ); it( "should execute a function reference.", function( done ) { circuitBreaker .execute( function() { return( 1 ); } ) .then( function( result ) { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); it( "should execute a function reference in a given context.", function( done ) { circuitBreaker .executeInContext( context, context.valueMethod ) .then( function( result ) { expect( result ).to.equal( context.value ); } ) .then( nullify( done ), done ) ; }); it( "should execute a method.", function( done ) { circuitBreaker .executeMethod( context, "valueMethod", [] ) .then( ( result ) => { expect( result ).to.equal( context.value ); } ) .then( nullify( done ), done ) ; }); it( "should use a fallback when executing a function reference.", function( done ) { var fallbackValue = "fallbackValue"; circuitBreaker .execute( context.throwMethod, fallbackValue ) .then( function( result ) { expect( result ).to.equal( fallbackValue ); } ) .then( nullify( done ), done ) ; }); it( "should use a fallback when executing a function reference in a given context.", function( done ) { var fallbackValue = "fallbackValue"; circuitBreaker .executeInContext( context, context.throwMethod, [], fallbackValue ) .then( function( result ) { expect( result ).to.equal( fallbackValue ); } ) .then( nullify( done ), done ) ; }); it( "should use a fallback when executing a method.", function( done ) { var fallbackValue = "fallbackValue"; circuitBreaker .executeMethod( context, "throwMethod", [], fallbackValue ) .then( ( result ) => { expect( result ).to.equal( fallbackValue ); } ) .then( nullify( done ), done ) ; }); it( "should allow returning a promise value.", function( done ) { circuitBreaker .execute( function() { return( Promise.resolve( 1 ) ); } ) .then( ( result ) => { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); it( "should parle rejected promise value into error.", function( done ) { circuitBreaker .execute( function() { return( Promise.reject( error ) ); } ) .then( function( result ) { throw( invalidBranchError ); }, function( result ) { expect( result ).to.equal( error ); } ) .then( nullify( done ), done ) ; }); it( "should allow returning a promise fallback value.", function( done ) { circuitBreaker .execute( context.throwMethod, function() { return( Promise.resolve( 1 ) ); } ) .then( ( result ) => { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); it( "should allow using a promise fallback value.", function( done ) { var fallback = Promise.resolve( 1 ); circuitBreaker .execute( context.throwMethod, fallback ) .then( ( result ) => { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); it( "should pass arguments to function.", function( done ) { circuitBreaker .executeInContext( null, context.echoMethod, [ 1 ] ) .then( ( result ) => { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); it( "should pass arguments to method.", function( done ) { circuitBreaker .executeMethod( context, "echoMethod", [ 1 ] ) .then( ( result ) => { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); it( "should pass arguments to function fallback.", function( done ) { circuitBreaker .executeInContext( null, context.throwMethod, [ 1 ], context.echoMethod ) .then( ( result ) => { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); it( "should pass arguments to method fallback.", function( done ) { circuitBreaker .executeMethod( context, "throwMethod", [ 1 ], context.echoMethod ) .then( ( result ) => { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); it( "should short-circuit when failing.", function( done ) { var promise = Promise.resolve(); // Get to just under the volume threshold of errors. for ( var i = 0 ; i < ( volumeThreshold - 1 ) ; i++ ) { promise = promise.then( function() { var executePromise = circuitBreaker.execute( context.throwMethod, 1 ); return( executePromise ); } ); } promise .then( function() { expect( circuitBreaker.isClosed() ).to.be.true; }, function() { throw( invalidBranchError ); } ) .then( function() { // Reach the failure threshold and the volume threshold. var executePromise = circuitBreaker.execute( context.throwMethod, 1 ); return( executePromise ); } ) .then( function() { expect( circuitBreaker.isOpened() ).to.be.true; var executePromise = circuitBreaker.execute( function() { return( 1 ); } ); return( executePromise ); } ) .then( function() { throw( invalidBranchError ); }, function( resultError ) { expect( resultError instanceof OpenError ).to.be.true; } ) .then( nullify( done ), done ) ; }); // ------------------------------------------------------------------------------- // // ------------------------------------------------------------------------------- // // CAUTION: The following tests use requests that have to hang for some period of // time in order to test capacity and timeout restrictions. // ------------------------------------------------------------------------------- // // ------------------------------------------------------------------------------- // it( "should log events to the monitor.", function( done ) { Promise .all([ circuitBreaker.executeMethod( context, "valueMethod" ), circuitBreaker.executeMethod( context, "rejectMethod", [], "fallback value" ), circuitBreaker.executeInContext( null, timer, [ requestTimeout + 10 ] ).catch( context.echoMethod ) ]) .then( function() { var events = monitor.getEvents(); var i = 0; expect( events[ i++ ].type ).to.equal( "emit" ); expect( events[ i++ ].type ).to.equal( "execute" ); expect( events[ i++ ].type ).to.equal( "emit" ); expect( events[ i++ ].type ).to.equal( "execute" ); expect( events[ i++ ].type ).to.equal( "emit" ); expect( events[ i++ ].type ).to.equal( "execute" ); expect( events[ i++ ].type ).to.equal( "success" ); expect( events[ i++ ].type ).to.equal( "failure" ); expect( events[ i++ ].type ).to.equal( "fallbackEmit" ); expect( events[ i++ ].type ).to.equal( "fallbackSuccess" ); expect( events[ i++ ].type ).to.equal( "timeout" ); expect( events[ i++ ].type ).to.equal( "fallbackEmit" ); expect( events[ i++ ].type ).to.equal( "fallbackMissing" ); expect( events.length ).to.equal( i ); } ) .then( function() { var promise = timer( requestTimeout - 10 ); var promises = []; // Exhaust AND EXCEED active threshold by one. for ( var i = 0 ; i <= activeThreshold ; i++ ) { monitor.clearEvents(); promises.push( circuitBreaker.executeMethod( context, "echoMethod", [ promise ], "fallback value" ) ); } return( Promise.all( promises ) ); } ) .then( function() { var events = monitor.getEvents(); var i = 0; expect( events[ i++ ].type ).to.equal( "opened" ); expect( events[ i++ ].type ).to.equal( "emit" ); expect( events[ i++ ].type ).to.equal( "closed" ); expect( events[ i++ ].type ).to.equal( "shortCircuited" ); expect( events[ i++ ].type ).to.equal( "fallbackEmit" ); expect( events[ i++ ].type ).to.equal( "fallbackSuccess" ); } ) .then( nullify( done ), done ) ; }); it( "should timeout request.", function( done ) { circuitBreaker .execute( function() { return( timer( requestTimeout + 30 ) ); } ) .then( function() { throw( invalidBranchError ); }, function( resultError ) { expect( resultError instanceof TimeoutError ).to.be.true; } ) .then( nullify( done ), done ) ; }); it( "should short-circuit when at capacity.", function( done ) { var promise = timer( requestTimeout - 1 ); var promises = []; // Exhaust active threshold. for ( var i = 0 ; i < activeThreshold ; i++ ) { promises.push( circuitBreaker .execute( function() { return( promise ); } ) .then( function( result ) { expect( result ).to.equal( undefined ); } ) ); } // Push to over-capacity. promises.push( circuitBreaker .execute( function() { return( promise ); } ) .then( function( result ) { throw( invalidBranchError ); }, function( resultError ) { expect( resultError instanceof OpenError ).to.be.true; } ) ); Promise .all(promises) .then( nullify( done ), done ) ; }); it( "should wait until a health check request can be sent.", function( done ) { var promises = []; // Exhaust the error threshold. for ( var i = 0 ; i < volumeThreshold ; i++ ) { promises.push( circuitBreaker.execute( function() { throw( error ); }, 1 // Fallback so it doesn't "reject" later down. ) ); } Promise .all( promises ) .then( function() { expect( circuitBreaker.isClosed() ).to.be.false; expect( circuitBreaker.isOpened() ).to.be.true; return( timer( almostEntireWindow ) ); } ) .then( function() { // We should still be in the failure window, so this should error. var promise = circuitBreaker.execute( function() { return( 1 ); }, 2 // Fallback so it doesn't "reject" later down. ); return( promise ); } ) .then( function( result ) { expect( result ).to.equal( 2 ); return( timer( entireWindow + 10 ) ); } ) .then( function() { // We should be beyond the failure window, so this should work. var promise = circuitBreaker.execute( function() { return( 1 ); } ); return( promise ); } ) .then( function( result ) { expect( result ).to.equal( 1 ); } ) .then( nullify( done ), done ) ; }); }); describe( "with a global fallback", function() { var requestTimeout = 30; var volumeThreshold = 10; var failureThreshold = 5; // 5 percent. var activeThreshold = 20; var globalFallback = "global fallback"; var fallbackOverride = "local fallback"; beforeEach( function() { monitor.clearEvents(); metrics = new Metrics( bucketCount, bucketDuration ); state = new State({ id: "testingState", requestTimeout: requestTimeout, volumeThreshold: volumeThreshold, failureThreshold: failureThreshold, activeThreshold: activeThreshold, monitor: monitor, metrics: metrics }); circuitBreaker = new CircuitBreaker( state, globalFallback ); } ); it( "should use global fallback when executing a function reference.", function( done ) { circuitBreaker .execute( context.throwMethod ) .then( function( result ) { expect( result ).to.equal( globalFallback ); } ) .then( nullify( done ), done ) ; }); it( "should use global fallback when executing a function reference in a given context.", function( done ) { circuitBreaker .executeInContext( context, context.throwMethod, [] ) .then( function( result ) { expect( result ).to.equal( globalFallback ); } ) .then( nullify( done ), done ) ; }); it( "should use global fallback when executing a method.", function( done ) { circuitBreaker .executeMethod( context, "throwMethod", [] ) .then( ( result ) => { expect( result ).to.equal( globalFallback ); } ) .then( nullify( done ), done ) ; }); it( "should use local fallback override when executing a function reference.", function( done ) { circuitBreaker .execute( context.throwMethod, fallbackOverride ) .then( function( result ) { expect( result ).to.equal( fallbackOverride ); } ) .then( nullify( done ), done ) ; }); it( "should use local fallback override when executing a function reference in a given context.", function( done ) { circuitBreaker .executeInContext( context, context.throwMethod, [], fallbackOverride ) .then( function( result ) { expect( result ).to.equal( fallbackOverride ); } ) .then( nullify( done ), done ) ; }); it( "should use local fallback override when executing a method.", function( done ) { circuitBreaker .executeMethod( context, "throwMethod", [], fallbackOverride ) .then( ( result ) => { expect( result ).to.equal( fallbackOverride ); } ) .then( nullify( done ), done ) ; }); }); }); function nullify( done ) { return( function doneProxy() { done(); } ); } function timer( timeout ) { var promise = new Promise( function( resolve, reject ) { setTimeout( resolve, timeout ); } ); return( promise ); }