slouch/scripts/request-class.js
Geoff Cox da7ca1123e 100% coverage (#4)
* doc(readme): clean up reasons

* doc(motto)

* test(db-and-doc): more coverage

* test(create-or-update-ignore-conflict)

* test(upsert)

* test(ignore-missing)

* test(post-and-ignore-conflict)

* test(get-merge-put)

* refactor(all): rename post and put

* test(get-merge-create-or-update)

* test(get-merge-update-ignore-conflict)

* test(get-merge-upsert)

* test(get-modify-upsert)

* refactor(doc): redundant code

* test(destroy-ignore-conflict)

* test(get-and-destroy)

* test(mark-as-destroyed)

* test(set-destroyed)

* refactor(attachment)

* test(doc): 100% coverage

* test(attachment): create with base 64

* test(attachment): clean up binary code

* test(attachment): get

* test(attachment): destroy

* test(system): is couchdb 1

* test(system): get

* test(system): reset

* test(updates)

* test(updates)

* test(all): unique DB names

* test(system): reactivate tests

* test(user): add role

* test(user): downsert role

* feat(stream-iterator): indefinite

* test(user): 100% coverage

* test(request-class)

* test(request-class): 100% coverage

* test(config)

* test(config): more coverage

* test(config): more coverage

* test(config): 100% coverage

* test(all): 100% coverage

* refactor(beautify)

* test(coverage): enforce 100%

* test(system): fix race condition

* test(user): shortcut for browser

* test(updates): test continuous stream in phantomjs

* test(updates): test continuous stream in phantomjs

* test(continuous): mock for phantomjs

* test(system): abort iterators

* test(system): fake abort
2017-07-18 07:45:32 -07:00

178 lines
4.9 KiB
JavaScript

'use strict';
var Promise = require('sporks/scripts/promise'),
req = Promise.promisify(require('request')),
Throttler = require('squadron').Throttler,
Backoff = require('backoff-promise');
// Until https://github.com/Gozala/querystring/issues/20 is fixed, we need to manually define an
// unescape function
var QueryString = require('request/lib/querystring').Querystring;
QueryString.prototype.unescape = function (s) {
return decodeURIComponent(s);
};
var RequestClass = function () {
this._throttler = new Throttler(RequestClass.DEFAULT_CONNECTIONS);
this._req = req;
};
// For debugging all traffic
RequestClass.LOG_EVERYTHING = false;
RequestClass.prototype._log = function () {
if (RequestClass.LOG_EVERYTHING) {
console.log.apply(console.log, arguments);
}
};
// The default value for max_dbs_open is 500 and we want to leave some space for other processes to
// also hit the DB.
RequestClass.DEFAULT_CONNECTIONS = 20;
RequestClass.MAX_RETRIES = 10;
// Preserve some compatibility with nano
RequestClass.prototype._getStatusCode = function (body) {
switch (body.error) {
case 'conflict':
return 409;
case 'not_found':
return 404;
}
if (body.reason && body.reason.indexOf('Could not open source database') !== -1) {
return 404;
}
};
RequestClass.prototype._request = function (opts, parseBody) {
var self = this,
selfArguments = arguments;
return self._req.apply(this, arguments).then(function (response) {
var err = null;
// Sometimes CouchDB just returns an malformed error
if (!response || !response.body) {
err = new Error('malformed body');
err.error = 'malformed body';
self._log('Slouch Request:', {
error: 'malformed body',
request: opts
});
throw err;
}
var body = null;
if (opts.encoding === null || typeof response.body !== 'string') {
body = response.body;
} else {
body = JSON.parse(response.body);
}
self._log('Slouch Request:', {
request: opts,
response: body
});
if (body.error) {
err = new Error('reason=' + body.reason + ', error=' + body.error + ', arguments' +
JSON.stringify(selfArguments));
err.statusCode = self._getStatusCode(body);
err.error = body.error;
throw err;
} else {
return parseBody ? body : response;
}
});
};
RequestClass.prototype._throttledRequestClass = function () {
var self = this,
selfArguments = arguments;
return self._throttler.run(function () {
return self._request.apply(self, selfArguments);
});
};
RequestClass.prototype._shouldReconnect = function (err) {
switch (err.message) {
case 'all_dbs_active': // No more connections
return true;
default:
// - ECONNREFUSED => Connection refused, e.g. because the CouchDB server is being restarted.
// - Occurs randomly when many simultaenous connections:
// - emfile
// - socket hang up
// - ECONNRESET
// - ETIMEDOUT
// - function_clause (CouchDB 2)
// - unknown_error (CouchDB 2)
// - internal_server_error (CouchDB 2)
return new RegExp([
'ECONNREFUSED',
'ENETUNREACH', // can occur when box sleeps/wakes-up
'emfile',
'socket hang up',
'ECONNRESET',
'ETIMEDOUT',
'function_clause',
'unknown_error',
'internal_server_error',
'Failed to fetch', // ECONNREFUSED/ENOTFOUND in Chrome
'Type error', // ECONNREFUSED/ENOTFOUND in Safari
'XHR error' // ECONNREFUSED/ENOTFOUND in Firefox
].join('|'), 'i').test(err.message);
}
};
RequestClass.prototype._shouldIgnore = function (err) {
// For some strange reason, CouchDB will give us "default_authentication_handler" errors even when
// the request was successful and we need to ignore these errors.
return /default_authentication_handler/.test(err.message);
};
// Provide a construct for mocking
RequestClass.prototype._newBackoff = function () {
return new Backoff();
};
RequestClass.prototype.request = function () {
var self = this,
selfArguments = arguments,
backoff = self._newBackoff(),
retries = 0;
var backoffThrottledRequestClass = function () {
return backoff.attempt(function () {
return self._throttledRequestClass.apply(self, selfArguments);
}).catch(function (err) {
// Reached max retries?
if (retries++ >= RequestClass.MAX_RETRIES) {
throw err;
} else if (self._shouldReconnect(err)) {
// Attempt again
return backoffThrottledRequestClass();
} else if (!self._shouldIgnore(err)) {
// Error doesn't warrant retry to throw to caller
throw err;
}
});
};
return backoffThrottledRequestClass();
};
RequestClass.prototype.setMaxConnections = function (maxConnections) {
// TODO: reducing the number doesn't work when it is done after the new max has already been
// reached. Does it?
this._throttler.setMaxConcurrentProcesses(maxConnections);
};
module.exports = RequestClass;