From af4a775155b373c5f55b48cf1edc1d03845bd864 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Fri, 22 May 2015 16:22:58 -0400 Subject: [PATCH] AutoRetry delay implementation --- src/dapi/AutoRetry.js | 63 +++++++++++------- src/dapi/http/XhrHttpImpl.js | 1 - test/dapi/AutoRetryTest.js | 120 +++++++++++++++++++++++++++++++++-- 3 files changed, 155 insertions(+), 29 deletions(-) diff --git a/src/dapi/AutoRetry.js b/src/dapi/AutoRetry.js index 98155a5..77048c0 100644 --- a/src/dapi/AutoRetry.js +++ b/src/dapi/AutoRetry.js @@ -40,10 +40,10 @@ module.exports = Trait( 'AutoRetry' ) 'private _tries': 0, /** - * Delay in milliseconds before making the nth request as a function - * of n + * Function to be passed a continuation to introduce a delay between + * requests * - * @var {function(number): number} + * @var {function(number,function(),function())} delay */ 'private _delay': null, @@ -52,33 +52,42 @@ module.exports = Trait( 'AutoRetry' ) * Initialize auto-retry * * If TRIES is negative, then requests will continue indefinitely until - * one succeeds. If TRIES is 0, then no requests will be performed. + * one succeeds or is aborted by DELAY. If TRIES is 0, then no requests + * will be performed. * - * @param {function(?Error,*): boolean} pred predicate determining if - * a retry is needed - * @param {number} tries maximum number of tries, - * including the initial - * request - * @param {function(number): number} delay delay in milliseconds before - * making the nth request as - * a function of n + * If DELAY is a function, then it invoked with a retry continuation + * before each retry, the number of tries remaining, and a failure + * continuation that may be used to abort the process at an arbitrary + * time. * - * @return {undefined} - */ + * @param {function(?Error,*): boolean} pred predicate determining if + * a retry is needed + * @param {number} tries maximum number of tries, + * including the initial + * request + * + * @param {?function(number,function(),function())} delay + * an optional function + * accepting a continuation + * to continue with the next + * request + * + * @return {undefined} + */ __mixin: function( pred, tries, delay ) { if ( typeof pred !== 'function' ) { throw TypeError( 'Predicate must be a function' ); } - if ( typeof delay !== 'function' ) + if ( delay && ( typeof delay !== 'function' ) ) { throw TypeError( "Delay must be a function" ); } this._pred = pred; this._tries = +tries; - this._delay = delay; + this._delay = delay || function( _, c ) { c(); }; }, @@ -96,9 +105,8 @@ module.exports = Trait( 'AutoRetry' ) * data from the final request are returned. * * If the number of tries is negative, then requests will be performed - * indefinitely until success. - * - * TODO: A means of aborting. + * indefinitely until success; the delay function (as provided via the + * constructor) may be used to abort in this case. * * @param {string} input binary data to transmit * @param {function(?Error,*)} callback continuation upon reply @@ -144,14 +152,26 @@ module.exports = Trait( 'AutoRetry' ) return _self._succeed( output, callback ); } + var fail = function() + { + _self._fail( err, output, callback ); + }; + // note that we intentionally do not want to check <= 1, so that // we can proceed indefinitely (JavaScript does not wrap on overflow) if ( n === 1 ) { - return _self._fail( err, output, callback ); + return fail(); } - _self._try( input, callback, ( n - 1 ) ); + _self._delay( + ( n - 1 ), + function() + { + _self._try( input, callback, ( n - 1 ) ); + }, + fail + ); } ); }, @@ -184,4 +204,3 @@ module.exports = Trait( 'AutoRetry' ) callback( err, output ); }, } ); - diff --git a/src/dapi/http/XhrHttpImpl.js b/src/dapi/http/XhrHttpImpl.js index f0d0e08..e6ed598 100644 --- a/src/dapi/http/XhrHttpImpl.js +++ b/src/dapi/http/XhrHttpImpl.js @@ -194,4 +194,3 @@ module.exports = Class( 'XhrHttpImpl' ) ); } } ); - diff --git a/test/dapi/AutoRetryTest.js b/test/dapi/AutoRetryTest.js index a5e6ab2..8b10529 100644 --- a/test/dapi/AutoRetryTest.js +++ b/test/dapi/AutoRetryTest.js @@ -42,7 +42,7 @@ describe( 'dapi.AutoRetry trait', function() // success (but note the number of retries presented) var stub = _createStub( null, '' ) - .use( Sut( _void, 5, _void ) ) + .use( Sut( _void, 5 ) ) (); stub.request( given, function() @@ -62,7 +62,7 @@ describe( 'dapi.AutoRetry trait', function() // notice that we provide an error to the stub; this will ensure // that the returned error is null even when one is provided var stub = _createStub( {}, chk ) - .use( Sut( _void, 1, _void ) ) + .use( Sut( _void, 1 ) ) (); stub.request( '', function( err, data ) @@ -85,7 +85,7 @@ describe( 'dapi.AutoRetry trait', function() var n = 5; var stub = _createStub( {}, {} ) - .use( Sut( _true, n, _void ) ) + .use( Sut( _true, n ) ) (); stub.request( {}, function( err, _ ) @@ -104,7 +104,7 @@ describe( 'dapi.AutoRetry trait', function() // XXX: this does not test for most recent, because the return // data are static for each request var stub = _createStub( e, output ) - .use( Sut( _true, 1, _void ) ) + .use( Sut( _true, 1 ) ) (); stub.request( {}, function( err, data ) @@ -127,7 +127,7 @@ describe( 'dapi.AutoRetry trait', function() }; var stub = _createStub() - .use( Sut( pred, -1, _void ) ) + .use( Sut( pred, -1 ) ) (); stub.request( {}, function( _, __ ) @@ -145,7 +145,7 @@ describe( 'dapi.AutoRetry trait', function() it( 'will perform zero requests with null results', function( done ) { var stub = _createStub( {}, {} ) - .use( Sut( _void, 0, _void ) ) + .use( Sut( _void, 0 ) ) (); stub.request( {}, function( err, data ) @@ -157,6 +157,114 @@ describe( 'dapi.AutoRetry trait', function() } ); } ); } ); + + + describe( 'when a delay function is provided', function() + { + it( 'will wait for continuation before retry', function( done ) + { + var waited = false; + + var wait = function( _, c ) + { + waited = true; + c(); + }; + + var stub = _createStub( {}, {} ) + .use( Sut( _true, 2, wait ) ) + (); + + stub.request( {}, function( _, __ ) + { + expect( waited ).to.equal( true ); + done(); + } ); + } ); + + + it( 'will not process if continuation is not called', function() + { + var waited = false; + var wait = function( _, c ) + { + waited = true; + /* do not invoke */ + }; + + var stub = _createStub( {}, {} ) + .use( Sut( _true, 2, wait ) ) + (); + + // this works because we know that our stub is invoked + // synchronously + stub.request( {}, function( _, __ ) + { + throw Error( "Should not have been called!" ); + } ); + + expect( waited ).to.equal( true ); + } ); + + + it( 'will call delay function until success', function() + { + var n = 5; + var wait = function( tries_left, c ) + { + n--; + + // the first argument is the number of tries left + expect( tries_left ).to.equal( n ); + c(); + }; + + var pred = function() + { + return n > 0; + }; + + var stub = _createStub( {}, {} ) + .use( Sut( pred, n, wait ) ) + (); + + // this works because we know that our stub is invoked + // synchronously + stub.request( {}, _void ); + + // the first request counts as one, which brings us down to 4, + // but the wait function has not been called at this point; so, + // we expect that it will only be called four times + expect( n ).to.equal( 1 ); + } ); + + + it( 'allows aborting via failure continuation', function( done ) + { + var err_expect = {}, + out_expect = []; + + var wait = function( _, __, abort ) + { + abort(); + }; + + // without aborting, this would never finish + var stub = _createStub( err_expect, out_expect ) + .use( Sut( _true, -1, wait ) ) + (); + + // this works because we know that our stub is invoked + // synchronously + stub.request( {}, function( err, output ) + { + expect( err ).to.equal( err_expect ); + expect( output ).to.equal( out_expect ); + + done(); + } ); + } ); + } ); } );