dns-endpoint-pool
Advanced tools
Comparing version 1.2.0 to 1.3.0
@@ -0,3 +1,7 @@ | ||
## 1.3.0 / 2017-05-30 | ||
- Adds a new circuit breaker algorithm which is less reliant on traffic rates. | ||
## 1.2.0 / 2017-05-29 | ||
- Add `getStatus()` method. |
15
index.js
@@ -1,9 +0,9 @@ | ||
var EndpointPool, | ||
_ = require('underscore'), | ||
dns = require('dns'), | ||
Events = require('events'), | ||
PoolManager = require('./pool-manager'), | ||
util = require('util'), | ||
var EndpointPool; | ||
var _ = require('underscore'); | ||
var dns = require('dns'); | ||
var Events = require('events'); | ||
var PoolManager = require('./pool-manager'); | ||
var util = require('util'); | ||
DNS_LOOKUP_TIMEOUT = 1000; | ||
var DNS_LOOKUP_TIMEOUT = 1000; | ||
@@ -88,3 +88,2 @@ /** | ||
var poolStatus = this.poolManager.getStatus(); | ||
var endpoints = this.poolManager.endpoints; | ||
return _.assign({ | ||
@@ -91,0 +90,0 @@ age: Date.now() - this.lastUpdate |
{ | ||
"name": "dns-endpoint-pool", | ||
"version": "1.2.0", | ||
"version": "1.3.0", | ||
"description": "Manage and load-balance a pool of service endpoints retrieved from a DNS lookup for a service discovery name.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
var _ = require('underscore'); | ||
// endpoint states | ||
var CLOSED = 0; // closed circuit: endpoint is good to use | ||
var HALF_OPEN_READY = 1; // endpoint is in recovery state: offer it for use once | ||
var HALF_OPEN_PENDING = 2; // endpoint recovery is in process | ||
var OPEN = 3; // open circuit: endpoint is no good | ||
@@ -20,3 +25,6 @@ function PoolManager (options) { | ||
getNextEndpoint: function () { | ||
var i, l, offset, endpoint; | ||
var i; | ||
var l; | ||
var offset; | ||
var endpoint; | ||
@@ -35,3 +43,4 @@ for (i = 0, l = this.endpoints.length; i < l; ++i) { | ||
updateEndpoints: function (endpoints) { | ||
var matchingEndpoint, i; | ||
var matchingEndpoint; | ||
var i; | ||
var newEndpoints = endpoints.map(function (info) { | ||
@@ -65,3 +74,3 @@ return new Endpoint(info); | ||
}, 0) | ||
} | ||
}; | ||
} | ||
@@ -81,17 +90,21 @@ }; | ||
ejectOnErrorPoolManager: function (options) { | ||
// endpoint states | ||
var CLOSED = 0, // closed circuit: endpoint is good to use | ||
HALF_OPEN_READY = 1, // endpoint is in recovery state: offer it for use once | ||
HALF_OPEN_PENDING = 2, // endpoint recovery is in process | ||
OPEN = 3; // open circuit: endpoint is no good | ||
if (!options) { | ||
throw new Error('Must supply arguments to ejectOnErrorPoolManager'); | ||
} | ||
if (!options || !(options.failureWindow && options.maxFailures && options.resetTimeout)) { | ||
throw new Error('Must supply all arguments to ejectOnErrorPoolManager'); | ||
var poolConfig; | ||
if (options.failureWindow && options.maxFailures && options.resetTimeout) { | ||
poolConfig = getRollingWindowConfiguration(options.failureWindow, options.maxFailures, options.resetTimeout); | ||
} else if (options.failureRate && options.failureRateWindow && options.resetTimeout) { | ||
poolConfig = getRateConfiguration(options.failureRate, options.failureRateWindow, options.resetTimeout); | ||
} else { | ||
throw new Error('Must supply either configuration to ejectOnErrorPoolManager'); | ||
} | ||
var failureWindow = options.failureWindow; | ||
var maxFailures = options.maxFailures; | ||
var resetTimeout = options.resetTimeout; | ||
return new PoolManager(poolConfig); | ||
function disableEndpoint(endpoint) { | ||
if (endpoint.state === OPEN) { | ||
return; | ||
} | ||
endpoint.state = OPEN; | ||
@@ -101,55 +114,89 @@ clearInterval(endpoint._reopenTimeout); | ||
endpoint.state = HALF_OPEN_READY; | ||
}, resetTimeout); | ||
}, options.resetTimeout); | ||
} | ||
function isInPool(endpoint) { | ||
return endpoint.state === CLOSED || endpoint.state === HALF_OPEN_READY; | ||
} | ||
function onEndpointSelected(endpoint) { | ||
if (endpoint.state === HALF_OPEN_READY) { | ||
endpoint.state = HALF_OPEN_PENDING; // let one through, then turn it off again | ||
} | ||
} | ||
return new PoolManager({ | ||
isInPool: function (endpoint) { | ||
switch (endpoint.state) { | ||
case HALF_OPEN_READY: | ||
case CLOSED: | ||
return true; | ||
default: | ||
return false; | ||
} | ||
}, | ||
onEndpointSelected: function (endpoint) { | ||
if (endpoint.state === HALF_OPEN_READY) { | ||
endpoint.state = HALF_OPEN_PENDING; // let one through, then turn it off again | ||
} | ||
}, | ||
onEndpointRegistered: function (endpoint) { | ||
endpoint.state = CLOSED; | ||
endpoint.failCount = 0; | ||
// A ring buffer, holding the timestamp of each error. As we loop around the ring, the timestamp in the slot we're | ||
// about to fill will tell us the error rate. That is, `maxFailure` number of requests in how many milliseconds? | ||
endpoint.buffer = new Array(maxFailures - 1); | ||
endpoint.bufferPointer = 0; | ||
}, | ||
onEndpointReturned: function (endpoint, err) { | ||
if (err) { | ||
var oldestErrorTime, now; | ||
if (endpoint.state === OPEN) { | ||
return; | ||
} | ||
function getRollingWindowConfiguration(failureWindow, maxFailures, resetTimeout) { | ||
return { | ||
isInPool: isInPool, | ||
onEndpointSelected: onEndpointSelected, | ||
onEndpointRegistered: function (endpoint) { | ||
endpoint.state = CLOSED; | ||
// A ring buffer, holding the timestamp of each error. As we loop around the ring, the timestamp in the slot we're | ||
// about to fill will tell us the error rate. That is, `maxFailure` number of requests in how many milliseconds? | ||
endpoint.buffer = new RingBuffer(maxFailures - 1); | ||
}, | ||
onEndpointReturned: function (endpoint, err) { | ||
if (err) { | ||
if (endpoint.state === OPEN) { | ||
return; | ||
} | ||
if (endpoint.buffer.length === 0) { | ||
disableEndpoint(endpoint); | ||
return; | ||
if (endpoint.buffer.size === 0) { | ||
disableEndpoint(endpoint); | ||
return; | ||
} | ||
var now = Date.now(); | ||
var oldestErrorTime = endpoint.buffer.read(); | ||
endpoint.buffer.write(now); | ||
if (endpoint.state === HALF_OPEN_PENDING || (oldestErrorTime != null && now - oldestErrorTime <= failureWindow)) { | ||
disableEndpoint(endpoint); | ||
} | ||
} else if (endpoint.state === HALF_OPEN_PENDING) { | ||
endpoint.state = CLOSED; | ||
} | ||
} | ||
}; | ||
} | ||
oldestErrorTime = endpoint.buffer[endpoint.bufferPointer]; | ||
now = Date.now(); | ||
endpoint.buffer[endpoint.bufferPointer] = now; | ||
endpoint.bufferPointer++; | ||
endpoint.bufferPointer %= endpoint.buffer.length; | ||
function getRateConfiguration(failureRate, failureRateWindow, resetTimeout) { | ||
var maxErrorCount = failureRate * failureRateWindow; | ||
return { | ||
isInPool: isInPool, | ||
onEndpointSelected: onEndpointSelected, | ||
onEndpointRegistered: function (endpoint) { | ||
endpoint.state = CLOSED; | ||
endpoint.buffer = new RingBuffer(failureRateWindow); | ||
endpoint.errors = 0; | ||
}, | ||
onEndpointReturned: function (endpoint, err) { | ||
var state = endpoint.state; | ||
var newStatus = err ? 1 : 0; | ||
var oldestStatus = endpoint.buffer.read() ? 1 : 0; | ||
endpoint.buffer.write(newStatus); | ||
endpoint.errors += newStatus - oldestStatus; | ||
if (endpoint.state === HALF_OPEN_PENDING || (oldestErrorTime != null && now - oldestErrorTime <= failureWindow)) { | ||
if (err && (state === HALF_OPEN_PENDING || endpoint.errors >= maxErrorCount)) { | ||
disableEndpoint(endpoint); | ||
} else if (!err && state === HALF_OPEN_PENDING) { | ||
endpoint.state = CLOSED; | ||
} | ||
} else if (endpoint.state === HALF_OPEN_PENDING) { | ||
endpoint.state = CLOSED; | ||
} | ||
} | ||
}) | ||
}; | ||
} | ||
} | ||
}; | ||
function RingBuffer(size) { | ||
this.buffer = new Array(size); | ||
this.offset = 0; | ||
this.size = size; | ||
} | ||
_.assign(RingBuffer.prototype, { | ||
read: function () { | ||
return this.buffer[this.offset]; | ||
}, | ||
write: function (val) { | ||
this.buffer[this.offset] = val; | ||
this.offset = (this.offset + 1) % this.size; | ||
} | ||
}); |
@@ -43,3 +43,3 @@ ## DNS Endpoint Pool | ||
### `new DNSEndpointPool(serviceDiscoveryName, ttl, maxFailures, failureWindow, resetTimeout, onReady)` | ||
### `new DNSEndpointPool(serviceDiscoveryName, ttl, circuitBreakerConfig, onReady)` | ||
@@ -51,6 +51,11 @@ Creates a new pool object. | ||
interval. | ||
- `maxFailures`: how many failures from a single endpoint before it is removed from the pool. | ||
- `failureWindow`: size of the sliding window of time in which the failures are counted. | ||
- `resetTimeout`: the length of the window in which to record failures. Also the timeout before an endpoint will be | ||
tried again. | ||
- `circuitBreakerConfig`: optional configuration for circuit breaker behavior. If specified, errors on a particular endpoint will be tracked and bad endpoints removed from the pool. If none provided, no circuit breaker logic is applied. There are two different circuit-breaker behaviors available: | ||
- Errors over time: the CB is configured with the an error threshold within a sliding time window. (eg: 5 errors in 10 seconds) | ||
- `maxFailures`: how many failures from a single endpoint before it is removed from the pool. | ||
- `failureWindow`: size of the sliding window of time in which the failures are counted. | ||
- `resetTimeout`: The timeout before a failing endpoint will be re-entered to the pool and tried again. | ||
- Error rate: the CB is configured with the percentage of errors within a sliding window of requests. (eg: 50% errors over 20 requests). | ||
- `failureRate`: a number, `0 < n <= 1` that describes the rate at which the endpoint is disabled. | ||
- `failureRateWindow`: the number of requests over which to calculate the failure rate. | ||
- `resetTimeout`: The timeout before a failing endpoint will be re-entered to the pool and tried again. | ||
- `onReady`: callback that will be executed after the list of endpoints is fetched for the first time. This does *not* guarantee that the endpoint list is not empty. | ||
@@ -61,3 +66,3 @@ | ||
Returns the next active `endpoint` from the pool, or `null` if none are available. If none are available, the pool will | ||
emit `'noEndpoints'`. | ||
emit `'noEndpoints'`. If using circuit breakers, you _must_ call `endpoint.callback(err)` with the result of a call to this endpoint. | ||
@@ -68,3 +73,3 @@ ### `pool.getStatus()` | ||
- `total`: The total number of endpoints in the pool, in any status. | ||
- `total`: The total number of endpoints in the pool, in any state. | ||
- `unhealthy`: The number of endpoints which are unavailable (eg: due to their circuit breaker being open) | ||
@@ -91,3 +96,7 @@ - `age`: The number of milliseconds since the last successful update of endpoints. | ||
```js | ||
var pool = new DNSEndpointPool('my.domain.example.com', 10000, 5, 10000, 10000); | ||
var pool = new DNSEndpointPool('my.domain.example.com', 10000, { | ||
maxFailures: 5, | ||
failureWindow: 10000, | ||
resetTimeout: 10000 | ||
}); | ||
@@ -94,0 +103,0 @@ pool.on('updateError', function (err) { |
286
test.js
/*globals it, describe, beforeEach, afterEach */ | ||
var expect = require('expect.js'), | ||
Sinon = require('sinon'), | ||
DEP = require('./'); | ||
var expect = require('expect.js'); | ||
var Sinon = require('sinon'); | ||
var DEP = require('./'); | ||
describe('DNS Endpoint Pool', function () { | ||
var stubs = [], clock; | ||
var stubs = []; | ||
var clock; | ||
function autoRestore(stub) { | ||
@@ -35,4 +36,5 @@ stubs.push(stub); | ||
it('will automatically begin updating when constructed', function () { | ||
var stub = autoRestore(Sinon.stub(DEP.prototype, 'update')), | ||
dep = new DEP('foo.localhost', 5000); | ||
/*eslint no-unused-vars: 0 */ | ||
var stub = autoRestore(Sinon.stub(DEP.prototype, 'update')); | ||
var dep = new DEP('foo.localhost', 5000); | ||
Sinon.assert.calledOnce(stub); | ||
@@ -42,8 +44,7 @@ }); | ||
it('will execute a callback after the first update', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
called = false, | ||
dep; | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
var called = false; | ||
resolve.callsArgWith(0, null, []); | ||
dep = new DEP('foo.localhost', 5000, null, function () { | ||
var dep = new DEP('foo.localhost', 5000, null, function () { | ||
called = true; | ||
@@ -58,7 +59,6 @@ }); | ||
it('will update on a timer', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
dep; | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
resolve.callsArgWith(0, null, []); | ||
dep = new DEP('foo.localhost', 5000); | ||
var dep = new DEP('foo.localhost', 5000); | ||
@@ -77,4 +77,3 @@ Sinon.assert.calledOnce(resolve); | ||
it('will add resolved endpoints and serve them in rotation', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
dep; | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
@@ -85,3 +84,3 @@ resolve.callsArgWith(0, null, [ | ||
]); | ||
dep = new DEP('foo.localhost', 5000); | ||
var dep = new DEP('foo.localhost', 5000); | ||
@@ -94,5 +93,3 @@ expect(dep.getEndpoint().url).to.be('bar.localhost:8000'); | ||
it('will trigger an error if resolving fails', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
errorData, | ||
dep; | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
@@ -106,6 +103,7 @@ resolve | ||
dep = new DEP('foo.localhost', 5000); | ||
var dep = new DEP('foo.localhost', 5000); | ||
expect(dep.getEndpoint().url).to.be('bar.localhost:8000'); | ||
var errorData; | ||
dep.on('updateError', function (err) { | ||
@@ -123,5 +121,3 @@ errorData = err; | ||
it('will update with new endpoints returned', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
bazEndpoint, | ||
dep; | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
@@ -137,6 +133,6 @@ resolve | ||
]); | ||
dep = new DEP('foo.localhost', 5000); | ||
var dep = new DEP('foo.localhost', 5000); | ||
dep.getEndpoint(); // bar | ||
bazEndpoint = dep.getEndpoint(); | ||
var bazEndpoint = dep.getEndpoint(); | ||
@@ -155,4 +151,3 @@ expect(bazEndpoint.url).to.be('baz.localhost:8001'); | ||
it('can query the state of endpoints', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
dep; | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
@@ -164,3 +159,3 @@ resolve | ||
dep = new DEP('foo.localhost', 5000); | ||
var dep = new DEP('foo.localhost', 5000); | ||
@@ -176,9 +171,2 @@ expect(dep.hasEndpoints()).to.be(false); | ||
describe('with eject-on-error pool management', function () { | ||
var ejectOnErrorConfig = { | ||
maxFailures: 2, | ||
failureWindow: 10000, | ||
resetTimeout: 10000 | ||
}; | ||
it('enforces that config object has proper shape', function () { | ||
@@ -188,85 +176,165 @@ autoRestore(Sinon.stub(DEP.prototype, 'update')); | ||
function () { return new DEP('foo.localhost', 5000, { maxFailures: 2 }); }, | ||
function () { return new DEP('foo.localhost', 5000, { maxFailures: 2, failureWindow: 10000 }); } | ||
function () { return new DEP('foo.localhost', 5000, { maxFailures: 2, failureWindow: 10000 }); }, | ||
function () { return new DEP('foo.localhost', 5000, { failureRate: 0.5 }); }, | ||
function () { return new DEP('foo.localhost', 5000, { failureRateWindow: 10 }); } | ||
].forEach(function (fn) { | ||
expect(fn).to.throwError('Must supply all arguments to ejectOnErrorPoolManager'); | ||
expect(fn).to.throwError('Must supply either configuration to ejectOnErrorPoolManager'); | ||
}); | ||
}); | ||
it('will remove endpoints from the pool if they fail', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
barEndpoint, | ||
bazEndpoint, | ||
status, | ||
dep; | ||
describe('using time-based rolling window', function () { | ||
var ejectOnErrorConfig = { | ||
maxFailures: 2, | ||
failureWindow: 10000, | ||
resetTimeout: 10000 | ||
}; | ||
resolve.callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 }, | ||
{ name: 'baz.localhost', port: 8001 } | ||
]); | ||
it('will remove endpoints from the pool if they fail', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
dep = new DEP('foo.localhost', 5000, ejectOnErrorConfig); | ||
resolve.callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 }, | ||
{ name: 'baz.localhost', port: 8001 } | ||
]); | ||
barEndpoint = dep.getEndpoint(); | ||
barEndpoint.callback(true); | ||
var dep = new DEP('foo.localhost', 5000, ejectOnErrorConfig); | ||
bazEndpoint = dep.getEndpoint(); | ||
var barEndpoint = dep.getEndpoint(); | ||
barEndpoint.callback(true); | ||
expect(dep.getEndpoint()).to.be(barEndpoint); // still in the pool | ||
barEndpoint.callback(true); | ||
var bazEndpoint = dep.getEndpoint(); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); // bar is removed | ||
expect(dep.getEndpoint()).to.be(barEndpoint); // still in the pool | ||
barEndpoint.callback(true); | ||
status = dep.getStatus(); | ||
expect(status.total).to.be(2); | ||
expect(status.unhealthy).to.be(1); | ||
expect(status.age).to.be.a('number'); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); // bar is removed | ||
dep.stopUpdating(); | ||
}); | ||
var status = dep.getStatus(); | ||
expect(status.total).to.be(2); | ||
expect(status.unhealthy).to.be(1); | ||
expect(status.age).to.be.a('number'); | ||
it('will reinstate endpoints for a single request after a timeout', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
barEndpoint, | ||
bazEndpoint, | ||
dep; | ||
dep.stopUpdating(); | ||
}); | ||
resolve.callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 }, | ||
{ name: 'baz.localhost', port: 8001 } | ||
]); | ||
it('will reinstate endpoints for a single request after a timeout', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
dep = new DEP('foo.localhost', 5000, ejectOnErrorConfig); | ||
resolve.callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 }, | ||
{ name: 'baz.localhost', port: 8001 } | ||
]); | ||
barEndpoint = dep.getEndpoint(); | ||
barEndpoint.callback(true); | ||
barEndpoint.callback(true); // removed from pool. | ||
var dep = new DEP('foo.localhost', 5000, ejectOnErrorConfig); | ||
bazEndpoint = dep.getEndpoint(); | ||
var barEndpoint = dep.getEndpoint(); | ||
barEndpoint.callback(true); | ||
barEndpoint.callback(true); // removed from pool. | ||
clock.tick(10000); | ||
var bazEndpoint = dep.getEndpoint(); | ||
expect(dep.getEndpoint()).to.be(barEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); // only return barEndpoint once | ||
clock.tick(10000); | ||
barEndpoint.callback(null); // denotes success | ||
expect(dep.getEndpoint()).to.be(barEndpoint); // it's back in the game | ||
dep.stopUpdating(); | ||
expect(dep.getEndpoint()).to.be(barEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); // only return barEndpoint once | ||
barEndpoint.callback(null); // denotes success | ||
expect(dep.getEndpoint()).to.be(barEndpoint); // it's back in the game | ||
dep.stopUpdating(); | ||
}); | ||
it('reports the age of the endpoints when updates fail', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
var errorHandler = Sinon.spy(); | ||
var errObj = { error: true }; | ||
// works on the first and fourth calls, fails every other time | ||
resolve | ||
.callsArgWith(0, errObj) | ||
.onFirstCall().callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 }, | ||
{ name: 'baz.localhost', port: 8001 } | ||
]) | ||
.onCall(3).callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 }, | ||
{ name: 'baz.localhost', port: 8001 } | ||
]); | ||
var dep = new DEP('foo.localhost', 5000, ejectOnErrorConfig); // call 1 | ||
dep.on('updateError', errorHandler); | ||
clock.tick(5000); // call 2 | ||
clock.tick(5000); // call 3 | ||
clock.tick(5000); // call 4, should reset the timer | ||
clock.tick(5000); // call 5 | ||
Sinon.assert.calledThrice(errorHandler); | ||
expect(errorHandler.firstCall.calledWithExactly(errObj, 5000)).to.be(true); | ||
expect(errorHandler.secondCall.calledWithExactly(errObj, 10000)).to.be(true); | ||
expect(errorHandler.thirdCall.calledWithExactly(errObj, 5000)).to.be(true); | ||
dep.stopUpdating(); | ||
}); | ||
}); | ||
it('reports the age of the endpoints when updates fail', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')), | ||
errorHandler = Sinon.spy(), | ||
errObj = { error: true }, | ||
dep; | ||
describe('using percentage error rates', function () { | ||
var ejectOnErrorConfig = { | ||
failureRate: 1, | ||
failureRateWindow: 2, | ||
resetTimeout: 10000 | ||
}; | ||
// works on the first and fourth calls, fails every other time | ||
resolve | ||
.callsArgWith(0, errObj) | ||
.onFirstCall().callsArgWith(0, null, [ | ||
it('will remove endpoints from the pool if they fail', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
resolve.callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 }, | ||
{ name: 'baz.localhost', port: 8001 } | ||
]) | ||
.onCall(3).callsArgWith(0, null, [ | ||
]); | ||
var dep = new DEP('foo.localhost', 5000, ejectOnErrorConfig); | ||
var barEndpoint = dep.getEndpoint(); | ||
barEndpoint.callback(true); | ||
var bazEndpoint = dep.getEndpoint(); | ||
expect(dep.getEndpoint()).to.be(barEndpoint); // still in the pool | ||
barEndpoint.callback(true); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); // bar is removed | ||
var status = dep.getStatus(); | ||
expect(status.total).to.be(2); | ||
expect(status.unhealthy).to.be(1); | ||
expect(status.age).to.be.a('number'); | ||
dep.stopUpdating(); | ||
}); | ||
it('will remove an endpoint from the pool once it reaches its error threshold', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
resolve.callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 } | ||
]); | ||
var dep = new DEP('foo.localhost', 5000, { | ||
failureRate: 0.75, | ||
failureRateWindow: 4, | ||
resetTimeout: 10000 | ||
}); | ||
var endpoint = dep.getEndpoint(); | ||
endpoint.callback(true); | ||
endpoint = dep.getEndpoint(); | ||
endpoint.callback(true); | ||
endpoint = dep.getEndpoint(); | ||
endpoint.callback(true); | ||
expect(dep.getEndpoint()).to.be(null); | ||
}); | ||
it('will reinstate endpoints for a single request after a timeout', function () { | ||
var resolve = autoRestore(Sinon.stub(DEP.prototype, 'resolve')); | ||
resolve.callsArgWith(0, null, [ | ||
{ name: 'bar.localhost', port: 8000 }, | ||
@@ -276,18 +344,26 @@ { name: 'baz.localhost', port: 8001 } | ||
dep = new DEP('foo.localhost', 5000, ejectOnErrorConfig); // call 1 | ||
dep.on('updateError', errorHandler); | ||
clock.tick(5000); // call 2 | ||
clock.tick(5000); // call 3 | ||
clock.tick(5000); // call 4, should reset the timer | ||
clock.tick(5000); // call 5 | ||
var dep = new DEP('foo.localhost', 5000, { | ||
failureRate: 1, | ||
failureRateWindow: 2, | ||
resetTimeout: 10000 | ||
}); | ||
Sinon.assert.calledThrice(errorHandler); | ||
var barEndpoint = dep.getEndpoint(); | ||
barEndpoint.callback(true); | ||
barEndpoint.callback(true); // removed from pool. | ||
expect(errorHandler.firstCall.calledWithExactly(errObj, 5000)).to.be(true); | ||
expect(errorHandler.secondCall.calledWithExactly(errObj, 10000)).to.be(true); | ||
expect(errorHandler.thirdCall.calledWithExactly(errObj, 5000)).to.be(true); | ||
dep.stopUpdating(); | ||
var bazEndpoint = dep.getEndpoint(); | ||
clock.tick(10000); | ||
expect(dep.getEndpoint()).to.be(barEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); | ||
expect(dep.getEndpoint()).to.be(bazEndpoint); // only return barEndpoint once | ||
barEndpoint.callback(null); // denotes success | ||
expect(dep.getEndpoint()).to.be(barEndpoint); // it's back in the game | ||
dep.stopUpdating(); | ||
}); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
29432
531
113
1