mirror of
https://github.com/donl/slouch.git
synced 2026-05-26 06:12:11 -06:00
* 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
178 lines
4.9 KiB
JavaScript
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;
|