cluster-service
Version:
Turns your single process code into a fault-resilient multi-process service with built-in REST & CLI support
440 lines (370 loc) • 13 kB
JavaScript
var cservice = require("../cluster-service");
var cluster = require("cluster");
var workersHelper = require("./workers");
var path = require("path");
var async = require("async");
var fs = require("fs");
module.exports = {
start: start,
stop: stop,
version: version,
promote: promote,
updateProxyWorkers: updateProxyWorkers,
getProxyWorkers: getProxyWorkers,
getVersionWorkers: getVersionWorkers,
info: info
};
function start(options, cb) {
if (cluster.isWorker === true) {
cservice.log("Proxy cannot be started from worker".warn);
return cb && cb("Proxy cannot be started from worker");
}
var configPath = options.configPath || cservice.options.proxy;
if (typeof configPath !== "string") {
// disabled
return cb && cb();
}
if (cservice.locals.proxy.enabled === true) {
cservice.log("Proxy already running".warn);
return cb && cb("Proxy already running");
}
cservice.locals.proxy.configPath = path.resolve(configPath);
cservice.locals.proxy.options =
JSON.parse(fs.readFileSync(cservice.locals.proxy.configPath))
;
options = cservice.locals.proxy.options;
if (Array.isArray(options.bindings) === false ||
options.bindings.length === 0) {
options.bindings = [{ port: 80, workerCount: 2 }]; // default
}
options.nonDefaultWorkerCount = options.nonDefaultWorkerCount || 1;
options.nonDefaultWorkerIdleTime = options.nonDefaultWorkerIdleTime || 3600;
cservice.locals.proxy.versionPath =
options.versionPath || path.dirname(cservice.locals.proxy.configPath)
;
cservice.locals.proxy.versionPath =
path.resolve(cservice.locals.proxy.versionPath)
;
cservice.locals.proxy.workerFilename = options.workerFilename || "worker.js";
cservice.locals.proxy.versionHeader = options.versionHeader || "x-version";
var portRange = (options.versionPorts || "11000-12000").split("-");
cservice.locals.proxy.portRange = {
min: parseInt(portRange[0]),
max: parseInt(portRange[1])
};
cservice.locals.proxy.nextAvailablePortIndex = 0;
var proxyWorkerTasks = options.bindings.map(function(b) {
return function(cb) {
var workerOptions = {
type: "proxy", // proxy worker type
worker: path.resolve(__dirname, "proxy-worker.js"),
bindingInfo: JSON.stringify(b),
versionPath: cservice.locals.proxy.versionPath,
versionHeader: cservice.locals.proxy.versionHeader,
workerFilename: cservice.locals.proxy.workerFilename,
versions: JSON.stringify(cservice.locals.proxy.versions),
count: b.workerCount || 2
};
cservice.newWorker(workerOptions, cb);
};
});
cservice.locals.proxy.refreshnessTimer =
setInterval(checkVersionsForFreshness, 10000)
;
cservice.locals.proxy.refreshnessTimer.unref();
cservice.locals.proxy.enabled = true;
async.parallel(proxyWorkerTasks, function (err) {
var portArr = options.bindings.map(function(b) {
return b.port.toString().data;
});
if (err) {
cservice.error("Proxy failed to run on ports ".error +
portArr.join(",".info) + " with error ".error + err.toString().data);
return cb && cb(err);
}
cservice.log("Proxy running on ports ".info + portArr.join(",".info));
if (!cservice.locals.proxy.options.defaultVersion) {
// no current version
return cb && cb();
}
version(cservice.locals.proxy.options.defaultVersion,
{ workerCount: cservice.locals.options.workerCount },
function (err, version) {
if (err) {
cservice.error("Proxy failed to run on ports ".error +
portArr.join(",".info) + " with error ".error + err.toString().data
);
return cb && cb(err);
}
return cb && cb();
});
});
}
function stop(cb) {
if (cluster.isWorker === true) {
cservice.log("Proxy cannot be stopped from worker".warn);
return cb && cb("Proxy cannot be stopped from worker");
}
if (typeof cservice.locals.proxy.configPath !== "string") {
// disabled
return cb && cb();
}
if (cservice.locals.proxy.enabled === false) {
cservice.log("Proxy not running".warn);
return cb && cb("Proxy not running");
}
clearInterval(cservice.locals.proxy.refreshnessTimer);
cservice.locals.proxy.refreshnessTimer = null;
// now lets trigger a shutdown
cservice.trigger("shutdown", function(err, result) {
cservice.locals.proxy.enabled = false;
return cb && cb();
}, "all");
}
function version(versionStr, options, cb) {
if (cluster.isWorker === true) {
cservice.log("Proxy cannot invoke 'version' from worker".warn);
return cb && cb("Proxy cannot invoke 'version' from worker");
}
options = options || {};
if (isNaN(options.workerCount) === true) {
options.workerCount =
(versionStr === cservice.locals.proxy.options.defaultVersion)
? cservice.locals.options.workerCount
: cservice.locals.proxy.options.nonDefaultWorkerCount
;
}
// detect current version worker count
var currentVersionWorkers = getVersionWorkers(versionStr);
// determine worker delta from desired count and actual count
var workerCountDelta = options.workerCount - currentVersionWorkers.length;
// get existing version listing
var v = cservice.locals.proxy.versions[versionStr];
if (v) {
// update version lastAccess
v.lastAccess = Date.now();
}
// if version worker count is already current, nothing more to do
if (workerCountDelta === 0) {
return cb && cb();
}
if (workerCountDelta < 0) {
// if desired version count is less than current, reduce worker count
var workersToShutdown = currentVersionWorkers.length - options.workerCount;
var shutdownTasks = [];
for (var workerToShutdown = 0; workerToShutdown < workersToShutdown;
workerToShutdown++) {
shutdownTasks.push(
getWorkerShutdownTask(
currentVersionWorkers[workerToShutdown],
options.reason || "proxy version"
)
);
}
// shutdown all at once
async.parallel(shutdownTasks, function (err, results) {
return cb && cb(err);
});
return;
}
// if desired version count is more than current, spin up new workers
var workerPath = path.resolve(cservice.locals.proxy.versionPath,
versionStr, cservice.locals.proxy.workerFilename
);
// use existing port if available, otherwise allocate a new one
var versionPort = (v && v.port) || getNextAvailablePort();
cservice.trigger("start", function (err, result) {
return cb && cb(err, result);
}, workerPath, {
count: workerCountDelta,
version: versionStr,
PROXY_PORT: versionPort
});
}
function getWorkerShutdownTask(worker, reason) {
return function(cb) {
worker.cservice.reason = reason;
cservice.trigger("shutdown", cb, worker.process.pid);
};
}
function getNextAvailablePort() {
// always continue where we left off from the last time
// we fetched an available port.
// this generally will allow us to return in O(1),
// unless there are tons of active versions.
var totalPorts = cservice.locals.proxy.portRange.max
- cservice.locals.proxy.portRange.min
;
for (var i = 0; i < totalPorts; i++) {
var port = cservice.locals.proxy.portRange.min
+ ((cservice.locals.proxy.nextAvailablePortIndex + i) % totalPorts);
if (isPortInUse(port) === false) {
cservice.locals.proxy.nextAvailablePortIndex = ((i + 1) % totalPorts);
return port;
}
}
throw new Error(
"All proxy ports have been used up! Try increasing range of " +
"`proxy.versionPorts` or reducing `proxy.nonDefaultWorkerIdleTime`.");
}
function isPortInUse(port) {
for (var k in cservice.locals.proxy.versions) {
if (cservice.locals.proxy.versions.hasOwnProperty(k) === false) {
continue; // ignore
}
if (cservice.locals.proxy.versions[k].port === port) {
return true; // NOT available
}
}
// if we get this far, we're OK to use port
return false;
}
function promote(versionStr, options, cb) {
if (cluster.isWorker === true) {
cservice.log("Proxy cannot invoke 'promote' from worker".warn);
return cb && cb("Proxy cannot invoke 'promote' from worker");
}
options = options || {};
options.workerCount = options.workerCount ||
cservice.locals.options.workerCount
;
var oldVersion = cservice.locals.proxy.versions[
cservice.locals.proxy.options.defaultVersion
];
// set to-be-promoted version to desired worker count
version(versionStr, options, function (err) {
if (err) {
// pass failure on
return cb && cb(err);
}
// persist to-be-promoted version
cservice.locals.proxy.options.defaultVersion = versionStr;
fs.writeFile(cservice.locals.proxy.configPath,
JSON.stringify(cservice.locals.proxy.options, null, " "), function(err) {
if (err) {
// pass failure on
cservice.error("Failed to proxy promote version".error +
versionStr.info
);
return cb && cb(err);
}
// notify proxy-workers of promoted version
updateProxyWorkers();
cservice.log("Proxy promoted version ".success +
versionStr.info +
" successfully".success
);
// bring previously promoted version down to
// `nonDefaultWorkerCount` workers, but no need to wait for callback
if (oldVersion && oldVersion.name !== versionStr) {
version(oldVersion.name,
{
workerCount: cservice.locals.proxy.options.nonDefaultWorkerCount,
reason: "proxy demote"
}, function(err) {
if (err) {
return cservice.error("Failed to proxy demote version".error +
oldVersion.name.info
);
}
cservice.log("Proxy demoted old version ".success +
oldVersion.name.info +
" successfully".success
);
}
);
}
if (cb) {
setImmediate(cb);
}
});
});
}
function updateProxyWorkers() {
var msg = cservice.msgBus.createMessage("proxyVersions", {
versions: cservice.locals.proxy.versions,
defaultVersion: cservice.locals.proxy.options.defaultVersion
});
getProxyWorkers().forEach(function(worker) {
worker.send(msg);
});
}
function info(cb) {
if (cluster.isWorker === true) {
cservice.log("Proxy cannot invoke 'info' from worker".warn);
return cb && cb("Proxy cannot invoke 'info' from worker");
}
var now = Date.now();
var proxyWorkers = getProxyWorkers().map(function(worker) {
var bindingInfo = JSON.parse(worker.cservice.bindingInfo);
return {
port: bindingInfo.port,
ssl: typeof bindingInfo.tlsOptions === "object"
};
});
var versionWorkers = getVersionWorkers().map(function(worker) {
var versionInfo = cservice.locals.proxy.versions[worker.cservice.version];
return {
worker: worker.cservice.worker,
version: worker.cservice.version,
lastAccess: versionInfo ?
Math.round((now - versionInfo.lastAccess) / 1000) : "?"
};
});
cb(null, {
versionPath: cservice.locals.proxy.versionPath,
workerFilename: cservice.locals.proxy.workerFilename,
portRange: cservice.locals.proxy.portRange,
options: cservice.locals.proxy.options,
proxyWorkers: proxyWorkers,
versionWorkers: versionWorkers
});
}
function getProxyWorkers() {
return cservice.workers.filter(function(worker) {
return worker.cservice.type === "proxy";
});
}
function getVersionWorkers(explicitVersion) {
return cservice.workers.filter(function(worker) {
var result =
typeof worker.cservice.version === "string" &&
(!explicitVersion || worker.cservice.version === explicitVersion)
;
return result;
});
}
function isVersionRunning(versionStr, cb) {
if (!(versionStr in cservice.locals.proxy.versions)) {
return false; // version not available
}
// are any worker processes running desired version?
var workers = cservice.locals.workerProcesses;
for (var i = 0; i < workers.length; i++) {
var worker = workers[i];
var pid = worker.process.pid;
}
return false; // no workers running desired version
}
function checkVersionsForFreshness() {
var now = Date.now();
for (var k in cservice.locals.proxy.versions) {
if (
// live version is exempt
k === cservice.locals.proxy.options.defaultVersion ||
// verify a valid version
!cservice.locals.proxy.versions.hasOwnProperty(k)) {
continue; // skip
}
var v = cservice.locals.proxy.versions[k];
var diff = (now - v.lastAccess) / 1000; // seconds
if (diff < cservice.locals.proxy.options.nonDefaultWorkerIdleTime) {
continue; // all OK
}
cservice.log("Proxy version ".warn + k.info +
" shutting down due to inactivity".warn
);
// kill all the things
version(k, { workerCount: 0 });
}
}