AutoRetry trait initial implementation
parent
8fbd4dd220
commit
ca5d064455
|
@ -0,0 +1,187 @@
|
|||
/**
|
||||
* DataApi auto-retry requests on specified failure
|
||||
*
|
||||
* Copyright (C) 2015 LoVullo Associates, Inc.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework
|
||||
*
|
||||
* Liza is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var Trait = require( 'easejs' ).Trait,
|
||||
DataApi = require( './DataApi' );
|
||||
|
||||
|
||||
module.exports = Trait( 'AutoRetry' )
|
||||
.implement( DataApi )
|
||||
.extend(
|
||||
{
|
||||
/**
|
||||
* Predicate function determining whether a retry is needed
|
||||
* @var {function(?Error,*): boolean}
|
||||
*/
|
||||
'private _pred': '',
|
||||
|
||||
/**
|
||||
* Maximum number of tries (including initial request)
|
||||
* @var {number}
|
||||
*/
|
||||
'private _tries': 0,
|
||||
|
||||
/**
|
||||
* Delay in milliseconds before making the nth request as a function
|
||||
* of n
|
||||
*
|
||||
* @var {function(number): number}
|
||||
*/
|
||||
'private _delay': null,
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
__mixin: function( pred, tries, delay )
|
||||
{
|
||||
if ( typeof pred !== 'function' )
|
||||
{
|
||||
throw TypeError( 'Predicate must be a function' );
|
||||
}
|
||||
if ( typeof delay !== 'function' )
|
||||
{
|
||||
throw TypeError( "Delay must be a function" );
|
||||
}
|
||||
|
||||
this._pred = pred;
|
||||
this._tries = +tries;
|
||||
this._delay = delay;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Perform an asynchronous request and invoke CALLBACK with the
|
||||
* reply
|
||||
*
|
||||
* In the special case that the number of tries is set to zero, CALLBACK
|
||||
* will be immediately invoked with a null error and result (but not
|
||||
* necessarily asynchronously---that remains undefined).
|
||||
*
|
||||
* Otherwise, requests will continue to be re-issued until either the
|
||||
* request succeeds or the number of retries are exhausted, whichever
|
||||
* comes first. Once the retries are exhausted, the error and output
|
||||
* 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.
|
||||
*
|
||||
* @param {string} input binary data to transmit
|
||||
* @param {function(?Error,*)} callback continuation upon reply
|
||||
*
|
||||
* @return {DataApi} self
|
||||
*/
|
||||
'abstract override public request': function( input, callback )
|
||||
{
|
||||
this._try( input, callback, this._tries );
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Recursively perform request until success or try exhaustion
|
||||
*
|
||||
* For more information, see `#request'.
|
||||
*
|
||||
* @param {string} input binary data to transmit
|
||||
* @param {function(?Error,*)} callback continuation upon reply
|
||||
* @param {number} n number of retries remaining
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
'private _try': function( input, callback, n )
|
||||
{
|
||||
var _self = this;
|
||||
|
||||
// the special case of 0 retries still invokes the callback, but has
|
||||
// no data to return
|
||||
if ( n === 0 )
|
||||
{
|
||||
callback( null, null );
|
||||
return;
|
||||
}
|
||||
|
||||
this.request.super.call( this, input, function( err, output )
|
||||
{
|
||||
// predicate determines whether a retry is necessary
|
||||
if ( !!_self._pred( err, output ) === false )
|
||||
{
|
||||
return _self._succeed( 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 );
|
||||
}
|
||||
|
||||
_self._try( input, callback, ( n - 1 ) );
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Produce a successful response
|
||||
*
|
||||
* @param {*} output output data
|
||||
* @param {function(?Error,*)} callback continuation to invoke
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
'private _succeed': function( output, callback )
|
||||
{
|
||||
callback( null, output );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Produce a negative response
|
||||
*
|
||||
* @param {Error} err most recent error
|
||||
* @param {*} output most recent output data
|
||||
* @param {function(?Error,*)} callback continuation to invoke
|
||||
*
|
||||
* @return {undefined}
|
||||
*/
|
||||
'private _fail': function( err, output, callback )
|
||||
{
|
||||
callback( err, output );
|
||||
},
|
||||
} );
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* XMLHttpRequest HTTP protocol implementation
|
||||
*
|
||||
* Copyright (C) 2014 LoVullo Associates, Inc.
|
||||
* Copyright (C) 2015 LoVullo Associates, Inc.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework
|
||||
*
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* Test case for AutoRetry
|
||||
*
|
||||
* Copyright (C) 2015 LoVullo Associates, Inc.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework
|
||||
*
|
||||
* Liza is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var dapi = require( '../../' ).dapi,
|
||||
expect = require( 'chai' ).expect,
|
||||
Class = require( 'easejs' ).Class,
|
||||
DataApi = dapi.DataApi,
|
||||
Sut = dapi.AutoRetry;
|
||||
|
||||
var _void = function() {},
|
||||
_true = function() { return true; };
|
||||
|
||||
|
||||
describe( 'dapi.AutoRetry trait', function()
|
||||
{
|
||||
/**
|
||||
* If there are no failures, then AutoRetry has no observable effects.
|
||||
*/
|
||||
describe( 'when the request is successful', function()
|
||||
{
|
||||
it( 'makes only one request', function( done )
|
||||
{
|
||||
var given = {};
|
||||
|
||||
// success (but note the number of retries presented)
|
||||
var stub = _createStub( null, '' )
|
||||
.use( Sut( _void, 5, _void ) )
|
||||
();
|
||||
|
||||
stub.request( given, function()
|
||||
{
|
||||
expect( stub.given ).to.equal( given );
|
||||
expect( stub.requests ).to.equal( 1 );
|
||||
done();
|
||||
} );
|
||||
|
||||
} );
|
||||
|
||||
|
||||
it( 'returns the response data with no error', function( done )
|
||||
{
|
||||
var chk = { foo: 'bar' };
|
||||
|
||||
// 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 ) )
|
||||
();
|
||||
|
||||
stub.request( '', function( err, data )
|
||||
{
|
||||
expect( err ).to.equal( null );
|
||||
expect( data ).to.equal( chk );
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
/**
|
||||
* This is when we care.
|
||||
*/
|
||||
describe( 'when the request fails', function()
|
||||
{
|
||||
it( 'will re-perform request N-1 times until failure', function( done )
|
||||
{
|
||||
var n = 5;
|
||||
|
||||
var stub = _createStub( {}, {} )
|
||||
.use( Sut( _true, n, _void ) )
|
||||
();
|
||||
|
||||
stub.request( {}, function( err, _ )
|
||||
{
|
||||
expect( stub.requests ).to.equal( n );
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
it( 'will return most recent error and output data', function( done )
|
||||
{
|
||||
var e = Error( 'foo' ),
|
||||
output = {};
|
||||
|
||||
// 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 ) )
|
||||
();
|
||||
|
||||
stub.request( {}, function( err, data )
|
||||
{
|
||||
expect( err ).to.equal( e );
|
||||
expect( data ).to.equal( output );
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
describe( 'given a negative number of tries', function()
|
||||
{
|
||||
it( 'will continue until a successful request', function( done )
|
||||
{
|
||||
var n = 10,
|
||||
pred = function( _, __ )
|
||||
{
|
||||
return --n > 0;
|
||||
};
|
||||
|
||||
var stub = _createStub()
|
||||
.use( Sut( pred, -1, _void ) )
|
||||
();
|
||||
|
||||
stub.request( {}, function( _, __ )
|
||||
{
|
||||
expect( n ).to.equal( 0 );
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
describe( 'when the number of tries is zero', function()
|
||||
{
|
||||
it( 'will perform zero requests with null results', function( done )
|
||||
{
|
||||
var stub = _createStub( {}, {} )
|
||||
.use( Sut( _void, 0, _void ) )
|
||||
();
|
||||
|
||||
stub.request( {}, function( err, data )
|
||||
{
|
||||
expect( stub.requests ).to.equal( 0 );
|
||||
expect( err ).to.equal( null );
|
||||
expect( data ).to.equal( null );
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
||||
|
||||
function _createStub( err, resp )
|
||||
{
|
||||
return Class.implement( DataApi ).extend(
|
||||
{
|
||||
given: null,
|
||||
requests: 0,
|
||||
|
||||
'virtual public request': function( data, callback )
|
||||
{
|
||||
this.given = data;
|
||||
this.requests++;
|
||||
|
||||
callback( err, resp );
|
||||
}
|
||||
} );
|
||||
}
|
Loading…
Reference in New Issue