Skip to content
This repository has been archived by the owner on Apr 21, 2020. It is now read-only.

Commit

Permalink
Merge pull request #3 from Cimpress-MCP/graceful_service_restart
Browse files Browse the repository at this point in the history
Graceful service restart
  • Loading branch information
ryanbreen committed Feb 4, 2015
2 parents 3631b24 + e07d8af commit 2c98a10
Show file tree
Hide file tree
Showing 15 changed files with 477 additions and 157 deletions.
97 changes: 2 additions & 95 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
var fs = require('fs');
var path = require('path');

var Proxy = require('./proxy');
var ProxyConfig = require('./proxy/config.js');
var proxy_manager = require('./proxy_manager.js');

var logger = require('./logger.js').getLogger();

Expand All @@ -14,98 +10,9 @@ var logger = require('./logger.js').getLogger();
* files and restarting proxies. TODO: Should there be?
*/
exports.init = function(config_dir, cb) {

if (!config_dir) return cb("Failed to open directory " + config_dir);

fs.stat(config_dir, function(err, stat) {
/* istanbul ignore next */
if (err) return cb("Failed to open directory " + config_dir);

if (!stat.isDirectory()) return cb('oauth_reverse_proxy config dir is not a directory');

// Load all proxy configurations.
loadConfigFiles(config_dir, cb);
});
proxy_manager.init(config_dir, cb);
};

/**
* Each proxy is defined by a JSON file that stores the configuration of the proxy.
*/
function loadConfigFiles(config_dir, cb) {

logger.info("Config dir is %s", config_dir);

// Stores all proxies created from configuration files in config_dir. If a proxy can not be loaded
// the config file name will map to the error message instead of a proxy object.
var proxies = {};

fs.readdir(config_dir, function(err, files) {
/* istanbul ignore if */
if (err) return cb(err);

// Fire a callback only once all config files have been processed.
var countdown = files.length;
var wrapped_cb = function() {
--countdown;
if (countdown <= 0) return cb(null, proxies);
};

files.forEach(function(file) {
logger.info('Loading proxy configuration file %s', file);
fs.readFile(config_dir + path.sep + file, {'encoding':'utf8'}, function(err, data) {
try {
// Parse the configuration into an object, create a ProxyConfig around it, and validate
// that the configuration meets our viability requirements.
var config = JSON.parse(data);
var proxy_config = new ProxyConfig(config);

// If the proxy configuration is incorrect, consider the proxy failed.
// CONTROVERSIAL STATEMENT ALERT: we do not consider a configuration error with a
// proxy to be a fatal error for oauth_reverse_proxy. As long as at least 1 configuration
// file is valid, we will proceed with proxy creation. This is to prevent a single
// busted configuration file from DOSing any other proxies by preventing their startup.
var proxy_error = proxy_config.isInvalid();
if (proxy_error) {
logger.error("Failed to load proxy %s due to %s", file, proxy_error);
proxies[file] = proxy_error;
return wrapped_cb();
}
} catch(e) {
logger.error("Failed to load proxy %s due to %s", file, e);
proxies[file] = e.message;
return wrapped_cb();
}

try {
// Create and start a proxy around a validated config.
var proxy = new Proxy(proxy_config);
proxy.start(function(err) {
/* istanbul ignore if */
if (err) {
// CONTROVERSIAL STATEMENT ALERT: we do not consider a startup error with a
// proxy to be a fatal error for oauth_reverse_proxy. As long as at least 1
// proxy starts properly, we will proceed with proxy creation. This is to prevent
// a single busted configuration file from DOSing any other proxies by preventing
// their startup.
proxies[file] = "Failed to start proxy " + proxy_config.service_name + " due to " + err;
return wrapped_cb();
}

logger.info("Started proxy %s", proxy_config.service_name);
proxies[file] = proxy;
wrapped_cb();
});
} catch(e) {
/* istanbul ignore next */
proxies[file] = "Uncaught exception starting proxy " + proxy_config.service_name + ": " + e + "\n" + e.stack;
/* istanbul ignore next */
wrapped_cb();
}
});
});
});
}

