thywill
Version:
A Node.js clustered framework for single page web applications based on asynchronous messaging.
369 lines (346 loc) • 14.5 kB
JavaScript
/**
* @fileOverview
* Exports functions to start up one of the cluster members running the Display
* application.
*/
var express = require('express');
var RedisSessionStore = require('connect-redis')(express);
var RedisSocketStore = require('socket.io/lib/stores/redis');
var http = require('http');
var redis = require('redis');
var Display = require('./display');
var config = require('../../../serverConfig/thywill/baseThywillConfig');
var Thywill = require('thywill');
/**
* Obtain a configuration object.
*
* @param {number} port
* The port to listen on.
* @param {string} clusterMemberId
* The name of the cluster member to start.
* @return {object}
* The configuration object.
*/
exports.getConfig = function (port, clusterMemberId) {
// All of the Redis clients that will be needed.
var redisClients = {
pub: redis.createClient(6379, '127.0.0.1'),
sub: redis.createClient(6379, '127.0.0.1'),
other: redis.createClient(6379, '127.0.0.1')
};
// Express application and http.Server.
var app = express();
var server = http.createServer(app).listen(port);
/**
* Thywill configuration is a nested set of objects, one for each of the
* components. The configuration specifies which component implementations
* to use, and provides their specific configuration parameters.
*/
var config = {
// Parameters for the main Thywill wrapper, such as port to listen on,
// and configuration for the administrative interface.
thywill: {
process: {
// Group ID and User ID for the Thywill process to downgrade to
// once launched - this allows for such things as launching as
// root to bind to privileged ports, then running later as a lesser
// user.
//
// Not used on Windows, of course.
//
// Note that if you set either of these to a numeric uid, it must be a
// number not a numeric string - 312, not '312'.
groupId: 'node',
userId: 'node'
}
},
// The cache manager in an interface for creating and managing (usually
// in-memory) caches.
cacheManager: {
// Here we specify the use of the core LRU implementation.
implementation: {
type: 'core',
name: 'lruCacheManager'
}
},
// The client interface component manages communication between web browser
// and server, and thus will usually have a fairly large set of
// configuration parameters.
clientInterface: {
// Here we specify the use of the core Socket.IO and Express
// implementation.
implementation: {
type: 'core',
name: 'socketIoExpressClientInterface'
},
// The base path is prepended to all URLs generated by the client
// interface, allowing distinction between different Thywill Node.js
// services running as backends on the same server.
baseClientPath: '/display',
// Attributes for the <body> element of the Thywill main application page
// are specified here. Attribute values can be arrays, in which case they
// will be joined to a space-delimited string.
elementAttributesBody: {},
// Attributes for the <html> element of the Thywill main application page
// are specified here. Attribute values can be arrays, in which case they
// will be joined to a space-delimited string.
elementAttributesHtml: {
class: ['no-js'],
lang: 'en'
},
// If true, minify and merge bootstrap CSS into a single resource. This
// is the CSS initially loaded when a client connections.
minifyCss: true,
// If true, minify and merge bootstrap Javascript into a single resource.
// This is the Javascript initially loaded when a client connections.
minifyJavascript: true,
// A namespace to apply to Socket.IO communications, allowing other
// Socket.IO applications to run on the same server in their own,
// separate namespaces.
namespace: '/display',
// The HTML page encoding to use.
pageEncoding: 'utf-8',
// The http.Server and Express application are set here.
server: {
app: app,
server: server
},
sessions: {
cookieSecret: 'some long random string',
cookieKey: 'sid',
// Create a session store. Since this is a clustered example, use a
// Redis-backed store.
store: new RedisSessionStore({
client: redisClients.other
})
},
// Configuration to apply to the Socket.IO client on setup. This is
// used as-is. See:
// https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO
socketClientConfig: {
'max reconnection attempts': 50,
// This MUST match the value of the socketConfig.*.resource value, but
// with the leading / dropped.
'resource': 'display/socket.io'
},
// Configuration to apply to the Socket.IO server setup. Both the global
// configuration and any environment-specific configurations are applied.
// Socket.IO will only use environment-specific parameters if the
// environment name matches the NODE_ENV environment variable. See:
// https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO
socketConfig: {
// Global Socket.IO configuration, will apply unless overruled by a
// environment-specific value.
global: {
'browser client minification': true,
'browser client etag': true,
// A comparatively low level of logging. If trying to debug whether
// or not websockets are working at all, setting log level to 3 is
// helpful.
'log level': 1,
// Match origin protocol MUST be true - when Node.js is operating
// behind an SSL proxy, which is the default Thywill setup, the
// difference between ws: and wss: websocket protocols becomes
// important. This ensures that Socket.IO does the right thing.
'match origin protocol': true,
// This MUST match the value of the socketClientConfig.resource value,
// but with the addition of a leading /.
'resource': '/display/socket.io',
// If using multiple Node.js processes and process A will need to
// send messages to connections on process B, then this must be a
// RedisStore and usePubSubForSending must be true. Otherwise, use a
// MemoryStore.
'store': new RedisSocketStore({
redisPub: redisClients.pub,
redisSub: redisClients.sub,
redisClient: redisClients.other
}),
// The transports to use. We're trying to be modern here and stick
// with websockets only. This means some browsers will fail
// miserably - so adjust as needed for your circumstance.
'transports': ['websocket']
},
// If NODE_ENV = production, then values set here will override the
// global configuration.
production: {
'log level': 0
}
},
// Other text file encodings, such as when loading resources from the
// file system.
textEncoding: 'utf-8',
// The path for a page to be called by proxy and monitoring up checks.
upCheckClientPath: '/alive',
// If you have set up an application with multiple Node.js processes in
// the backend, then each individual connection socket will only exist in
// one of the processes. Your application may be structured in a way that
// requires process A to send a message to a socket connected to process
// B. If this is the case, you must set usePubSubForSending to true, and
// provide a RedisStore to the socketConfig.store configuration
// parameter.
usePubSubForSending: false
},
// Add a ClientTracker component so as to enable the various
// cross-cluster-member notices of connection, disconnection, etc.
clientTracker: {
implementation: {
type: 'extra',
name: 'inMemoryClientTracker'
}
},
// The cluster implementation.
cluster: {
implementation: {
type: 'core',
name: 'httpCluster'
},
// The cluster has two members.
clusterMembers: {
'alpha': {
host: '127.0.0.1',
port: 20091
},
'beta': {
host: '127.0.0.1',
port: 20092
}
},
// Up checks run via HTTP.
upCheck: {
// How many consecutive failures in order to consider a server down?
consecutiveFailedChecks: 2,
// Interval is the time between the end of one request and the start
// of the next one. This can lag late for all sorts of reasons: e.g.
// network latency in the request, or more likely the Node.js process
// on client or endpoint is doing something else and being slow in
// getting to either launch the request or respond to it.
interval: 200,
// Adjust the timeout for expectations as to how long a cluster member
// can likely take to respond. It should be close to immediate unless
// there are long-running and computationally expensive tasks taking
// place.
requestTimeout: 500
},
// The local member name is drawn from the arguments.
localClusterMemberId: clusterMemberId,
// Again, adjust for expectations as to how long a cluster member can
// take to immediately respond to a requests.
taskRequestTimeout: 500
},
// This core component provides an interface for logging.
log: {
// We specify the simple console log implementation, which emits log
// messages via console.log(). Since a Thywill process will run as a
// service and direct stdout to a log file, this is usually just fine.
implementation: {
type: 'core',
name: 'consoleLog'
},
// The date format used in the log output.
dateFormat: 'ddd mmm dS yyyy HH:MM:ss Z',
// The minimum log level to be logged by this implementation. Lesser log
// levels are ignored. The log levels are, in order,
// [debug, warn, error].
level: 'debug'
},
// The minifer component manages minification and merging of CSS and
// Javascript resources that are assembled by Thywill.
minifier: {
// The ugly implementation cobbles together Uglify.js and CleanCSS
// for minification. It is, in fact, pretty ugly.
implementation: {
type: 'core',
name: 'uglyMinifier'
},
// The base path to use when defining a new resource for merged CSS.
cssBaseClientPath: '/display/css',
// The base path to use when defining a new resource for merged
// Javascript.
jsBaseClientPath: '/display/js'
},
// Use a Redis-backed ResourceManager to allow resources to be shared
// between cluster members - not that this is important for this particular
// example application, but it will be for most clustered applications.
resourceManager: {
implementation: {
type: 'core',
name: 'redisResourceManager'
},
cacheSize: 100,
redisPrefix: 'thywill:display:resource:',
redisClient: redisClients.other
},
// The template component provides an interface to a templating engine,
// always useful when slinging around HTML and Javascript.
templateEngine: {
// Specify the Handlebars templating engine implementation.
implementation: {
type: 'core',
name: 'handlebarsTemplateEngine'
},
// The maximum number of compiled templates to retain in an LRU cache.
templateCacheLength: 100
}
};
return config;
};
/**
* Start an application process running.
*
* Use in one of these ways, with the callback being optional.
*
* start (config, [callback])
* start (port, clusterMemberId, [callback])
*/
exports.start = function () {
// Sort out the configuration.
var config;
if (typeof arguments[0] === 'object') {
config = arguments[0];
} else {
config = exports.getConfig(arguments[0], arguments[1]);
}
// Sort out the callback for the launch.
var callback;
if (typeof arguments[arguments.length - 1] === 'function') {
callback = arguments[arguments.length - 1];
} else {
callback = function (error, thywill) {
if (error) {
if (error instanceof Error) {
error = error.stack;
}
console.error('Thywill launch failed with error: ' + error);
process.exit(1);
}
// Protect the Redis clients from hanging if they are timed out by the server.
for (var key in config._redisClients) {
thywill.protectRedisClient(config._redisClients[key]);
}
thywill.log.info('Thywill is ready to run cluster member [' + config.cluster.localClusterMemberId + '] for the Display example application.');
};
}
// Add minimal configuration to the Express application: just the cookie and
// session middleware.
var app = config.clientInterface.server.app;
app.use(express.cookieParser(config.clientInterface.sessions.cookieSecret));
app.use(express.session({
cookie: {
httpOnly: true
},
key: config.clientInterface.sessions.cookieKey,
secret: config.clientInterface.sessions.cookieSecret,
store: config.clientInterface.sessions.store
}));
// Middleware and routes might be added here or after Thywill launches. Either
// way is just fine and won't interfere with Thywill's use of Express to serve
// resources. e.g. adding a catch-all here is acceptable:
app.all('*', function (req, res, next) {
res.statusCode = 404;
res.send('No such resource.');
});
// Instantiate an application object.
var display = new Display('display');
// And off we go: launch a Thywill instance to run the the application.
Thywill.launch(config, display, callback);
};