amino-gateway
Version:
Clusterable load-balancer for Amino services
130 lines (115 loc) • 4 kB
JavaScript
var cookie = require('cookie')
, parseUrl = require('url').parse
, http = require('http')
, httpProxy = require('http-proxy')
, addr = require('addr')
, cluster = require('cluster')
exports.attach = function (options) {
options || (options = {});
var amino = this;
amino.createGateway = function (opts) {
var d = require('domain').create();
d.on('error', function (err) {
console.error('uncaught error!', err.stack || err);
try {
// make sure we close down within 30 seconds
var killtimer = setTimeout(function() {
process.exit(1);
}, 30000);
// But don't keep the process open just for that!
killtimer.unref();
// stop taking new requests.
server.close();
// Let the master know we're dead. This will trigger a
// 'disconnect' in the cluster master, and then it will fork
// a new worker.
cluster.worker.disconnect();
} catch (err2) {
// oh well, not much we can do at this point.
console.error('Error closing server', err2.stack || err2);
}
});
opts = amino.utils.copy(opts);
var serviceSpec = new amino.Spec(opts.service);
function setupRequest (req, cb) {
req.on('error', function (err) {
console.error(err, '#error');
});
var stickyId;
if (opts.stickyCookie || opts.stickyIp || opts.stickyQuery) {
if (opts.stickyCookie && req.headers.cookie) {
stickyId = cookie.parse(req.headers.cookie)[opts.stickyCookie];
}
else if (opts.stickyIp) {
stickyId = addr(req);
}
else if (opts.stickyQuery) {
var query = parseUrl(req.url, true).query;
stickyId = query[opts.stickyQuery];
}
}
req._sReq = amino.requestService({
service: serviceSpec.service,
version: req.headers['x-amino-version'] || serviceSpec.version,
stickyId: req.headers['x-amino-stickyid'] || stickyId
}, cb);
}
function onReqError (err, req, res, sReq, spec) {
// For certain errors, we don't want the spec to be released.
if (['ECONNRESET', 'EADDRNOTAVAIL'].indexOf(err.code) === -1) {
sReq.emit('error', err);
}
// Connection resets, if coming from the client, are not log-worthy.
if (err.code !== 'ECONNRESET') {
console.error(err, '#error on ' + spec + ' for ' + req.method + ' ' + req.url);
}
if (opts.onError) {
opts.onError(err, req, res);
}
// WebSockets have no res
else if (res && !res.headersSent) {
res.writeHead(500, {'content-type': 'text/plain'});
res.write('Internal server error. Please try again later.');
res.end();
}
}
if (options.maintPage) {
var maintPage = require('dish').file(options.maintPage);
}
var proxy = httpProxy.createProxyServer();
var server = http.createServer(function (req, res) {
if (options.maintMode) {
var remoteIp = addr(req);
if (options.maintIps && ~options.maintIps.indexOf(remoteIp)) doProxy();
else {
maintPage(req, res, 503);
}
}
else {
doProxy();
}
function doProxy () {
setupRequest(req, function (spec) {
req._spec = spec;
d.run(function () {
proxy.web(req, res, { target: spec }, function httpErrorHandler (err, req, res) {
onReqError(err, req, res, req._sReq, req._spec);
});
});
});
}
});
server.on('upgrade', function (req, socket, head) {
setupRequest(req, function (spec) {
req._spec = spec;
d.run(function () {
proxy.ws(req, socket, head, { target: spec }, function wsErrorHandler (err, req, socket) {
onReqError(err, req, null, req._sReq, req._spec);
socket.destroy();
});
});
});
});
return server;
};
};