clustering works (re-started work for standalone)

This commit is contained in:
AJ ONeal 2015-11-17 08:18:56 +00:00
vanhempi 65645a7602
commit 96b7c9bb65
10 muutettua tiedostoa jossa 182 lisäystä ja 461 poistoa

Näytä tiedosto

@ -3,15 +3,15 @@
// TODO if RAM is very low we should not fork at all,
// but use a different process altogether
console.log('pid:', process.pid);
console.log('title:', process.title);
console.log('arch:', process.arch);
console.log('platform:', process.platform);
console.log('\n\n\n[MASTER] Welcome to WALNUT!');
console.info('pid:', process.pid);
console.info('title:', process.title);
console.info('arch:', process.arch);
console.info('platform:', process.platform);
console.info('\n\n\n[MASTER] Welcome to WALNUT!');
var cluster = require('cluster');
var path = require('path');
var minWorkers = 2;
//var minWorkers = 2;
var numCores = 1; // Math.max(minWorkers, require('os').cpus().length);
var workers = [];
var caddypath = '/usr/local/bin/caddy';
@ -23,8 +23,8 @@ var conf = {
// TODO externalInsecurePort?
, locked: false // TODO XXX
, ipcKey: null
, caddyfilepath: path.join(__dirname, 'Caddyfile')
, sitespath: path.join(__dirname, 'sites-enabled')
, caddyfilepath: path.join(__dirname, '..', 'Caddyfile')
, sitespath: path.join(__dirname, '..', 'sites-enabled')
};
var state = {};
var caddy;
@ -45,9 +45,10 @@ cluster.on('online', function (worker) {
var certPaths = [path.join(__dirname, 'certs', 'live')];
var info;
console.log('[MASTER] Worker ' + worker.process.pid + ' is online');
console.info('[MASTER] Worker ' + worker.process.pid + ' is online');
fork();
// TODO communicate config with environment vars?
info = {
type: 'com.daplie.walnut.init'
, conf: {
@ -72,7 +73,7 @@ cluster.on('online', function (worker) {
// calls init if init has not been called
state.caddy = caddy;
state.workers = workers;
require('./lib/master').touch(conf, state).then(function () {
require('../lib/master').touch(conf, state).then(function () {
info.type = 'com.daplie.walnut.webserver.onrequest';
info.conf.ipcKey = conf.ipcKey;
info.conf.memstoreSock = conf.memstoreSock;
@ -84,7 +85,7 @@ cluster.on('online', function (worker) {
});
cluster.on('exit', function (worker, code, signal) {
console.log('[MASTER] Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
console.info('[MASTER] Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
workers = workers.map(function (w) {
if (worker !== w) {
@ -102,7 +103,7 @@ cluster.on('exit', function (worker, code, signal) {
fork();
if (useCaddy) {
caddy = require('./lib/spawn-caddy').create(conf);
caddy = require('../lib/spawn-caddy').create(conf);
// relies on { localPort, locked }
caddy.spawn(conf);
}

113
boot/worker.js Normal file
Näytä tiedosto

@ -0,0 +1,113 @@
'use strict';
module.exports.create = function (opts) {
var id = '0';
function createAndBindServers(message, cb) {
var msg = message.conf;
require('../lib/local-server').create(msg.certPaths, msg.localPort, function (err, webserver) {
if (err) {
console.error('[ERROR] worker.js');
console.error(err.stack);
throw err;
}
console.info("#" + id + " Listening on " + msg.protocol + "://" + webserver.address().address + ":" + webserver.address().port, '\n');
return cb(webserver);
});
// TODO conditional if 80 is being served by caddy
require('../lib/insecure-server').create(msg.externalPort, msg.insecurePort);
}
//
// Worker Mode
//
function waitForConfig(message) {
if ('com.daplie.walnut.init' !== message.type) {
console.warn('[Worker] 0 got unexpected message:');
console.warn(message);
return;
}
process.removeListener('message', waitForConfig);
// NOTE: this callback must return a promise for an express app
createAndBindServers(message, function (webserver) {
var PromiseA = require('bluebird');
return new PromiseA(function (resolve) {
function initWebServer(srvmsg) {
if ('com.daplie.walnut.webserver.onrequest' !== srvmsg.type) {
console.warn('[Worker] 1 got unexpected message:');
console.warn(srvmsg);
return;
}
process.removeListener('message', initWebServer);
resolve(require('../lib/worker').create(webserver, srvmsg));
}
process.send({ type: 'com.daplie.walnut.webserver.listening' });
process.on('message', initWebServer);
}).then(function (app) {
console.info('[Worker Ready]');
return app;
});
});
}
//
// Standalone Mode
//
if (opts) {
// NOTE: this callback must return a promise for an express app
createAndBindServers(opts, function (webserver) {
var PromiseA = require('bluebird');
return new PromiseA(function (resolve) {
opts.getConfig(function (srvmsg) {
resolve(require('../lib/worker').create(webserver, srvmsg));
});
}).then(function (app) {
console.info('[Standalone Ready]');
return app;
});
});
} else {
// we are in cluster mode, as opposed to standalone mode
id = require('cluster').worker.id.toString();
// We have to wait to get the configuration from the master process
// before we can start our webserver
console.info('[Worker #' + id + '] online!');
process.on('message', waitForConfig);
}
//
// Debugging
//
process.on('exit', function (code) {
// only sync code can run here
console.info('uptime:', process.uptime());
console.info(process.memoryUsage());
console.info('[exit] process.exit() has been called (or master has killed us).');
console.info(code);
});
process.on('beforeExit', function () {
// async can be scheduled here
console.info('[beforeExit] Event Loop is empty. Process will end.');
});
process.on('unhandledRejection', function (err) {
// this should always throw
// (it means somewhere we're not using bluebird by accident)
console.error('[caught] [unhandledRejection]');
console.error(Object.keys(err));
console.error(err);
console.error(err.stack);
});
process.on('rejectionHandled', function (msg) {
console.error('[rejectionHandled]');
console.error(msg);
});
};

23
etc/init/caddy.conf Normal file
Näytä tiedosto

@ -0,0 +1,23 @@
# sudo rsync -av etc/init/caddy.conf /etc/init/caddy.conf
description "Caddy Server"
version "1.0"
author "AJ ONeal"
# Upstart has nothing in $PATH by default
env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Keep the server running on crash or machine reboot
respawn
respawn limit 10 120
start on runlevel [2345]
# Start the server using spark and redirect output to log files
script
DATE=`date '+%F_%H-%M-%S'`
cd /srv/walnut
# exec /usr/local/bin/caddy -conf /srv/walnut/Caddyfile -pidfile /tmp/caddy.pid \
exec start-stop-daemon --start --pidfile /tmp/caddy.pid --exec /usr/local/bin/caddy -- -conf /srv/walnut/Caddyfile --pidfile /tmp/caddy.pid \
> "./logs/access.caddy.${DATE}.log" \
2> "./logs/error.caddy.${DATE}.log"
end script

Näytä tiedosto

@ -14,17 +14,13 @@ module.exports.create = function (conf, deps, app) {
var PromiseA = require('bluebird');
var path = require('path');
console.log(route);
// TODO needs some version stuff (which would also allow hot-loading of updates)
// TODO version could be tied to sha256sum
var pkgpath = path.join(conf.apipath, (route.api.package || route.api.id), (route.api.version || ''));
console.log('pkgpath');
console.log(pkgpath);
return new PromiseA(function (resolve, reject) {
try {
route.route = require(pkgpath).create(conf, deps.app, app);
route.route = require(pkgpath).create(conf, deps, app);
} catch(e) {
reject(e);
return;
@ -37,9 +33,6 @@ module.exports.create = function (conf, deps, app) {
function api(req, res, next) {
var apps;
console.log('hostname', req.hostname);
console.log('headers', req.headers);
if (!vhostsMap[req.hostname]) {
// TODO keep track of match-only vhosts, such as '*.example.com',
// separate from exact matches
@ -71,14 +64,10 @@ module.exports.create = function (conf, deps, app) {
});
if (!apps) {
console.log('No apps to try for this hostname');
console.log(vhostsMap[req.hostname]);
next();
return;
}
//console.log(apps);
function nextify(err) {
var route;

Näytä tiedosto

@ -109,8 +109,6 @@ function getVhostsMap(config) {
vhosts.sort(sortApps);
vhosts.forEach(function (domain) {
console.log(domain.hostname, domain.pathname, domain.dirname);
if (!vhostsMap[domain.hostname]) {
vhostsMap[domain.hostname] = { pathnamesMap: {}, pathnames: [] };
}
@ -129,7 +127,6 @@ function getVhostsMap(config) {
module.exports.deserialize = deserialize;
module.exports.getVhostsMap = getVhostsMap;
module.exports.create = function (db) {
console.log('[DB -1]');
var wrap = require('dbwrap');
var dir = [

Näytä tiedosto

@ -1,343 +0,0 @@
'use strict';
module.exports.create = function (securePort, vhostsdir) {
var PromiseA = require('bluebird').Promise;
var serveStatic;
var fs = require('fs');
var path = require('path');
var dummyCerts;
var loopbackToken = require('crypto').randomBytes(32).toString('hex');
function handleAppScopedError(tag, domaininfo, req, res, fn) {
function next(err) {
if (!err) {
fn(req, res);
return;
}
if (res.headersSent) {
console.error('[ERROR] handleAppScopedError headersSent');
console.log(err);
console.log(err.stack);
return;
}
console.error('[ERROR] handleAppScopedError');
console.log(err);
console.log(err.stack);
res.writeHead(500);
res.end(
"<html>"
+ "<head>"
+ '<link rel="icon" href="favicon.ico" />'
+ "</head>"
+ "<body>"
+ "<pre>"
+ "<code>"
+ "Method: " + encodeURI(req.method)
+ '\n'
+ "Hostname: " + encodeURI(domaininfo.hostname)
+ '\n'
+ "App: " + encodeURI(domaininfo.pathname ? (domaininfo.pathname + '/') : '')
+ '\n'
+ "Route: " + encodeURI(req.url)//.replace(/^\//, '')
+ '\n'
// TODO better sanatization
+ 'Error: ' + (err.message || err.toString()).replace(/</g, '&lt;')
+ "</code>"
+ "</pre>"
+ "</body>"
+ "</html>"
);
}
return next;
}
function createPromiseApps(secureServer) {
return new PromiseA(function (resolve) {
var forEachAsync = require('foreachasync').forEachAsync.create(PromiseA);
var connect = require('connect');
// TODO make lazy
var app = connect().use(require('compression')());
var vhost = require('vhost');
var domainMergeMap = {};
var domainMerged = [];
function getDomainInfo(apppath) {
var parts = apppath.split(/[#%]+/);
var hostname = parts.shift();
var pathname = parts.join('/').replace(/\/+/g, '/').replace(/^\//, '');
return {
hostname: hostname
, pathname: pathname
, dirpathname: parts.join('#')
, dirname: apppath
, isRoot: apppath === hostname
};
}
function loadDomainMounts(domaininfo) {
var connectContext = {};
var appContext;
// should order and group by longest domain, then longest path
if (!domainMergeMap[domaininfo.hostname]) {
// create an connect / express app exclusive to this domain
// TODO express??
domainMergeMap[domaininfo.hostname] = {
hostname: domaininfo.hostname
, apps: connect()
, mountsMap: {}
};
domainMerged.push(domainMergeMap[domaininfo.hostname]);
}
if (domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname]) {
return;
}
console.log('[log] [once] Preparing mount for', domaininfo.hostname + '/' + domaininfo.dirpathname);
domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname] = function (req, res, next) {
res.setHeader('Strict-Transport-Security', 'max-age=10886400; includeSubDomains; preload');
function loadThatApp() {
var time = Date.now();
console.log('[log] LOADING "' + domaininfo.hostname + '/' + domaininfo.pathname + '"', req.url);
return getAppContext(domaininfo).then(function (localApp) {
console.info((Date.now() - time) + 'ms Loaded ' + domaininfo.hostname + ':' + securePort + '/' + domaininfo.pathname);
//if (localApp.arity >= 2) { /* connect uses .apply(null, arguments)*/ }
if ('function' !== typeof localApp) {
localApp = getDummyAppContext(null, "[ERROR] no connect-style export from " + domaininfo.dirname);
}
function fourohfour(req, res) {
res.writeHead(404);
res.end(
"<html>"
+ "<head>"
+ '<link rel="icon" href="favicon.ico" />'
+ "</head>"
+ "<body>"
+ "Cannot "
+ encodeURI(req.method)
+ " 'https://"
+ encodeURI(domaininfo.hostname)
+ '/'
+ encodeURI(domaininfo.pathname ? (domaininfo.pathname + '/') : '')
+ encodeURI(req.url.replace(/^\//, ''))
+ "'"
+ "<br/>"
+ "<br/>"
+ "Domain: " + encodeURI(domaininfo.hostname)
+ "<br/>"
+ "App: " + encodeURI(domaininfo.pathname)
+ "<br/>"
+ "Route : " + encodeURI(req.url)
+ "</body>"
+ "</html>"
);
}
// Note: pathname should NEVER have a leading '/' on its own
// we always add it explicitly
function localAppWrapped(req, res) {
console.log('[debug]', domaininfo.hostname + '/' + domaininfo.pathname, req.url);
localApp(req, res, handleAppScopedError('localApp', domaininfo, req, res, fourohfour));
}
try {
var localConnect = connect();
localConnect.use(require('connect-query')());
localConnect.use(localAppWrapped);
domainMergeMap[domaininfo.hostname].apps.use('/' + domaininfo.pathname, localConnect);
return localConnect;
} catch(e) {
console.error('[ERROR] '
+ domaininfo.hostname + ':' + securePort
+ '/' + domaininfo.pathname
);
console.error(e);
// TODO this may not work in web apps (due to 500), probably okay
res.writeHead(500);
res.end('{ "error": { "message": "[ERROR] could not load '
+ encodeURI(domaininfo.hostname) + ':' + securePort + '/' + encodeURI(domaininfo.pathname)
+ 'or default error app." } }');
}
});
}
function suckItDubDubDub(req, res) {
var newLoc = 'https://' + (req.headers.host||'').replace(/^www\./) + req.url;
res.statusCode = 301;
res.setHeader('Location', newLoc);
res.end("<html><head><title></title></head><body><!-- redirecting nowww --></body><html>");
}
function nextify() {
if (!appContext) {
appContext = loadThatApp();
}
if (!appContext.then) {
appContext(req, res, next);
} else {
appContext.then(function (localConnect) {
appContext = localConnect;
appContext(req, res, next);
});
}
}
if (!serveStatic) {
serveStatic = require('serve-static');
}
if (!connectContext.static) {
console.log('[static]', path.join(vhostsdir, domaininfo.dirname, 'public'));
connectContext.static = serveStatic(path.join(vhostsdir, domaininfo.dirname, 'public'));
}
if (/^www\./.test(req.headers.host)) {
if (/\.appcache\b/.test(req.url)) {
res.setHeader('Content-Type', 'text/cache-manifest');
res.end('CACHE MANIFEST\n\n# v0__DELETE__CACHE__MANIFEST__\n\nNETWORK:\n*');
return;
}
suckItDubDubDub(req, res);
return;
}
if (/^\/api\//.test(req.url)) {
nextify();
return;
}
connectContext.static(req, res, nextify);
};
domainMergeMap[domaininfo.hostname].apps.use(
'/' + domaininfo.pathname
, domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname]
);
return PromiseA.resolve();
}
function readNewVhosts() {
return fs.readdirSync(vhostsdir).filter(function (node) {
// not a hidden or private file
return '.' !== node[0] && '_' !== node[0];
}).map(getDomainInfo).sort(function (a, b) {
var hlen = b.hostname.length - a.hostname.length;
var plen = b.pathname.length - a.pathname.length;
// A directory could be named example.com, example.com# example.com##
// to indicate order of preference (for API addons, for example)
var dlen = b.dirname.length - a.dirname.length;
if (!hlen) {
if (!plen) {
return dlen;
}
return plen;
}
return plen;
});
}
function getDummyAppContext(err, msg) {
console.error('[ERROR] getDummyAppContext');
console.error(err);
console.error(msg);
return function (req, res) {
res.writeHead(500);
res.end('{ "error": { "message": "' + msg + '" } }');
};
}
function getLoopbackApp() {
return function (req, res) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify({ "success": true, "token": loopbackToken }));
};
}
function getAppContext(domaininfo) {
var localApp;
if ('loopback.daplie.invalid' === domaininfo.dirname) {
return getLoopbackApp();
}
try {
// TODO live reload required modules
localApp = require(path.join(vhostsdir, domaininfo.dirname, 'app.js'));
if (localApp.create) {
// TODO read local config.yml and pass it in
// TODO pass in websocket
localApp = localApp.create(secureServer, {
dummyCerts: dummyCerts
, hostname: domaininfo.hostname
, port: securePort
, url: domaininfo.pathname
});
if (!localApp) {
localApp = getDummyAppContext(null, "[ERROR] no app was returned by app.js for " + domaininfo.dirname);
}
}
if (!localApp.then) {
localApp = PromiseA.resolve(localApp);
} else {
return localApp.catch(function (e) {
console.error("[ERROR] initialization failed during create() for " + domaininfo.dirname);
console.error(e);
throw e;
//return getDummyAppContext(e, "[ERROR] initialization failed during create() for " + domaininfo.dirname);
});
}
} catch(e) {
localApp = getDummyAppContext(e, "[ERROR] could not load app.js for " + domaininfo.dirname);
localApp = PromiseA.resolve(localApp);
}
return localApp;
}
function loadDomainVhosts() {
domainMerged.forEach(function (domainApp) {
if (domainApp._loaded) {
return;
}
console.log('[log] [once] Loading all mounts for ' + domainApp.hostname);
domainApp._loaded = true;
app.use(vhost(domainApp.hostname, domainApp.apps));
app.use(vhost('www.' + domainApp.hostname, function (req, res) {
res.send({
error: {
message: "this is an api. ain't no www belong here"
, code: "E_WWW"
}
});
}));
});
}
// TODO pre-cache these once the server has started?
// return forEachAsync(rootDomains, loadCerts);
// TODO load these even more lazily
return forEachAsync(readNewVhosts(), loadDomainMounts).then(loadDomainVhosts).then(function () {
console.log('[log] TODO fix and use hotload');
//app.use(hotloadApp);
resolve(app);
return;
});
});
}
return { create: createPromiseApps };
};

Näytä tiedosto

@ -37,6 +37,14 @@ module.exports.create = function (webserver, info, state) {
});
var cstore = require('cluster-store');
if (info.conf.trustProxy) {
console.info('[Trust Proxy]');
app.set('trust proxy', ['loopback']);
//app.set('trust proxy', function (ip) { console.log('[ip]', ip); return true; });
} else {
console.info('[DO NOT trust proxy]');
}
/*
function unlockDevice(conf, state) {
return require('./lib/unlock-device').create().then(function (result) {
@ -65,7 +73,6 @@ module.exports.create = function (webserver, info, state) {
// I guess I just needs letsencrypt
function scrubTheDub(req, res, next) {
console.log('[no-www]', req.method, req.url);
var host = req.hostname;
if (!host || 'string' !== typeof host) {
@ -84,11 +91,23 @@ module.exports.create = function (webserver, info, state) {
require('./no-www').scrubTheDub(req, res);
}
if (info.trustProxy) {
app.set('trust proxy', ['loopback']);
//app.set('trust proxy', function (ip) { ... });
function caddyBugfix(req, res, next) {
// workaround for Caddy
// https://github.com/mholt/caddy/issues/341
if (app.get('trust proxy')) {
if (req.headers['x-forwarded-proto']) {
req.headers['x-forwarded-proto'] = (req.headers['x-forwarded-proto'] || '').split(/,\s+/g)[0] || undefined;
}
if (req.headers['x-forwarded-host']) {
req.headers['x-forwarded-host'] = (req.headers['x-forwarded-host'] || '').split(/,\s+/g)[0] || undefined;
}
}
next();
}
app.use('/', scrubTheDub);
app.use('/', caddyBugfix);
return PromiseA.all([
cstore.create({
@ -137,7 +156,6 @@ module.exports.create = function (webserver, info, state) {
*/
function handleApi(req, res, next) {
console.log('[API]', req.method, req.url);
var myApp;
if (!/^\/api/.test(req.url)) {
@ -155,8 +173,8 @@ module.exports.create = function (webserver, info, state) {
if (apiHandler) {
if (apiHandler.then) {
apiHandler.then(function (app) {
app(req, res, next);
apiHandler.then(function (myApp) {
myApp(req, res, next);
});
return;
}
@ -167,12 +185,16 @@ module.exports.create = function (webserver, info, state) {
// apiHandler = require('./vhost-server').create(info.localPort, vhostsdir).create(webserver, app)
myApp = express();
if (app.get('trust proxy')) {
myApp.set('trust proxy', app.get('trust proxy'));
}
apiHandler = require('./api-server').create(
{ apppath: '../packages/apps/'
, apipath: '../packages/apis/'
, vhostsMap: vhostsMap
, server: webserver
, externalPort: info.externalPort
, apiPrefix: '/api'
}
, { app: myApp
, memstore: memstore
@ -185,11 +207,6 @@ module.exports.create = function (webserver, info, state) {
}
).api;
// TODO
// X-Forwarded-For
// X-Forwarded-Proto
console.log('api server', req.hostname, req.secure, req.ip);
apiHandler(req, res, next);
}

Näytä tiedosto

@ -68,7 +68,7 @@
"escape-string-regexp": "1.x",
"etag": "^1.5.1",
"express": "4.x",
"express-lazy": "1.x",
"express-lazy": "^1.1.1",
"express-session": "^1.11.3",
"finalhandler": "^0.3.4",
"foreachasync": "5.x",

Näytä tiedosto

@ -20,7 +20,7 @@ Math.random = function () {
};
if (cluster.isMaster) {
require('./master');
require('./boot/master');
} else {
require('./worker');
require('./boot/worker').create(null);
}

Näytä tiedosto

@ -1,76 +0,0 @@
'use strict';
var cluster = require('cluster');
var id = cluster.worker.id.toString();
function waitForInit(message) {
if ('com.daplie.walnut.init' !== message.type) {
console.warn('[Worker] 0 got unexpected message:');
console.warn(message);
return;
}
var msg = message.conf;
process.removeListener('message', waitForInit);
require('./lib/local-server').create(msg.certPaths, msg.localPort, function (err, webserver) {
if (err) {
console.error('[ERROR] worker.js');
console.error(err.stack);
throw err;
}
console.log("#" + id + " Listening on " + msg.protocol + "://" + webserver.address().address + ":" + webserver.address().port, '\n');
var PromiseA = require('bluebird');
return new PromiseA(function (resolve) {
function initWebServer(srvmsg) {
if ('com.daplie.walnut.webserver.onrequest' !== srvmsg.type) {
console.warn('[Worker] 1 got unexpected message:');
console.warn(srvmsg);
return;
}
process.removeListener('message', initWebServer);
resolve(require('./lib/worker').create(webserver, srvmsg));
}
process.send({ type: 'com.daplie.walnut.webserver.listening' });
process.on('message', initWebServer);
});
});
// TODO conditional if 80 is being served by caddy
require('./lib/insecure-server').create(msg.externalPort, msg.insecurePort);
}
// We have to wait to get the configuration from the master process
// before we can start our webserver
console.log('[Worker #' + id + '] online!');
process.on('message', waitForInit);
//
// Debugging
//
process.on('exit', function (code) {
// only sync code can run here
console.log('uptime:', process.uptime());
console.log(process.memoryUsage());
console.log('[exit] process.exit() has been called (or master has killed us).');
console.log(code);
});
process.on('beforeExit', function (msg) {
// async can be scheduled here
console.log('[beforeExit] Event Loop is empty. Process will end.');
console.log(msg);
});
process.on('unhandledRejection', function (err) {
// this should always throw
// (it means somewhere we're not using bluebird by accident)
console.error('[caught] [unhandledRejection]');
console.error(Object.keys(err));
console.error(err);
console.error(err.stack);
});
process.on('rejectionHandled', function (msg) {
console.error('[rejectionHandled]');
console.error(msg);
});