From d68f739d7d717639500468295ecc73b75c77e724 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 17 Feb 2013 15:39:29 -0800 Subject: [PATCH 1/4] Add pack server events --- lib/pack.js | 235 ++++++++++++++++++++++++++++------------------------ 1 file changed, 126 insertions(+), 109 deletions(-) diff --git a/lib/pack.js b/lib/pack.js index db440cf4a..c126ef00f 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -2,6 +2,7 @@ var Fs = require('fs'); var Path = require('path'); +var Events = require('events'); var Async = require('async'); var Server = require('./server'); var Utils = require('./utils'); @@ -12,14 +13,15 @@ var Utils = require('./utils'); var internals = {}; -exports = module.exports = internals.Pack = function (config) { +exports = module.exports = internals.Pack = function (config) { this.config = Utils.clone(config || {}); // Plugin shared configuration this.servers = []; // List of all pack server members this.labels = {}; // Server [names] organized by labels this.names = {}; // Servers indexed by name + this.events = new Events.EventEmitter(); // Consolidated subscription to all servers' events - return this; + return this; }; @@ -30,7 +32,7 @@ exports = module.exports = internals.Pack = function (config) { }; */ -internals.Pack.prototype.server = function (name, server, options) { +internals.Pack.prototype.server = function (name, server, options) { var self = this; @@ -46,13 +48,13 @@ internals.Pack.prototype.server = function (name, server, options) { // Add standard labels if (options.autoLabel !== false) { // Defaults to true - if (server.settings.tls) { - serverLabels.push('secure'); + if (server.settings.tls) { + serverLabels.push('secure'); } - if (server.cache) { - serverLabels.push('cached'); - } + if (server.cache) { + serverLabels.push('cached'); + } } serverLabels = Utils.unique(serverLabels); @@ -64,34 +66,44 @@ internals.Pack.prototype.server = function (name, server, options) { // Add to labels - serverLabels.forEach(function (label) { + serverLabels.forEach(function (label) { self.labels[label] = self.labels[label] || []; - self.labels[label].push(name); + self.labels[label].push(name); + }); + + // Subscribe to events + + ['log', 'response', 'tail'].forEach(function (event) { + + server.on(event, function (request, data) { + + self.events.emit(event, request, data); + }); }); }; -internals.Pack.prototype.validate = function (plugin) { +internals.Pack.prototype.validate = function (plugin) { Utils.assert(plugin, 'Missing plugin'); - if (!plugin.name) { - return new Error('Plugin missing name'); + if (!plugin.name) { + return new Error('Plugin missing name'); } - if (!plugin.version) { - return new Error('Plugin missing version'); + if (!plugin.version) { + return new Error('Plugin missing version'); } if (!plugin.register || - typeof plugin.register !== 'function') { + typeof plugin.register !== 'function') { - return new Error('Plugin missing register() method'); + return new Error('Plugin missing register() method'); } // Valid - return null; + return null; }; @@ -108,7 +120,7 @@ internals.Pack.prototype.validate = function (plugin) { }; */ -internals.Pack.prototype.register = function (plugin/*, [options], callback */) { +internals.Pack.prototype.register = function (plugin/*, [options], callback */) { var self = this; @@ -121,94 +133,99 @@ internals.Pack.prototype.register = function (plugin/*, [options], callback */) Utils.assert(callback, 'Missing callback'); var invalid = this.validate(plugin); - if (invalid) { - return callback(invalid); + if (invalid) { + return callback(invalid); } // Add plugin to servers lists var pluginName = options.name || plugin.name; - this.servers.forEach(function (server) { + this.servers.forEach(function (server) { - server.plugin.list[pluginName] = plugin; + server.plugin.list[pluginName] = plugin; }); // Permissions - var permissions = { + var permissions = { route: true, helper: true, state: true, - ext: false + events: true, + ext: false }; Utils.merge(permissions, options.permissions); // Setup pack interface - var step = function (criteria, subset) { + var step = function (criteria, subset) { var selection = self.select(criteria, subset); - var methods = { + var methods = { version: Utils.version, config: self.config, length: selection.servers.length, options: options.plugin || {}, next: callback, - api: function (set) { + api: function (set) { - selection.servers.forEach(function (server) { + selection.servers.forEach(function (server) { server.plugins[pluginName] = server.plugins[pluginName] || {}; - Utils.merge(server.plugins[pluginName], set); - }); + Utils.merge(server.plugins[pluginName], set); + }); }, - select: function (criteria) { + select: function (criteria) { - return step(criteria, selection.index); - } + return step(criteria, selection.index); + } }; - if (permissions.route) { - methods.route = function (options) { + if (permissions.route) { + methods.route = function (options) { - self._applySync(selection.servers, Server.prototype.route, [options]); - }; + self._applySync(selection.servers, Server.prototype.route, [options]); + }; } - if (permissions.state) { - methods.state = function (name, options) { + if (permissions.state) { + methods.state = function (name, options) { - self._applySync(selection.servers, Server.prototype.state, [name, options]); - }; + self._applySync(selection.servers, Server.prototype.state, [name, options]); + }; } - if (permissions.helper) { - methods.helper = function (name, method, options) { + if (permissions.helper) { + methods.helper = function (name, method, options) { + + self._applySync(selection.servers, Server.prototype.helper, [name, method, options]); + }; + } - self._applySync(selection.servers, Server.prototype.helper, [name, method, options]); - }; + if (permissions.events) { + methods.events = self.events; } - if (permissions.ext) { - methods.ext = function (event, func) { + if (permissions.ext) { + methods.ext = function (event, func) { - self._applySync(selection.servers, Server.prototype.ext, [event, func]); - }; + self._applySync(selection.servers, Server.prototype.ext, [event, func]); + }; } - return methods; + return methods; }; // Register - plugin.register.call(null, step(), options.plugin || {}, callback); + plugin.register.call(null, step(), options.plugin || {}, callback); }; -internals.Pack.prototype.select = function (criteria, subset) { +internals.Pack.prototype.select = function (criteria, subset) { var self = this; @@ -216,85 +233,85 @@ internals.Pack.prototype.select = function (criteria, subset) { var names = []; - if (criteria) { + if (criteria) { if (criteria.names || - criteria.name) { + criteria.name) { - ['names', 'name'].forEach(function (item) { names = names.concat(criteria[item] || []); }); + ['names', 'name'].forEach(function (item) { names = names.concat(criteria[item] || []); }); } if (criteria.labels || - criteria.label) { + criteria.label) { var labels = []; ['labels', 'label'].forEach(function (item) { labels = labels.concat(criteria[item] || []); }); - labels.forEach(function (label) { + labels.forEach(function (label) { - names = names.concat(self.labels[label]); - }); + names = names.concat(self.labels[label]); + }); } - Utils.unique(names); + Utils.unique(names); } - else { - names = names.concat(Object.keys(subset || this.names)); + else { + names = names.concat(Object.keys(subset || this.names)); } var servers = []; var index = {}; - names.forEach(function (name) { + names.forEach(function (name) { if (subset && - !subset[name]) { + !subset[name]) { - return; + return; } var server = self.names[name]; - if (server) { + if (server) { servers.push(server); - index[name] = true; - } + index[name] = true; + } }); - return { servers: servers, index: index }; + return { servers: servers, index: index }; }; -internals.Pack.prototype.require = function (name/*, [options], callback*/) { +internals.Pack.prototype.require = function (name/*, [options], callback*/) { var options = (arguments.length === 3 ? arguments[1] : {}); var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); - if (name[0] === '.') { - name = internals.getSourceFilePath() + '/' + name; + if (name[0] === '.') { + name = internals.getSourceFilePath() + '/' + name; } var plugin = null; - try { + try { var pkg = require(name + '/package.json'); var mod = require(name); - plugin = { + plugin = { name: pkg.name, version: pkg.version, - register: mod.register - }; + register: mod.register + }; } - catch (err) { - return callback(err); + catch (err) { + return callback(err); } - this.register(plugin, options, callback); + this.register(plugin, options, callback); }; -internals.Pack.prototype.requireDirectory = function (path /*, options, callback */) { +internals.Pack.prototype.requireDirectory = function (path /*, options, callback */) { - if (path[0] === '.') { - path = internals.getSourceFilePath() + '/' + path; + if (path[0] === '.') { + path = internals.getSourceFilePath() + '/' + path; } path = Path.resolve(path); @@ -305,69 +322,69 @@ internals.Pack.prototype.requireDirectory = function (path /*, options, callback exclude = Utils.mapToObject(exclude instanceof Array ? exclude : [exclude]); var names = []; - Fs.readdirSync(path).forEach(function (filename) { + Fs.readdirSync(path).forEach(function (filename) { if (filename.indexOf('.') !== -1 || - exclude[filename]) { + exclude[filename]) { - return; + return; } - names.push(path + '/' + filename); + names.push(path + '/' + filename); }); - this.requireList(names, options, callback); + this.requireList(names, options, callback); }; -internals.Pack.prototype.requireList = function (names/*, [options], callback*/) { +internals.Pack.prototype.requireList = function (names/*, [options], callback*/) { var self = this; var options = (arguments.length === 3 ? arguments[1] : {}); var callback = (arguments.length === 3 ? arguments[2] : arguments[1]); - Async.forEachSeries(names, function (name, next) { + Async.forEachSeries(names, function (name, next) { - self.require(name, options, next); + self.require(name, options, next); }, - function (err) { + function (err) { - return callback(err); - }); + return callback(err); + }); }; -internals.Pack.prototype.start = function (callback) { +internals.Pack.prototype.start = function (callback) { - this._apply(this.servers, Server.prototype.start, null, callback || function () { }); + this._apply(this.servers, Server.prototype.start, null, callback || function () { }); }; -internals.Pack.prototype.stop = function () { +internals.Pack.prototype.stop = function () { - this._applySync(this.servers, Server.prototype.stop); + this._applySync(this.servers, Server.prototype.stop); }; -internals.Pack.prototype._apply = function (servers, func, args, callback) { +internals.Pack.prototype._apply = function (servers, func, args, callback) { - Async.forEachSeries(servers, function (server, next) { + Async.forEachSeries(servers, function (server, next) { - func.apply(server, (args || []).concat([next])); + func.apply(server, (args || []).concat([next])); }, - function (err) { + function (err) { - return callback(err); - }); + return callback(err); + }); }; -internals.Pack.prototype._applySync = function (servers, func, args) { +internals.Pack.prototype._applySync = function (servers, func, args) { - for (var i = 0, il = servers.length; i < il; ++i) { - func.apply(servers[i], args); - } + for (var i = 0, il = servers.length; i < il; ++i) { + func.apply(servers[i], args); + } }; @@ -378,7 +395,7 @@ internals.getSourceFilePath = function () { // 2 - **Caller var stack = Utils.callStack(); - return stack[2][0].substring(0, stack[2][0].lastIndexOf('/')); + return stack[2][0].substring(0, stack[2][0].lastIndexOf('/')); }; From 2d6a94126a9526501a7a26c9eb318de2af3b3056 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 18 Feb 2013 10:45:46 -0800 Subject: [PATCH 2/4] Test --- lib/pack.js | 5 ++++- test/integration/pack.js | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/pack.js b/lib/pack.js index c126ef00f..bf7c5cbfa 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -287,12 +287,15 @@ internals.Pack.prototype.require = function (name/*, [options], callback*/) { if (name[0] === '.') { name = internals.getSourceFilePath() + '/' + name; } + else if (name[0] !== '/') { + name = require.main.paths[0] + '/' + name; + } var plugin = null; try { - var pkg = require(name + '/package.json'); var mod = require(name); + var pkg = require(name + '/package.json'); plugin = { name: pkg.name, diff --git a/test/integration/pack.js b/test/integration/pack.js index abba1da98..0841302f9 100755 --- a/test/integration/pack.js +++ b/test/integration/pack.js @@ -188,6 +188,20 @@ describe('Pack', function () { }); }); + it('fails to require missing module in default route', function (done) { + + var server1 = new Hapi.Server(); + var pack = new Hapi.Pack({ a: 1 }); + pack.server('s1', server1, { labels: ['a', 'b'] }); + + pack.require('none', function (err) { + + expect(err).to.exist; + expect(err.message).to.contain('Cannot find module'); + done(); + }); + }); + it('starts and stops', function (done) { var server1 = new Hapi.Server(0); From d1a713c8d9456ae1d652a96711490b9c59a76353 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 18 Feb 2013 19:12:41 -0800 Subject: [PATCH 3/4] Socket timeout support --- lib/defaults.js | 5 +- lib/schema.js | 1 + lib/server.js | 8 +++ test/integration/serverTimeout.js | 91 ++++++++++++++++++++++--------- 4 files changed, 76 insertions(+), 29 deletions(-) diff --git a/lib/defaults.js b/lib/defaults.js index 01a93921b..f6b238195 100755 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -53,8 +53,9 @@ exports.server = { // timeout limits timeout: { - client: 10000, // Determines how long to wait for a client connection to end before erroring out - server: null // server timeout disabled by default + socket: null, // Determines how long before closing request socket. Defaults to node (2 minutes) + client: 10 * 1000, // Determines how long to wait for receiving client payload. Defaults to 10 seconds + server: null // Determines how long to wait for server request processing. Disabled by default }, // Optional components diff --git a/lib/schema.js b/lib/schema.js index 24216cac3..d1112fef4 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -130,6 +130,7 @@ internals.serverSchema = { relativeTo: T.String() }).nullOk().allow(false).allow(true), timeout: T.Object({ + socket: T.Number().nullOk().allow(false).allow(true), client: T.Number().nullOk().allow(false).allow(true), server: T.Number().nullOk().allow(false).allow(true) }).nullOk().allow(false).allow(true), diff --git a/lib/server.js b/lib/server.js index 2491dff67..9a37c5394 100755 --- a/lib/server.js +++ b/lib/server.js @@ -73,6 +73,9 @@ module.exports = internals.Server = function (/* host, port, options */) { this.settings.uri = (this.settings.tls ? 'https://' : 'http://') + this.settings.host + ':' + this.settings.port; } + Utils.assert(this.settings.timeout.server === null || this.settings.timeout.socket === null || this.settings.timeout.server < this.settings.timeout.socket, 'Server timeout must be shorter than socket timeout'); + Utils.assert(this.settings.timeout.client === null || this.settings.timeout.socket === null || this.settings.timeout.client < this.settings.timeout.socket, 'Client timeout must be shorter than socket timeout'); + // Extensions this._ext = { @@ -210,6 +213,11 @@ internals.Server.prototype._dispatch = function (options) { // Create request object var request = new Request(self, req, res, options); + if (req.socket && + self.settings.timeout.socket !== null) { + + req.socket.setTimeout(self.settings.timeout.socket); + } // Execute onRequest extensions (can change request method and url) diff --git a/test/integration/serverTimeout.js b/test/integration/serverTimeout.js index 8b6289fa5..5c8a386be 100755 --- a/test/integration/serverTimeout.js +++ b/test/integration/serverTimeout.js @@ -3,6 +3,7 @@ var Chai = require('chai'); var Http = require('http'); var Stream = require('stream'); +var Request = require('request'); var Hapi = require('../helpers'); @@ -223,19 +224,19 @@ describe('Server Timeout', function () { }); }); -describe('Server and Client timeouts', function () { +describe('Server and Client timeouts', function () { - var timeoutHandler = function (request) { + var timeoutHandler = function (request) { }; - var cachedTimeoutHandler = function (request) { + var cachedTimeoutHandler = function (request) { var reply = request.reply; - setTimeout(function () { + setTimeout(function () { - reply.bind(request, new Hapi.Response.Text('Cached')); - }, 70); + reply.bind(request, new Hapi.Response.Text('Cached')); + }, 70); }; var _server = new Hapi.Server('127.0.0.1', 0, { timeout: { server: 50, client: 50 }, cache: { engine: 'memory' } }); @@ -244,64 +245,100 @@ describe('Server and Client timeouts', function () { { method: 'POST', path: '/timeoutcache', config: { handler: cachedTimeoutHandler } } ]); - before(function (done) { + before(function (done) { - _server.start(done); + _server.start(done); }); - it('are returned when both client and server timeouts are the same and the client times out', function (done) { + it('are returned when both client and server timeouts are the same and the client times out', function (done) { var timer = new Hapi.utils.Timer(); - var options = { + var options = { hostname: '127.0.0.1', port: _server.settings.port, path: '/timeout', - method: 'POST' + method: 'POST' }; - var req = Http.request(options, function (res) { + var req = Http.request(options, function (res) { expect([503, 408]).to.contain(res.statusCode); expect(timer.elapsed()).to.be.at.least(49); - done(); + done(); }); req.write('\n'); - setTimeout(function() { + setTimeout(function () { - req.end(); - }, 100); + req.end(); + }, 100); }); - it('initial long running requests don\'t prevent server timeouts from occuring on future requests', function (done) { + it('initial long running requests don\'t prevent server timeouts from occuring on future requests', function (done) { var timer = new Hapi.utils.Timer(); - var options = { + var options = { hostname: '127.0.0.1', port: _server.settings.port, path: '/timeoutcache', - method: 'POST' + method: 'POST' }; - var req1 = Http.request(options, function (res1) { + var req1 = Http.request(options, function (res1) { expect([503, 408]).to.contain(res1.statusCode); expect(timer.elapsed()).to.be.at.least(49); - var req2 = Http.request(options, function (res2) { + var req2 = Http.request(options, function (res2) { expect(res2.statusCode).to.equal(503); - done(); + done(); }); - req2.end(); + req2.end(); }); req1.write('\n'); - setTimeout(function() { + setTimeout(function () { + + req1.end(); + }, 100); + }); +}); + +describe('Socket timeout', function () { + + var server = new Hapi.Server('0.0.0.0', 0, { timeout: { client: 45, socket: 50 } }); + server.route({ + method: 'GET', path: '/', config: { + handler: function () { + + setTimeout(function (request) { + + request.reply('too late'); + }, 70); + } + } + }); - req1.end(); - }, 100); + var port = 0; + before(function (done) { + + server.start(function () { + + port = server.settings.port; + done(); + }); }); -}); \ No newline at end of file + + it('closes connection on socket timeout', function (done) { + + Request('http://localhost:' + port + '/', function (err, response, body) { + + expect(err).to.exist; + expect(err.message).to.equal('socket hang up'); + done(); + }); + }); +}); From 20ce4f586521908f9e724c7d2a3aa9cbe4a4617c Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Mon, 18 Feb 2013 19:51:28 -0800 Subject: [PATCH 4/4] fix timeout test --- test/integration/serverTimeout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/serverTimeout.js b/test/integration/serverTimeout.js index 5c8a386be..3e9766b3e 100755 --- a/test/integration/serverTimeout.js +++ b/test/integration/serverTimeout.js @@ -312,9 +312,9 @@ describe('Socket timeout', function () { var server = new Hapi.Server('0.0.0.0', 0, { timeout: { client: 45, socket: 50 } }); server.route({ method: 'GET', path: '/', config: { - handler: function () { + handler: function (request) { - setTimeout(function (request) { + setTimeout(function () { request.reply('too late'); }, 70);