1
0
Fork 0
liza/src/server/daemon/Daemon.js

607 lines
15 KiB
JavaScript

/**
* Daemon class
*
* Copyright (C) 2010-2019 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework.
*
* liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var AbstractClass = require( 'easejs' ).AbstractClass,
liza = require( '../..' ),
sys = require( 'util' ),
sprintf = require( 'php' ).sprintf;
/**
* Facade handling core logic for the daemon
*
* TODO: Factor out unrelated logic
*/
module.exports = AbstractClass( 'Daemon',
{
/**
* System configuration
* @type {Store}
*/
'private _conf': null,
/**
* Server to accept HTTP requests
* @type {HttpServer}
*/
'private _httpServer': null,
/**
* Path to access log
* @var {string}
*/
'private _accessLogPath': '',
/**
* Path to debug log
* @var {string}
*/
'private _debugLogPath': '',
/**
* Access logger
* @type {AccessLog}
*/
'private _accessLog': null,
/**
* Debug logger
* @type {DebugLog}
*/
'private _debugLog': null,
/**
* Encryption service
* @type {EncryptionService}
*/
'private _encService': null,
/**
* Memcache client
* @type {Object}
*/
'private _memcache': null,
/**
* Routers to use to handle user requests, ordered from most likely to be
* used to least for performance reasons
*
* @type {Array.<Object>}
*/
'private _routers': null,
/**
* Rating service
* @type {Object}
*/
'private _rater': null,
'public __construct': function( conf )
{
this._conf = conf;
},
/**
* Starts initializing the daemon
*
* @return {undefined}
*/
'public start'()
{
return Promise.all( [
this._createDebugLog(),
this._createAccessLog(),
this._conf.get( 'skey' ),
this._conf.get( 'services.rating.postRatePublish' ),
] ).then( ([ debug_log, access_log, skey, post_rate ]) =>
{
this._debugLog = debug_log;
this._accessLog = access_log;
this._httpServer = this.getHttpServer();
this._rater = liza.server.rater.ProcessManager();
this._encService = this.getEncryptionService();
this._memcache = this.getMemcacheClient();
return post_rate.reduce(
( accum, value, key ) =>
{
accum[ key ] = value;
return accum;
},
{}
).then( post_rate_publish =>
this._routers = this.getRouters(
skey,
post_rate_publish
)
);
} )
.then( () => this._startDaemon() );
},
'private _startDaemon'()
{
this._debugLog.log( this._debugLog.PRIORITY_IMPORTANT,
"Access log path: %s", this._accessLogPath
);
this._debugLog.log( this._debugLog.PRIORITY_IMPORTANT,
"Debug log path: %s", this._debugLogPath
);
this._initSignalHandlers();
this._testEncryptionService( () =>
{
this._memcacheConnect();
this._initMemoryLogger();
this._initRouters();
this._initHttpServer( () =>
{
this._initUncaughtExceptionHandler();
// ready to roll
this._debugLog.log( this._debugLog.PRIORITY_INFO,
"Daemon initialization complete."
);
} );
} );
},
'protected getDebugLog': function()
{
return this._debugLog;
},
'protected getHttpServer': function()
{
return require( './http_server' );
},
'protected getAccessLog': function()
{
return liza.server.log.AccessLog;
},
'protected getPriorityLog': function()
{
return liza.server.log.PriorityLog;
},
/**
* Get (and initialize) controller
*
* The controller will only be initialized with the session key SKEY and
* post-rate AMQP configuration if they are provided, respectively.
*
* @param {string=} skey session key
* @param {Object=} post_rate_publish configuration for post-rate messages
*
* @return {Object} controller
*/
'protected getProgramController': function( skey, post_rate_publish )
{
var controller = require( './controller' );
controller.rater = this._rater;
controller.post_rate_publish =
post_rate_publish || controller.post_rate_publish;
if ( skey )
{
controller.skey = skey;
}
return controller;
},
'protected getScriptsController': function()
{
return require( './scripts' );
},
'protected getClientErrorController': function()
{
return require( './clienterr' );
},
'protected getUserRequest': function()
{
return liza.server.request.UserRequest;
},
'protected getUserSession': function()
{
return liza.server.request.UserSession;
},
'protected getMemcacheClient': function()
{
var MemcacheClient = require( 'memcache/lib/memcache' ).Client,
ResilientMemcache = liza.server.cache.ResilientMemcache,
memc = ResilientMemcache(
new MemcacheClient(
process.env.MEMCACHE_PORT || 11211,
process.env.MEMCACHE_HOST || 'localhost'
)
);
var _self = this;
memc
.on( 'preConnect', function()
{
_self._debugLog.log( _self._debugLog.PRIORITY_IMPORTANT,
'Connecting to memcache server...'
);
} )
.on( 'connect', function()
{
_self._debugLog.log( _self._debugLog.PRIORITY_IMPORTANT,
'Connected to memcache server.'
);
} )
.on( 'connectError', function( e )
{
_self._debugLog.log( _self._debugLog.PRIORITY_ERROR,
'Failed to connect to memcached: %s',
e.message
);
} )
.on( 'queuePurged', function( n )
{
_self._debugLog.log( _self._debugLog.PRIORITY_ERROR,
'Memcache request queue (size %d) purged!',
n
);
} )
.on( 'error', function( e )
{
_self._debugLog.log( _self._debugLog.PRIORITY_ERROR,
'Memcache error: %s',
e.message
);
} );
return memc;
},
'abstract protected getEncryptionService': [],
'protected getRouters': function( skey, post_rate_publish )
{
return [
this.getProgramController(
skey, post_rate_publish
),
this.getScriptsController(),
this.getClientErrorController(),
];
},
/**
* Perform a graceful shutdown
*
* @param {string} signal the signal that caused the shutdown
*
* @return {undefined}
*/
'protected shutdown': function( signal )
{
this._debugLog.log( this._debugLog.PRIORITY_IMPORTANT,
"Received %s. Beginning graceful shutdown...",
signal
);
this._debugLog.log( this._debugLog.PRIOIRTY_IMPORTANT,
"Closing HTTP server..."
);
this._httpServer.close();
this._debugLog.log( this._debugLog.PRIORITY_IMPORTANT,
"Shutdown complete. Exiting..."
);
process.exit();
},
'private _createAccessLog': function()
{
return this._conf.get( 'log.access.path' )
.then( log_path =>
{
this._accessLogPath = log_path;
return this.getAccessLog()( this._accessLogPath );
} );
},
'private _createDebugLog': function()
{
return Promise.all( [
this._conf.get( 'log.priority' ),
this._conf.get( 'log.debug.path' ),
] )
.then( ([ priority, debug_log_path ]) =>
{
this._debugLogPath = debug_log_path;
return this.getPriorityLog()(
debug_log_path,
priority
)
} );
},
/**
* Catches and logs uncaught exceptions to prevent early termination
*
* @return {undefined}
*/
'private _initUncaughtExceptionHandler': function()
{
var _self = this;
// chances are, we don't want to crash; we are, after all, a webserver
process.on( 'uncaughtException', function( err )
{
_self._debugLog.log( _self._debugLog.PRIORITY_ERROR,
"Uncaught exception: %s\n%s",
err,
( err.stack ) ? err.stack : '(No stack trace)'
);
// should we terminate on uncaught exceptions?
if ( process.env.NODEJS_UCE_TERM )
{
_self._debugLog.log( _self._debugLog.PRIORITY_ERROR,
"NODEJS_UCE_TERM set; terminating..."
);
// SIGINT
process.kill( process.pid );
}
});
// notify the user of the UCE_TERM flag is set
if ( process.env.NODEJS_UCE_TERM )
{
this._debugLog.log( 1,
"NODEJS_UCE_TERM set; " +
"will terminate on uncaught exceptions."
);
}
},
'private _initSignalHandlers': function()
{
var _self = this;
// graceful shutdown on SIGINT and SIGTERM (cannot catch SIGKILL)
try
{
process
.on( 'SIGHUP', function()
{
_self._debugLog.log( _self._debugLog.PRIORITY_IMPORTANT,
"SIGHUP received; requesting reload"
);
_self.getProgramController().reload();
} )
.on( 'SIGINT', function()
{
_self.shutdown( 'SIGINT' );
} )
.on( 'SIGTERM', function()
{
_self.shutdown( 'SIGTERM' );
} );
}
catch ( e )
{
console.log( "note: signal handling unsupported on this OS" );
}
},
'private _testEncryptionService': function( callback )
{
var enc_test = 'test string',
_self = this;
this._debugLog.log( this._debugLog.PRIORITY_INFO,
"Performing encryption service sanity check..."
);
// encryption sanity check to ensure we won't end up working with data
// that will only become corrupt
this._encService.encrypt( enc_test, function( data )
{
_self._encService.decrypt( data, function( data )
{
if ( enc_test !== data.toString( 'ascii' ) )
{
_self._debugLog.log( _self._debugLog.PRIORITY_ERROR,
"Encryption service is incompetant. Aborting."
);
process.exit( 1 );
}
_self._debugLog.log( _self._debugLog.PRIORITY_INFO,
"Encryption service sanity check passed."
);
callback();
} );
} );
},
/**
* Attempts to make connection to memcache server
*
* @param memcache.Client memcache client to connect to server
*
* @return undefined
*/
'private _memcacheConnect': function()
{
try
{
this._memcache.connect();
}
catch( err )
{
this._debugLog.log( this._debugLog.PRIORITY_ERROR,
"Failed to connected to memcached server: %s",
err
);
}
},
'private _initMemoryLogger': function()
{
var _self = this;
// log memory usage (every 15 min)
setInterval( function()
{
_self._debugLog.log( _self._debugLog.PRIORITY_IMPORTANT,
'Memory usage: %s MB (rss), %s/%s MB (V8 heap)',
( process.memoryUsage().rss / 1024 / 1024 ).toFixed( 2 ),
( process.memoryUsage().heapUsed / 1024 / 1024 ).toFixed( 2 ),
( process.memoryUsage().heapTotal / 1024 / 1024 ).toFixed( 2 )
);
}, 900000 );
},
'private _initRouters': function()
{
var _self = this;
// initialize each router
this._routers.forEach( function( router )
{
if ( router.init instanceof Function )
{
router.init(
_self._debugLog,
_self._encService,
_self._conf,
process.env.NODE_ENV
);
}
});
},
'private _initHttpServer': function( callback )
{
var _self = this;
/**
* Builds UserRequest from the provided request and response objects
*
* @param {HttpServerRequest} request
* @param {HttpServerResponse} response
*
* @return {UserRequest} instance to represent current request
*/
function request_builder( request, response )
{
return _self.getUserRequest()(
request,
response,
function( sess_id )
{
// build a new user session from the given session id
return _self.getUserSession()( sess_id, _self._memcache );
}
);
}
// create the HTTP server and listen for connections
try
{
this._httpServer = this.getHttpServer().create(
this._routers,
request_builder,
this._accessLog,
this._debugLog
);
this._conf.get( 'http.port' )
.then( port => this._httpServer.listen( port, () =>
{
this._debugLog.log(
1, "Server running on port %d", port
);
callback();
} ) )
.catch( e => this._httpError( e ) );
}
catch( e )
{
this._httpError( e );
}
},
'private _httpError'( e )
{
this._debugLog.log( this._debugLog.PRIORITY_ERROR,
"Unable to start HTTP server: %s",
err
);
// TODO: use daemon-level promise and reject it
process.exit( 1 );
},
} );