pomelo-upgrade
Version:
1,187 lines (1,098 loc) • 28.5 kB
JavaScript
'use strict';
/**
* Module dependencies.
*/
const utils = require('./util/utils');
const logger = require('pomelo-logger-upgrade').getLogger('pomelo', __filename);
const EventEmitter = require('events').EventEmitter;
const events = require('./util/events');
const appUtil = require('./util/appUtil');
const Constants = require('./util/constants');
const appManager = require('./common/manager/appManager');
const fs = require('fs');
const path = require('path');
/**
* Application prototype.
*
* @module
*/
const Application = module.exports = {};
/**
* Application states
*/
const STATE_INITED = 1; // app has inited
const STATE_START = 2; // app start
const STATE_STARTED = 3; // app has started
const STATE_STOPED = 4; // app has stoped
/**
* Initialize the server.
*
* - setup default configuration
*/
Application.init = function(opts)
{
opts = opts || {};
this.loaded = []; // loaded component list
this.components = {}; // name -> component map
this.settings = {}; // collection keep set/get
const base = opts.base || path.dirname(require.main.filename);
this.set(Constants.RESERVED.BASE, base, true);
this.event = new EventEmitter(); // event object to sub/pub events
// current server info
this.serverId = null; // current server id
this.serverType = null; // current server type
this.curServer = null; // current server info
this.startTime = null; // current server start time
// global server infos
this.master = null; // master server info
this.servers = {}; // current global server info maps, id -> info
this.serverTypeMaps = {}; // current global type maps, type -> [info]
this.serverTypes = []; // current global server type list
this.lifecycleCbs = {}; // current server custom lifecycle callbacks
this.clusterSeq = {}; // cluster id seqence
appUtil.defaultConfiguration(this);
this.state = STATE_INITED;
logger.info('application inited: %j', this.getServerId());
};
/**
* Get application base path
*
* // cwd: /home/game/
* pomelo start
* // app.getBase() -> /home/game
*
* @return {string} application base path
*
* @memberOf Application
*/
Application.getBase = function()
{
return this.get(Constants.RESERVED.BASE);
};
/**
* Override require method in application
*
* @param {string} relative path of file
*
* @memberOf Application
*/
Application.require = function(ph)
{
return require(path.join(Application.getBase(), ph));
};
/**
* Configure logger with {$base}/config/log4js.json
*
* @param {object} logger pomelo-logger instance without configuration
*
* @memberOf Application
*/
Application.configureLogger = function(logger)
{
if (process.env.POMELO_LOGGER !== 'off')
{
const base = this.getBase();
const env = this.get(Constants.RESERVED.ENV);
const originPath = path.join(base, Constants.FILEPATH.LOG);
const presentPath = path.join(base, Constants.FILEPATH.CONFIG_DIR, env, path.basename(Constants.FILEPATH.LOG));
if (fs.existsSync(originPath))
{
logger.configure(originPath, {
serverId : this.serverId,
base : base
});
}
else if (fs.existsSync(presentPath))
{
logger.configure(presentPath, {
serverId : this.serverId,
base : base
});
}
else
{
logger.error('logger file path configuration is error.');
}
}
};
/**
* add a filter to before and after filter
*
* @param {object} filter provide before and after filter method.
* A filter should have two methods: before and after.
* @memberOf Application
*/
Application.filter = function(filter)
{
this.before(filter);
this.after(filter);
};
/**
* Add before filter.
*
* @param {Object|Function} bf before fileter, bf(msg, session, next)
* @memberOf Application
*/
Application.before = function(bf)
{
addFilter(this, Constants.KEYWORDS.BEFORE_FILTER, bf);
};
/**
* Add after filter.
*
* @param {Object|Function} af after filter, `af(err, msg, session, resp, next)`
* @memberOf Application
*/
Application.after = function(af)
{
addFilter(this, Constants.KEYWORDS.AFTER_FILTER, af);
};
/**
* add a global filter to before and after global filter
*
* @param {object} filter provide before and after filter method.
* A filter should have two methods: before and after.
* @memberOf Application
*/
Application.globalFilter = function(filter)
{
this.globalBefore(filter);
this.globalAfter(filter);
};
/**
* Add global before filter.
*
* @param {Object|Function} bf before fileter, bf(msg, session, next)
* @memberOf Application
*/
Application.globalBefore = function(bf)
{
addFilter(this, Constants.KEYWORDS.GLOBAL_BEFORE_FILTER, bf);
};
/**
* Add global after filter.
*
* @param {Object|Function} af after filter, `af(err, msg, session, resp, next)`
* @memberOf Application
*/
Application.globalAfter = function(af)
{
addFilter(this, Constants.KEYWORDS.GLOBAL_AFTER_FILTER, af);
};
/**
* Add rpc before filter.
*
* @param {Object|Function} bf before fileter, bf(serverId, msg, opts, next)
* @memberOf Application
*/
Application.rpcBefore = function(bf)
{
addFilter(this, Constants.KEYWORDS.RPC_BEFORE_FILTER, bf);
};
/**
* Add rpc after filter.
*
* @param {Object|Function} af after filter, `af(serverId, msg, opts, next)`
* @memberOf Application
*/
Application.rpcAfter = function(af)
{
addFilter(this, Constants.KEYWORDS.RPC_AFTER_FILTER, af);
};
/**
* add a rpc filter to before and after rpc filter
*
* @param {object} filter provide before and after filter method.
* A filter should have two methods: before and after.
* @memberOf Application
*/
Application.rpcFilter = function(filter)
{
this.rpcBefore(filter);
this.rpcAfter(filter);
};
/**
* Load component
*
* @param {string} name (optional) name of the component
* @param {object} component component instance or factory function of the component
* @param {[type]} opts (optional) construct parameters for the factory function
* @return {object} app instance for chain invoke
* @memberOf Application
*/
Application.load = function(name, component, opts)
{
if (typeof name !== 'string')
{
opts = component;
component = name;
name = null;
}
if (typeof component === 'function')
{
component = component(this, opts);
}
if (!name && typeof component.name === 'string')
{
name = component.name;
}
if (name && this.components[name])
{
// ignore duplicat component
logger.warn('ignore duplicate component: %j', name);
return;
}
this.loaded.push(component);
if (name)
{
// components with a name would get by name throught app.components later.
this.components[name] = component;
}
return this;
};
/**
* Load Configure json file to settings.(support different enviroment directory & compatible for old path)
*
* @param {string} key environment key
* @param {string} val environment value
* @param {boolean} reload whether reload after change default false
* @return {Server|Mixed} for chaining, or the setting value
* @memberOf Application
*/
Application.loadConfigBaseApp = function(key, val, reload)
{
const env = this.get(Constants.RESERVED.ENV);
const originPath = path.join(Application.getBase(), val);
const presentPath = path.join(Application.getBase(), Constants.FILEPATH.CONFIG_DIR, env, path.basename(val));
let realPath;
if (fs.existsSync(originPath))
{
realPath = originPath;
let file = require(originPath);
if (file[env])
{
file = file[env];
}
this.set(key, file);
}
else if (fs.existsSync(presentPath))
{
realPath = presentPath;
const pfile = require(presentPath);
this.set(key, pfile);
}
else
{
logger.error('invalid configuration with file path: %s', key);
}
if (Boolean(realPath) && Boolean(reload))
{
fs.watch(realPath, (event) =>
{
if (event === 'change')
{
delete require.cache[require.resolve(realPath)];
this.loadConfigBaseApp(key, val, false);
}
});
}
};
/**
* Load Configure json file to settings.
*
* @param {string} key environment key
* @param {string} val environment value
* @return {Server|Mixed} for chaining, or the setting value
* @memberOf Application
*/
Application.loadConfig = function(key, val)
{
const env = this.get(Constants.RESERVED.ENV);
val = require(val);
if (val[env])
{
val = val[env];
}
this.set(key, val);
};
/**
* Set the route function for the specified server type.
*
* Examples:
*
* app.route('area', routeFunc);
*
* var routeFunc = function(session, msg, app, cb) {
* // all request to area would be route to the first area server
* var areas = app.getServersByType('area');
* cb(null, areas[0].id);
* };
*
* @param {string} serverType server type string
* @param {function} routeFunc route function. routeFunc(session, msg, app, cb)
* @return {object} current application instance for chain invoking
* @memberOf Application
*/
Application.route = function(serverType, routeFunc)
{
let routes = this.get(Constants.KEYWORDS.ROUTE);
if (!routes)
{
routes = {};
this.set(Constants.KEYWORDS.ROUTE, routes);
}
routes[serverType] = routeFunc;
return this;
};
/**
* Set before stop function. It would perform before servers stop.
*
* @param {function} fun before close function
* @return {void}
* @memberOf Application
*/
Application.beforeStopHook = function(fun)
{
logger.warn('this method was deprecated in pomelo 0.8');
if (Boolean(fun) && typeof fun === 'function')
{
this.set(Constants.KEYWORDS.BEFORE_STOP_HOOK, fun);
}
};
/**
* Start application. It would load the default components and start all the loaded components.
*
* @param {function} cb callback function
* @memberOf Application
*/
Application.start = function(cb)
{
// 开始启动服务器
this.startTime = Date.now();
if (this.state > STATE_INITED)
{
utils.invokeCallback(cb, new Error('application has already start.'));
return;
}
appUtil.startByType(this, () =>
{
appUtil.loadDefaultComponents(this);
const startUp = () =>
{
appUtil.optComponents(this.loaded, Constants.RESERVED.START, (err) =>
{
this.state = STATE_START;
if (err)
{
utils.invokeCallback(cb, err);
}
else
{
// 某一个服务器启动完成
logger.info(`${this.getServerType()} ${this.getServerId()} 开始调用启动后的函数...`, );
this.afterStart(cb);
}
});
};
const beforeFun = this.lifecycleCbs[Constants.LIFECYCLE.BEFORE_STARTUP];
if (beforeFun)
{
beforeFun.call(null, this, startUp);
}
else
{
startUp();
}
});
};
/**
* Lifecycle callback for after start.
*
* @param {function} cb callback function
* @return {void}
*/
Application.afterStart = function(cb)
{
if (this.state !== STATE_START)
{
utils.invokeCallback(cb, new Error('application is not running now.'));
return;
}
const afterFun = this.lifecycleCbs[Constants.LIFECYCLE.AFTER_STARTUP];
appUtil.optComponents(this.loaded, Constants.RESERVED.AFTER_START, (err) =>
{
this.state = STATE_STARTED;
const id = this.getServerId();
const serverType = this.getServerType();
if (!err)
{
logger.info('%j finish start', id);
}
if (afterFun)
{
afterFun.call(null, this, function()
{
utils.invokeCallback(cb, err);
});
}
else
{
utils.invokeCallback(cb, err);
}
const usedTime = Date.now() - this.startTime;
logger.info(`${serverType} ${id} startup in ${usedTime} ms`);
this.event.emit(events.START_SERVER, id);
});
};
/**
* Stop components.
*
* @param {boolean} force whether stop the app immediately
*/
Application.stop = function(force)
{
if (this.state > STATE_STARTED)
{
logger.warn('[pomelo application] application is not running now.');
return;
}
this.state = STATE_STOPED;
this.stopTimer = setTimeout(function()
{
process.exit(0);
}, Constants.TIME.TIME_WAIT_STOP);
const cancelShutDownTimer = () =>
{
if (this.stopTimer)
{
clearTimeout(this.stopTimer);
this.stopTimer = null;
}
};
const shutDown = () =>
{
appUtil.stopComps(this.loaded, 0, force, function()
{
cancelShutDownTimer();
if (force)
{
process.exit(0);
}
});
};
const fun = this.get(Constants.KEYWORDS.BEFORE_STOP_HOOK);
const stopFun = this.lifecycleCbs[Constants.LIFECYCLE.BEFORE_SHUTDOWN];
if (stopFun)
{
stopFun.call(null, this, shutDown, cancelShutDownTimer);
}
else if (fun)
{
utils.invokeCallback(fun, self, shutDown, cancelShutDownTimer);
}
else
{
shutDown();
}
};
/**
* Assign `setting` to `val`, or return `setting`'s value.
*
* Example:
*
* app.set('key1', 'value1');
* app.get('key1'); // 'value1'
* app.key1; // undefined
*
* app.set('key2', 'value2', true);
* app.get('key2'); // 'value2'
* app.key2; // 'value2'
*
* @param {string} setting the setting of application
* @param {string} val the setting's value
* @param {boolean} attach whether attach the settings to application
* @return {Server|Mixed} for chaining, or the setting value
* @memberOf Application
*/
Application.set = function(setting, val, attach)
{
if (arguments.length === 1)
{
return this.settings[setting];
}
this.settings[setting] = val;
if (attach)
{
this[setting] = val;
}
return this;
};
/**
* Get property from setting
*
* @param {string} setting application setting
* @return {string} val
* @memberOf Application
*/
Application.get = function(setting)
{
return this.settings[setting];
};
/**
* Check if `setting` is enabled.
*
* @param {string} setting application setting
* @return {boolean}
* @memberOf Application
*/
Application.enabled = function(setting)
{
return Boolean(this.get(setting));
};
/**
* Check if `setting` is disabled.
*
* @param {string} setting application setting
* @return {boolean}
* @memberOf Application
*/
Application.disabled = function(setting)
{
return !this.get(setting);
};
/**
* Enable `setting`.
*
* @param {string} setting application setting
* @return {app} for chaining
* @memberOf Application
*/
Application.enable = function(setting)
{
return this.set(setting, true);
};
/**
* Disable `setting`.
*
* @param {string} setting application setting
* @return {app} for chaining
* @memberOf Application
*/
Application.disable = function(setting)
{
return this.set(setting, false);
};
/**
* Configure callback for the specified env and server type.
* When no env is specified that callback will
* be invoked for all environments and when no type is specified
* that callback will be invoked for all server types.
*
* Examples:
*
* app.configure(function(){
* // executed for all envs and server types
* });
*
* app.configure('development', function(){
* // executed development env
* });
*
* app.configure('development', 'connector', function(){
* // executed for development env and connector server type
* });
*
* @param {string} env application environment
* @param {function} fn callback function
* @param {string} type server type
* @return {Application} for chaining
* @memberOf Application
*/
Application.configure = function(env, type, fn)
{
const args = [].slice.call(arguments);
fn = args.pop();
env = type = Constants.RESERVED.ALL;
if (args.length > 0)
{
env = args[0];
}
if (args.length > 1)
{
type = args[1];
}
if (env === Constants.RESERVED.ALL || contains(this.settings.env, env))
{
if (type === Constants.RESERVED.ALL || contains(this.settings.serverType, type))
{
fn.call(this);
}
}
return this;
};
/**
* Register admin modules. Admin modules is the extends point of the monitor system.
*
* @param {string} module (optional) module id or provoided by module.moduleId
* @param {object} module module object or factory function for module
* @param {object} opts construct parameter for module
* @memberOf Application
*/
Application.registerAdmin = function(moduleId, module, opts)
{
let modules = this.get(Constants.KEYWORDS.MODULE);
if (!modules)
{
modules = {};
this.set(Constants.KEYWORDS.MODULE, modules);
}
if (typeof moduleId !== 'string')
{
opts = module;
module = moduleId;
if (module)
{
moduleId = module.moduleId;
}
}
if (!moduleId)
{
return;
}
modules[moduleId] = {
moduleId : moduleId,
module : module,
opts : opts
};
};
/**
* Use plugin.
*
* @param {object} plugin plugin instance
* @param {[type]} opts (optional) construct parameters for the factory function
* @memberOf Application
*/
Application.use = function(plugin, opts)
{
if (!plugin.components)
{
logger.error('invalid components, no components exist');
return;
}
const self = this;
opts = opts || {};
const dir = path.dirname(plugin.components);
if (!fs.existsSync(plugin.components))
{
logger.error('fail to find components, find path: %s', plugin.components);
return;
}
fs.readdirSync(plugin.components).forEach(function(filename)
{
if (!/\.js$/.test(filename))
{
return;
}
const name = path.basename(filename, '.js');
const param = opts[name] || {};
const absolutePath = path.join(dir, Constants.DIR.COMPONENT, filename);
if (!fs.existsSync(absolutePath))
{
logger.error('component %s not exist at %s', name, absolutePath);
}
else
{
self.load(require(absolutePath), param);
}
});
// load events
if (!plugin.events)
{
return;
}
if (!fs.existsSync(plugin.events))
{
logger.error('fail to find events, find path: %s', plugin.events);
return;
}
fs.readdirSync(plugin.events).forEach(function(filename)
{
if (!/\.js$/.test(filename))
{
return;
}
const absolutePath = path.join(dir, Constants.DIR.EVENT, filename);
if (!fs.existsSync(absolutePath))
{
logger.error('events %s not exist at %s', filename, absolutePath);
}
else
{
bindEvents(require(absolutePath), self);
}
});
};
/**
* Application transaction. Transaction includes conditions and handlers, if conditions are satisfied, handlers would be executed.
* And you can set retry times to execute handlers. The transaction log is in file logs/transaction.log.
* 事物处理
*
* @param {string} name transaction name
* @param {object} conditions functions which are called before transaction
* @param {object} handlers functions which are called during transaction
* @param {number} retry retry times to execute handlers if conditions are successfully executed
* @memberOf Application
*/
Application.transaction = function(name, conditions, handlers, retry)
{
appManager.transaction(name, conditions, handlers, retry);
};
/**
* Get master server info.
*
* @return {object} master server info, {id, host, port}
* @memberOf Application
*/
Application.getMaster = function()
{
return this.master;
};
/**
* Get current server info.
*
* @return {object} current server info, {id, serverType, host, port}
* @memberOf Application
*/
Application.getCurServer = function()
{
return this.curServer;
};
/**
* Get current server id.
*
* @return {String|Number} current server id from servers.json
* @memberOf Application
*/
Application.getServerId = function()
{
return this.serverId;
};
/**
* Get current server type.
*
* @return {String|Number} current server type from servers.json
* @memberOf Application
*/
Application.getServerType = function()
{
return this.serverType;
};
/**
* Get all the current server infos.
*
* @return {object} server info map, key: server id, value: server info
* @memberOf Application
*/
Application.getServers = function()
{
return this.servers;
};
/**
* Get all server infos from servers.json.
*
* @return {object} server info map, key: server id, value: server info
* @memberOf Application
*/
Application.getServersFromConfig = function()
{
return this.get(Constants.KEYWORDS.SERVER_MAP);
};
/**
* Get all the server type.
*
* @return {Array} server type list
* @memberOf Application
*/
Application.getServerTypes = function()
{
return this.serverTypes;
};
/**
* Get server info by server id from current server cluster.
*
* @param {string} serverId server id
* @return {Object} server info or undefined
* @memberOf Application
*/
Application.getServerById = function(serverId)
{
return this.servers[serverId];
};
/**
* Get server info by server id from servers.json.
*
* @param {string} serverId server id
* @return {Object} server info or undefined
* @memberOf Application
*/
Application.getServerFromConfig = function(serverId)
{
return this.get(Constants.KEYWORDS.SERVER_MAP)[serverId];
};
/**
* Get server infos by server type.
*
* @param {string} serverType server type
* @return {Array} server info list
* @memberOf Application
*/
Application.getServersByType = function(serverType)
{
return this.serverTypeMaps[serverType];
};
/**
* Check the server whether is a frontend server
*
* @param {server} server server info. it would check current server
* if server not specified
* @return {boolean}
*
* @memberOf Application
*/
Application.isFrontend = function(server)
{
server = server || this.getCurServer();
return Boolean(server) && server.frontend === 'true';
};
/**
* Check the server whether is a backend server
*
* @param {server} server server info. it would check current server
* if server not specified
* @return {boolean}
* @memberOf Application
*/
Application.isBackend = function(server)
{
server = server || this.getCurServer();
return Boolean(server) && !server.frontend;
};
/**
* Check whether current server is a master server
*
* @return {boolean}
* @memberOf Application
*/
Application.isMaster = function()
{
return this.serverType === Constants.RESERVED.MASTER;
};
/**
* Add new server info to current application in runtime.
*
* @param {Array} servers new server info list
* @memberOf Application
*/
Application.addServers = function(servers)
{
if (!servers || !servers.length)
{
return;
}
let item, slist;
for (let i = 0, l = servers.length; i < l; i++)
{
item = servers[i];
// update global server map
this.servers[item.id] = item;
// update global server type map
slist = this.serverTypeMaps[item.serverType];
if (!slist)
{
this.serverTypeMaps[item.serverType] = slist = [];
}
replaceServer(slist, item);
// update global server type list
if (this.serverTypes.indexOf(item.serverType) < 0)
{
this.serverTypes.push(item.serverType);
}
}
this.event.emit(events.ADD_SERVERS, servers);
};
/**
* Remove server info from current application at runtime.
*
* @param {Array} ids server id list
* @memberOf Application
*/
Application.removeServers = function(ids)
{
if (!ids || !ids.length)
{
return;
}
const removeServers = [];
let id, item, slist;
for (let i = 0, l = ids.length; i < l; i++)
{
id = ids[i];
item = this.servers[id];
if (!item)
{
continue;
}
removeServers.push(item);
// clean global server map
delete this.servers[id];
const serverType = item.serverType;
// clean global server type map
slist = this.serverTypeMaps[serverType];
removeServer(slist, id);
}
this.event.emit(events.REMOVE_SERVERS, removeServers);
};
/**
* Replace server info from current application at runtime.
*
* @param {Object} server id map
* @memberOf Application
*/
Application.replaceServers = function(servers)
{
if (!servers)
{
return;
}
this.servers = servers;
this.serverTypeMaps = {};
this.serverTypes = [];
const serverArray = [];
for (const id in servers)
{
const server = servers[id];
const serverType = server[Constants.RESERVED.SERVER_TYPE];
let slist = this.serverTypeMaps[serverType];
if (!slist)
{
this.serverTypeMaps[serverType] = slist = [];
}
this.serverTypeMaps[serverType].push(server);
// update global server type list
if (this.serverTypes.indexOf(serverType) < 0)
{
this.serverTypes.push(serverType);
}
serverArray.push(server);
}
this.event.emit(events.REPLACE_SERVERS, serverArray);
};
/**
* Add crons from current application at runtime.
*
* @param {Array} crons new crons would be added in application
* @memberOf Application
*/
Application.addCrons = function(crons)
{
if (!crons || !crons.length)
{
logger.warn('crons is not defined.');
return;
}
this.event.emit(events.ADD_CRONS, crons);
};
/**
* Remove crons from current application at runtime.
*
* @param {Array} crons old crons would be removed in application
* @memberOf Application
*/
Application.removeCrons = function(crons)
{
if (!crons || !crons.length)
{
logger.warn('ids is not defined.');
return;
}
this.event.emit(events.REMOVE_CRONS, crons);
};
/**
* Update crons from current application at runtime.
*
* @param {Array} crons old crons would be removed in application
* @memberOf Application
*/
Application.UpdateeCrons = function(crons)
{
if (!crons || !crons.length)
{
logger.warn('ids is not defined.');
return;
}
this.event.emit(events.REMOVE_CRONS, crons);
};
const replaceServer = function(slist, serverInfo)
{
for (let i = 0, l = slist.length; i < l; i++)
{
if (slist[i].id === serverInfo.id)
{
slist[i] = serverInfo;
return;
}
}
slist.push(serverInfo);
};
const removeServer = function(slist, id)
{
if (!slist || !slist.length)
{
return;
}
for (let i = 0, l = slist.length; i < l; i++)
{
if (slist[i].id === id)
{
slist.splice(i, 1);
return;
}
}
};
const contains = function(str, settings)
{
if (!settings)
{
return false;
}
const ts = settings.split('|');
for (let i = 0, l = ts.length; i < l; i++)
{
if (str === ts[i])
{
return true;
}
}
return false;
};
const bindEvents = function(Event, app)
{
const emethods = new Event(app);
for (const m in emethods)
{
if (typeof emethods[m] === 'function')
{
app.event.on(m, emethods[m].bind(emethods));
}
}
};
const addFilter = function(app, type, filter)
{
let filters = app.get(type);
if (!filters)
{
filters = [];
app.set(type, filters);
}
filters.push(filter);
};