UNPKG

backoff-web

Version:

Fibonacci and exponential backoffs.

403 lines (320 loc) 12.4 kB
var events = require('events'); var sinon = require('sinon'); var util = require('util'); var FunctionCall = require('../src/function_call'); function MockBackoff () { events.EventEmitter.call(this); this.reset = sinon.spy(); this.backoff = sinon.spy(); this.failAfter = sinon.spy(); } util.inherits(MockBackoff, events.EventEmitter); exports['FunctionCall'] = { setUp: function (callback) { this.wrappedFn = sinon.stub(); this.callback = sinon.stub(); this.backoff = new MockBackoff(); this.backoffFactory = sinon.stub(); this.backoffFactory.returns(this.backoff); callback(); }, tearDown: function (callback) { callback(); }, "constructor's first argument should be a function": function (test) { test.throws(function () { const strategy = new FunctionCall(1, [], function () {}); return strategy; }, /first argument to be a function/); test.done(); }, "constructor's last argument should be a function": function (test) { test.throws(function () { const strategy = new FunctionCall(function () {}, [], 3); return strategy; }, /Expected third \(last\) arugment to be a function/); test.done(); }, 'isPending should return false once the call is started': function (test) { this.wrappedFn .onFirstCall().yields(new Error()) .onSecondCall().yields(null, 'Success!'); var call = new FunctionCall(this.wrappedFn, [], this.callback); test.ok(call.isPending()); call.start(this.backoffFactory); test.ok(!call.isPending()); this.backoff.emit('ready'); test.ok(!call.isPending()); test.done(); }, 'isRunning should return true when call is in progress': function (test) { this.wrappedFn .onFirstCall().yields(new Error()) .onSecondCall().yields(null, 'Success!'); var call = new FunctionCall(this.wrappedFn, [], this.callback); test.ok(!call.isRunning()); call.start(this.backoffFactory); test.ok(call.isRunning()); this.backoff.emit('ready'); test.ok(!call.isRunning()); test.done(); }, 'isCompleted should return true once the call completes': function (test) { this.wrappedFn .onFirstCall().yields(new Error()) .onSecondCall().yields(null, 'Success!'); var call = new FunctionCall(this.wrappedFn, [], this.callback); test.ok(!call.isCompleted()); call.start(this.backoffFactory); test.ok(!call.isCompleted()); this.backoff.emit('ready'); test.ok(call.isCompleted()); test.done(); }, 'isAborted should return true once the call is aborted': function (test) { this.wrappedFn .onFirstCall().yields(new Error()) .onSecondCall().yields(null, 'Success!'); var call = new FunctionCall(this.wrappedFn, [], this.callback); test.ok(!call.isAborted()); call.abort(); test.ok(call.isAborted()); test.done(); }, 'setStrategy should overwrite the default strategy': function (test) { var replacementStrategy = {}; var call = new FunctionCall(this.wrappedFn, [], this.callback); call.setStrategy(replacementStrategy); call.start(this.backoffFactory); test.ok(this.backoffFactory.calledWith(replacementStrategy), 'User defined strategy should be used to instantiate ' + 'the backoff instance.'); test.done(); }, 'setStrategy should throw if the call is in progress': function (test) { var call = new FunctionCall(this.wrappedFn, [], this.callback); call.start(this.backoffFactory); test.throws(function () { call.setStrategy({}); }, /in progress/); test.done(); }, 'failAfter should not be set by default': function (test) { var call = new FunctionCall(this.wrappedFn, [], this.callback); call.start(this.backoffFactory); test.equal(0, this.backoff.failAfter.callCount); test.done(); }, 'failAfter should be used as the maximum number of backoffs': function (test) { var failAfterValue = 99; var call = new FunctionCall(this.wrappedFn, [], this.callback); call.failAfter(failAfterValue); call.start(this.backoffFactory); test.ok(this.backoff.failAfter.calledWith(failAfterValue), 'User defined maximum number of backoffs shoud be ' + 'used to configure the backoff instance.'); test.done(); }, 'failAfter should throw if the call is in progress': function (test) { var call = new FunctionCall(this.wrappedFn, [], this.callback); call.start(this.backoffFactory); test.throws(function () { call.failAfter(1234); }, /in progress/); test.done(); }, "start shouldn't allow overlapping invocation": function (test) { var call = new FunctionCall(this.wrappedFn, [], this.callback); var backoffFactory = this.backoffFactory; call.start(backoffFactory); test.throws(function () { call.start(backoffFactory); }, /already started/); test.done(); }, "start shouldn't allow invocation of aborted call": function (test) { var call = new FunctionCall(this.wrappedFn, [], this.callback); var backoffFactory = this.backoffFactory; call.abort(); test.throws(function () { call.start(backoffFactory); }, /aborted/); test.done(); }, 'call should forward its arguments to the wrapped function': function (test) { var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); call.start(this.backoffFactory); test.ok(this.wrappedFn.calledWith(1, 2, 3)); test.done(); }, 'call should complete when the wrapped function succeeds': function (test) { var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); this.wrappedFn .onCall(0).yields(new Error()) .onCall(1).yields(new Error()) .onCall(2).yields(new Error()) .onCall(3).yields(null, 'Success!'); call.start(this.backoffFactory); for (var i = 0; i < 2; i++) { this.backoff.emit('ready'); } test.equals(this.callback.callCount, 0); this.backoff.emit('ready'); test.ok(this.callback.calledWith(null, 'Success!')); test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3)); test.done(); }, 'call should fail when the backoff limit is reached': function (test) { var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); var error = new Error(); this.wrappedFn.yields(error); call.start(this.backoffFactory); for (var i = 0; i < 3; i++) { this.backoff.emit('ready'); } test.equals(this.callback.callCount, 0); this.backoff.emit('fail'); test.ok(this.callback.calledWith(error)); test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3)); test.done(); }, 'call should fail when the retry predicate returns false': function (test) { var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); call.retryIf(function (err) { return err.retriable; }); var retriableError = new Error(); retriableError.retriable = true; var fatalError = new Error(); fatalError.retriable = false; this.wrappedFn .onCall(0).yields(retriableError) .onCall(1).yields(retriableError) .onCall(2).yields(fatalError); call.start(this.backoffFactory); for (var i = 0; i < 2; i++) { this.backoff.emit('ready'); } test.equals(this.callback.callCount, 1); test.ok(this.callback.calledWith(fatalError)); test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3)); test.done(); }, "wrapped function's callback shouldn't be called after abort": function (test) { var call = new FunctionCall(function (callback) { call.abort(); // Abort in middle of wrapped function's execution. callback(null, 'ok'); }, [], this.callback); call.start(this.backoffFactory); test.equals(this.callback.callCount, 1, 'Wrapped function\'s callback shouldn\'t be called after abort.'); test.ok(this.callback.calledWithMatch(sinon.match(function (err) { return !!err.message.match(/Backoff aborted/); }, 'abort error'))); test.done(); }, 'abort event is emitted once when abort is called': function (test) { var call = new FunctionCall(this.wrappedFn, [], this.callback); this.wrappedFn.yields(new Error()); var callEventSpy = sinon.spy(); call.on('abort', callEventSpy); call.start(this.backoffFactory); call.abort(); call.abort(); call.abort(); test.equals(callEventSpy.callCount, 1); test.done(); }, 'getLastResult should return the last intermediary result': function (test) { var call = new FunctionCall(this.wrappedFn, [], this.callback); this.wrappedFn.yields(1); call.start(this.backoffFactory); for (var i = 2; i < 5; i++) { this.wrappedFn.yields(i); this.backoff.emit('ready'); test.deepEqual([i], call.getLastResult()); } this.wrappedFn.yields(null); this.backoff.emit('ready'); test.deepEqual([null], call.getLastResult()); test.done(); }, 'getNumRetries should return the number of retries': function (test) { var call = new FunctionCall(this.wrappedFn, [], this.callback); this.wrappedFn.yields(1); call.start(this.backoffFactory); // The inital call doesn't count as a retry. test.equals(0, call.getNumRetries()); for (var i = 2; i < 5; i++) { this.wrappedFn.yields(i); this.backoff.emit('ready'); test.equals(i - 1, call.getNumRetries()); } this.wrappedFn.yields(null); this.backoff.emit('ready'); test.equals(4, call.getNumRetries()); test.done(); }, "wrapped function's errors should be propagated": function (test) { var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); this.wrappedFn.throws(new Error()); test.throws(function () { call.start(this.backoffFactory); }, Error); test.done(); }, "wrapped callback's errors should be propagated": function (test) { var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); this.wrappedFn.yields(null, 'Success!'); this.callback.throws(new Error()); test.throws(function () { call.start(this.backoffFactory); }, Error); test.done(); }, 'call event should be emitted when wrapped function gets called': function (test) { this.wrappedFn.yields(1); var callEventSpy = sinon.spy(); var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback); call.on('call', callEventSpy); call.start(this.backoffFactory); for (var i = 1; i < 5; i++) { this.backoff.emit('ready'); } test.equal(5, callEventSpy.callCount, 'The call event should have been emitted 5 times.'); test.deepEqual([1, 'two'], callEventSpy.getCall(0).args, 'The call event should carry function\'s args.'); test.done(); }, 'callback event should be emitted when callback is called': function (test) { var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback); var callbackSpy = sinon.spy(); call.on('callback', callbackSpy); this.wrappedFn.yields('error'); call.start(this.backoffFactory); this.wrappedFn.yields(null, 'done'); this.backoff.emit('ready'); test.equal(2, callbackSpy.callCount, 'Callback event should have been emitted 2 times.'); test.deepEqual(['error'], callbackSpy.firstCall.args, 'First callback event should carry first call\'s results.'); test.deepEqual([null, 'done'], callbackSpy.secondCall.args, 'Second callback event should carry second call\'s results.'); test.done(); }, 'backoff event should be emitted on backoff start': function (test) { var err = new Error('backoff event error'); var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback); var backoffSpy = sinon.spy(); call.on('backoff', backoffSpy); this.wrappedFn.yields(err); call.start(this.backoffFactory); this.backoff.emit('backoff', 3, 1234, err); test.ok(this.backoff.backoff.calledWith(err), 'The backoff instance should have been called with the error.'); test.equal(1, backoffSpy.callCount, 'Backoff event should have been emitted 1 time.'); test.deepEqual([3, 1234, err], backoffSpy.firstCall.args, 'Backoff event should carry the backoff number, delay and error.'); test.done(); } };