// Register the catch-all exception handler. We want to ignore this line for code coverage purposes,
// which the instanbul ignore line accomplishes.
process.on('uncaughtException', /* istanbul ignore next */ function(err) {
Expand Down
31 changes: 20 additions & 11 deletions lib/proxy/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
var fs = require('fs');
var util = require('util');

var _ = require('underscore');

var logger = require('../logger.js').getLogger();

/**
Expand All @@ -13,26 +15,26 @@ function ProxyConfig(config) {

// We want all of these parameters to be immutable. While writeable defaults to false, having it explicitly
// set here is good for readability.
Object.defineProperty(this_obj, 'service_name', { 'value': config.service_name, writable: false });
Object.defineProperty(this_obj, 'from_port', { 'value': config.from_port, writable: false });
Object.defineProperty(this_obj, 'to_port', { 'value': config.to_port, writable: false });
Object.defineProperty(this_obj, 'oauth_secret_dir', { 'value': config.oauth_secret_dir, writable: false });
Object.defineProperty(this_obj, 'service_name', { 'value': config.service_name, writable: false, enumerable: true });
Object.defineProperty(this_obj, 'from_port', { 'value': config.from_port, writable: false, enumerable: true });
Object.defineProperty(this_obj, 'to_port', { 'value': config.to_port, writable: false, enumerable: true });
Object.defineProperty(this_obj, 'oauth_secret_dir', { 'value': config.oauth_secret_dir, writable: false, enumerable: true });

// An optional list of allowed Host header or URI path parameters can be specified as environment
// variables. Each of these is a substring match with no wildcards. A string that matches the
// substring is allowed. All others are rejected. Multiple of either setting can be provided.
Object.defineProperty(this_obj, 'required_uris', { 'value': config.required_uris, writable: false });
Object.defineProperty(this_obj, 'required_hosts', { 'value': config.required_hosts, writable: false });
Object.defineProperty(this_obj, 'required_uris', { 'value': config.required_uris, writable: false, enumerable: true });
Object.defineProperty(this_obj, 'required_hosts', { 'value': config.required_hosts, writable: false, enumerable: true });

// Whether this proxy listens on an HTTPS socket on from_port. Defaults to false.
Object.defineProperty(this_obj, 'https', { 'value': config.https != undefined || false, writable: false });
Object.defineProperty(this_obj, 'https', { 'value': config.https != undefined || false, writable: false, enumerable: true });
if (this_obj.https) {
Object.defineProperty(this_obj, 'https_key_file', { 'value': config.https.key, writable: false });
Object.defineProperty(this_obj, 'https_cert_file', { 'value': config.https.cert, writable: false });
Object.defineProperty(this_obj, 'https_key_file', { 'value': config.https.key, writable: false, enumerable: true });
Object.defineProperty(this_obj, 'https_cert_file', { 'value': config.https.cert, writable: false, enumerable: true });
}

// An optional object defining quotas to apply to inbound requests.
Object.defineProperty(this_obj, 'quotas', { 'value': (config.quotas || {thresholds:{}}), writable: false});
Object.defineProperty(this_obj, 'quotas', { 'value': (config.quotas || {thresholds:{}}), writable: false, enumerable: true});

// Use a default whitelist config if none is provided.
var whitelist = config.whitelist ||
Expand All @@ -45,9 +47,16 @@ function ProxyConfig(config) {
methods: [ "GET" ]
}];

Object.defineProperty(this_obj, 'whitelist', { 'value': whitelist, writable: false });
Object.defineProperty(this_obj, 'whitelist', { 'value': whitelist, writable: false, enumerable: true });
}

/**
* Performs a deep comparison of proxy configs and returns true iff configurations are identical.
*/
ProxyConfig.prototype.equals = function(other_proxy_config) {
return _.isEqual(this, other_proxy_config);
};

/**
* Returns a string matching the first validation that failed when evaluating this proxy config or returns
* undefined if the configuration is valid.
Expand Down
27 changes: 21 additions & 6 deletions lib/proxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,27 +145,42 @@ Proxy.prototype.start = function(cb) {

// If the proxy config specifically asks for https, use https. Otherwise, use http.
if (this_obj.config.https) {
var ipv4_server = https.createServer({
this_obj.ipv4_server = https.createServer({
key: fs.readFileSync(this_obj.config.https_key_file),
cert: fs.readFileSync(this_obj.config.https_cert_file)
}, app);
var ipv6_server = https.createServer({
this_obj.ipv6_server = https.createServer({
key: fs.readFileSync(this_obj.config.https_key_file),
cert: fs.readFileSync(this_obj.config.https_cert_file)
}, app);
} else {
var ipv4_server = http.createServer(app);
var ipv6_server = http.createServer(app);
this_obj.ipv4_server = http.createServer(app);
this_obj.ipv6_server = http.createServer(app);
}

// Listen on our 2 servers. If we attempt to listen on a single server, the results will be non-deterministic.
// It works on node 0.10.30 but not on 0.10.35, for example. Separating the two servers appears to work everywhere.
ipv4_server.listen(this_obj.config.from_port, '0.0.0.0');
ipv6_server.listen(this_obj.config.from_port, '::');
this_obj.ipv4_server.listen(this_obj.config.from_port, '0.0.0.0');
this_obj.ipv6_server.listen(this_obj.config.from_port, '::');

cb(null, this_obj);
});
};

/**
* Stop this proxy, shutting down its servers. Note that the servers will continue to hold existing connections until
* they complete: we make no effort to forcibly terminate connections.
*/
Proxy.prototype.stop = function() {

if (this.ipv4_server) {
this.ipv4_server.close();
}

if (this.ipv6_server) {
this.ipv6_server.close();
}
};

// Expose Proxy class.
module.exports = Proxy;
2 changes: 1 addition & 1 deletion lib/proxy/keystore.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ ProxyKeystore.prototype.setupWatcher = function() {
keystore_reload_pending = true;
logger.debug("Entering quiet period for file updates in %s", this_obj.oauth_secret_dir);
setTimeout(function() {
// Once the settimeout has fired, allow keystore reloads to be queued again.
// Once the setTimeout has fired, allow keystore reloads to be queued again.
keystore_reload_pending = false;
// We don't care what type of event happened in the key directory. We do a full
// reload of the keystore regardless.
Expand Down
Loading

0 comments on commit 2c98a10

Please sign in to comment.