From ca5d064455cbad912b994ff0b40fdbb9f1292439 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 19 May 2015 13:13:49 -0400 Subject: [PATCH] AutoRetry trait initial implementation --- src/dapi/AutoRetry.js | 187 +++++++++++++++++++++++++++++++++++ src/dapi/http/XhrHttpImpl.js | 2 +- test/dapi/AutoRetryTest.js | 179 +++++++++++++++++++++++++++++++++ 3 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 src/dapi/AutoRetry.js create mode 100644 test/dapi/AutoRetryTest.js diff --git a/src/dapi/AutoRetry.js b/src/dapi/AutoRetry.js new file mode 100644 index 0000000..98155a5 --- /dev/null +++ b/src/dapi/AutoRetry.js @@ -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 . + */ + +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 ); + }, +} ); + diff --git a/src/dapi/http/XhrHttpImpl.js b/src/dapi/http/XhrHttpImpl.js index a2482ee..f0d0e08 100644 --- a/src/dapi/http/XhrHttpImpl.js +++ b/src/dapi/http/XhrHttpImpl.js @@ -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 * diff --git a/test/dapi/AutoRetryTest.js b/test/dapi/AutoRetryTest.js new file mode 100644 index 0000000..a5e6ab2 --- /dev/null +++ b/test/dapi/AutoRetryTest.js @@ -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 . + */ + +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 ); + } + } ); +}