occaecatidicta
Version:
742 lines (663 loc) • 25.1 kB
text/typescript
import { getLogger, Logger } from 'omelox-logger';
import { failureProcess } from './failureProcess';
import { constants } from '../util/constants';
import * as Station from './mailstation';
import { MailStation, MailStationErrorHandler, MailStationOpts, RpcFilter, RpcServerInfo } from './mailstation';
import { Tracer } from '../util/tracer';
import * as Loader from 'omelox-loader';
import { LoaderPathType } from 'omelox-loader';
import { listEs6ClassMethods } from '../util/utils';
import * as router from './router';
import * as async from 'async';
import { ConsistentHash } from '../util/consistentHash';
import { RemoteServerCode } from '../../index';
let logger = getLogger('omelox-rpc', 'rpc-client');
/**
* Client states
*/
let STATE_INITED = 1; // client has inited
let STATE_STARTED = 2; // client has started
let STATE_CLOSED = 3; // client has closed
export type RouterFunction = (session: { [key: string]: any }, msg: RpcMsg, context: RouteContext, cb: (err: Error, serverId?: string) => void) => void;
export type Router = RouterFunction | { route: RouterFunction };
export type RouteServers = RpcServerInfo[];
export interface RouteContextClass {
getServersByType?: (serverType: string) => RouteServers;
}
export type RouteContext = RouteServers | RouteContextClass;
export interface Proxy {
// 根据路由参数决定发往哪台服务器,第一个是路由参数,其他是rpc参数
(routeParam: any, ...args: any[]): Promise<any>;
// 根据服务器id决定发往哪个服务器,serverId如果是*,则发往所有这个rpc所属类型的服务器
toServer(serverId: string, ...args: any[]): Promise<any>;
// 根据路由参数决定发往哪台服务器,typescript友好
route(routeParam: any, notify?: boolean): (...args: any[]) => Promise<any>;
// 默认传递null作为路由参数,typescript友好
defaultRoute(...args: any[]): Promise<any>;
// 根据服务器id决定发往哪个服务器,serverId如果是*,则发往所有这个rpc所属类型的服务器
to(serverId: string, notify?: boolean): (...args: any[]) => Promise<any>;
// 广播到所有这个rpc服务器的类型的服务器
broadcast(...args: any[]): Promise<any>;
}
export type ProxyCallback = (routeParam: any, serviceName: string, methodName: string, args: any[], attach: RemoteServerCode, isToSpecifiedServer?: boolean) => Promise<any>;
export type Proxies = {
[namespace: string]:
{
[serverType: string]:
{
[remoterName: string]:
{ [attr: string]: Proxy }
}
}
};
export interface RpcClientOpts extends MailStationOpts {
context?: any;
routeContext?: RouteContext;
router?: Router;
routerType?: string;
rpcDebugLog?: boolean;
clientId?: string;
servers?: { serverType: Array<RpcServerInfo> };
rpcLogger?: Logger;
station?: MailStation;
hashFieldIndex?: number;
bufferMsg?: boolean;
interval?: number;
timeout?: number;
// 使用动态 rpc.user.servertype rpc 方法 ,将支持 热更新的 新rpc方法与新rpc文件
// 同时带来一个额外好处,内存会变小,因为不会去load remoter文件。
// 有性能损耗
// 100w次 rpc调用
// 正常方法 32.143ms
// 动态代理方法: 95.970ms
// 测试代码:
/*
let objdefine = {user: {main: {}}}
objdefine.user.main = new Proxy(objdefine.user.main, {
get(target, remoterName,) {
if (target[remoterName]) {
return target[remoterName];
}
target[remoterName] = {};
target[remoterName] = new Proxy(target[remoterName], {
get(target, attr,) {
if (target[attr]) {
return target[attr];
}
// get attr
target[attr] = () => {
return 1
}
return target[attr];
}
})
return target[remoterName];
}
})
let testobj = {
user: {
main: {
remoter: {
method() {
return 1
}
}
}
}
}
console.log(testobj.user.main.remoter.method())
function testfun(obj, name) {
console.time(name)
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += obj.user.main.remoter.method()
}
console.timeEnd(name)
}
testfun(objdefine, 'objdefine')
testfun(testobj, 'object')
*/
dynamicUserProxy?: boolean;
}
export interface RpcMsg {
namespace: string;
serverType?: string;
service: string;
method: string;
args: any[];
}
export interface TargetRouterFunction {
(serverType: string, msg: RpcMsg, routeParam: object, cb: (err: Error, serverId: string) => void): void;
}
/**
* RPC Client Class
*/
export class RpcClient {
_context: any;
_routeContext: RouteContext;
router: Router;
routerType: string;
rpcDebugLog: boolean;
opts: RpcClientOpts;
proxies: Proxies;
_station: MailStation;
state: number;
targetRouterFunction: TargetRouterFunction;
wrrParam?: { [serverType: string]: { index: number, weight: number } };
chParam?: { [serverType: string]: { consistentHash: ConsistentHash } };
constructor(opts?: RpcClientOpts) {
opts = opts || {};
this._context = opts.context;
this._routeContext = opts.routeContext;
this.router = opts.router || router.df;
this.routerType = opts.routerType;
this.rpcDebugLog = opts.rpcDebugLog;
if (this._context) {
opts.clientId = this._context.serverId;
}
this.opts = opts;
this.proxies = {};
this.targetRouterFunction = this.getRouteFunction();
this._station = createStation(opts);
this.state = STATE_INITED;
}
/**
* Start the rpc client which would try to connect the remote servers and
* report the result by cb.
*
* @param cb {Function} cb(err)
*/
start(cb: (err?: Error) => void) {
if (this.state > STATE_INITED) {
cb(new Error('rpc client has started.'));
return;
}
let self = this;
this._station.start(function (err: Error) {
if (err) {
logger.error('[omelox-rpc] client start fail for ' + err.stack);
return cb(err);
}
self._station.on('error', failureProcess.bind(self._station));
self.state = STATE_STARTED;
cb();
});
}
/**
* Stop the rpc client.
*
* @param {Boolean} force
* @return {Void}
*/
stop(force: boolean) {
if (this.state !== STATE_STARTED) {
logger.warn('[omelox-rpc] client is not running now.');
return;
}
this.state = STATE_CLOSED;
this._station.stop(force);
}
/**
* Add a new proxy to the rpc client which would overrid the proxy under the
* same key.
*
* @param {Object} record proxy description record, format:
* {namespace, serverType, path}
*/
addProxy(record: RemoteServerCode) {
if (!record) {
return;
}
let proxy = this.generateProxy(record, this._context);
if (!proxy) {
return;
}
insertProxy(this.proxies, record.namespace, record.serverType, proxy);
}
/**
* Batch version for addProxy.
*
* @param {Array} records list of proxy description record
*/
addProxies(records: RemoteServerCode[]) {
if (!records || !records.length) {
return;
}
for (let i = 0, l = records.length; i < l; i++) {
this.addProxy(records[i]);
}
}
/**
* Add new remote server to the rpc client.
*
* @param {Object} server new server information
*/
addServer(server: RpcServerInfo) {
this._station.addServer(server);
}
/**
* Batch version for add new remote server.
*
* @param {Array} servers server info list
*/
addServers(servers: RpcServerInfo[]) {
this._station.addServers(servers);
}
/**
* Remove remote server from the rpc client.
*
* @param {String|Number} id server id
*/
removeServer(id: string | number) {
this._station.removeServer(id);
}
/**
* Batch version for remove remote server.
*
* @param {Array} ids remote server id list
*/
removeServers(ids: Array<string | number>) {
this._station.removeServers(ids);
}
/**
* Replace remote servers.
*
* @param {Array} servers server info list
*/
replaceServers(servers: RpcServerInfo[]) {
this._station.replaceServers(servers);
}
/**
* Do the rpc invoke directly.
*
* @param serverId {String} remote server id
* @param msg {Object} rpc message. Message format:
* {serverType: serverType, service: serviceName, method: methodName, args: arguments}
* @param cb {Function} cb(err, ...)
*/
rpcInvoke(serverId: string, msg: RpcMsg, cb: (err: Error, ...args: any[]) => void) {
let rpcDebugLog = this.rpcDebugLog;
let tracer: Tracer;
if (rpcDebugLog) {
tracer = new Tracer(this.opts.rpcLogger, this.opts.rpcDebugLog, this.opts.clientId, serverId, msg);
tracer.info('client', __filename, 'rpcInvoke', 'the entrance of rpc invoke');
}
if (this.state !== STATE_STARTED) {
tracer && tracer.error('client', __filename, 'rpcInvoke', 'fail to do rpc invoke for client is not running');
logger.error('[omelox-rpc] fail to do rpc invoke for client is not running');
cb ? cb(new Error('[omelox-rpc] fail to do rpc invoke for client is not running')) : null;
return;
}
this._station.dispatch(tracer, serverId, msg, this.opts, cb);
}
/**
* Add rpc before filter.
*
* @param filter {Function} rpc before filter function.
*
* @api public
*/
before(filter: RpcFilter | RpcFilter[]) {
this._station.before(filter);
}
/**
* Add rpc after filter.
*
* @param filter {Function} rpc after filter function.
*
* @api public
*/
after(filter: RpcFilter | RpcFilter[]) {
this._station.after(filter);
}
/**
* Add rpc filter.
*
* @param filter {Function} rpc filter function.
*
* @api public
*/
filter(filter: RpcFilter) {
this._station.filter(filter);
}
/**
* Set rpc filter error handler.
*
* @param handler {Function} rpc filter error handler function.
*
* @api public
*/
setErrorHandler(handler: MailStationErrorHandler) {
this._station.handleError = handler;
}
/**
* Generate prxoy for function type field
*
* @param client {Object} current client instance.
* @param serviceName {String} delegated service name.
* @param methodName {String} delegated method name.
* @param args {Object} rpc invoke arguments.
* @param attach {Object} attach parameter pass to proxyCB.
* @param isToSpecifiedServer {boolean} true means rpc route to specified remote server.
*
* @api private
*/
private rpcToRoute(routeParam: any, serviceName: string, methodName: string, args: Array<any>, attach: RemoteServerCode, notify: boolean = false) {
if (this.state !== STATE_STARTED) {
return Promise.reject(new Error('[omelox-rpc] fail to invoke rpc proxy for client is not running'));
}
let serverType = attach.serverType;
let msg = {
namespace: attach.namespace,
serverType: serverType,
service: serviceName,
method: methodName,
args: args
};
return new Promise((resolve, reject) => {
this.targetRouterFunction(serverType, msg, routeParam, (err: Error, serverId: string) => {
if (err) {
return reject(err);
}
let cb = notify ? null : (err: Error, resp: string) => err ? reject(err) : resolve(resp);
this.rpcInvoke(serverId, msg, cb);
if (notify) {
resolve();
}
});
});
}
/**
* Rpc to specified server id or servers.
*
* @param client {Object} current client instance.
* @param msg {Object} rpc message.
* @param serverType {String} remote server type.
* @param serverId {Object} mailbox init context parameter.
* @param cb {Function} AsyncResultArrayCallback<{}, {}>
*
* @api private
*/
private rpcToSpecifiedServer(serverId: string, serviceName: string, methodName: string, args: Array<any>, attach: RemoteServerCode, notify: boolean = false) {
if (this.state !== STATE_STARTED) {
return Promise.reject(new Error('[omelox-rpc] fail to invoke rpc proxy for client is not running'));
}
let serverType = attach.serverType;
let msg = {
namespace: attach.namespace,
serverType: serverType,
service: serviceName,
method: methodName,
args: args
};
return new Promise((resolve, reject) => {
if (typeof serverId !== 'string') {
logger.error('[omelox-rpc] serverId is not a string : %s', serverId);
return;
}
let cb = notify ? null : ((err: Error, resp: any) => err ? reject(err) : resolve(resp));
if (serverId === '*') {
// (client._routeContext as RouteContextClass).getServersByType(serverType);
let servers: string[];
if (this._routeContext && (this._routeContext as RouteContextClass).getServersByType) {
const serverinfos = (this._routeContext as RouteContextClass).getServersByType(serverType);
if (serverinfos) {
servers = serverinfos.map(v => v.id);
}
} else {
servers = this._station.serversMap[serverType];
}
// console.log('servers ', servers);
if (!servers) {
logger.error('[omelox-rpc] serverType %s servers not exist', serverType);
return;
}
async.map(servers, (serverId, next) => {
this.rpcInvoke(serverId, msg, cb ? next : null);
if (!cb) {
next();
}
}, cb);
} else {
this.rpcInvoke(serverId, msg, cb);
}
if (notify) {
return resolve();
}
});
}
/**
* Generate proxies for remote servers.
*
* @param client {Object} current client instance.
* @param record {Object} proxy reocrd info. {namespace, serverType, path}
* @param context {Object} mailbox init context parameter
*
* @api private
*/
private generateProxy(record: RemoteServerCode, context: object) {
if (!record) {
return;
}
if (this.opts.dynamicUserProxy && record.namespace === 'user') {
const self = this;
let res: { [key: string]: any } = {};
res = new Proxy(res, {
get(target: { [p: string]: any }, remoterName: string): any {
if (target[remoterName]) {
return target[remoterName];
}
target[remoterName] = {};
target[remoterName] = new Proxy(target[remoterName], {
get(target: { [p: string]: any }, attr: string): any {
if (target[attr]) {
return target[attr];
}
// get attr
target[attr] = self.genFunctionProxy(remoterName, attr, null, record);
return target[attr];
}
})
return target[remoterName];
}
})
return res;
}
let res: { [key: string]: any }, name;
let modules: { [key: string]: any } = Loader.load(record.path, context, false, false, LoaderPathType.OMELOX_REMOTER);
if (modules) {
res = {};
for (name in modules) {
res[name] = this.genObjectProxy(
name,
modules[name],
record
);
}
}
return res;
}
/**
* Create proxy.
* @param serviceName {String} deletgated service name
* @param origin {Object} delegated object
* @param attach {Object} attach parameter pass to proxyCB
* @return {Object} proxy instance
*/
private genObjectProxy(serviceName: string, origin: any, attach: RemoteServerCode) {
// generate proxy for function field
let res: { [key: string]: Proxy } = {};
let proto = listEs6ClassMethods(origin);
for (let field of proto) {
res[field] = this.genFunctionProxy(serviceName, field, origin, attach);
}
return res;
}
/**
* Generate prxoy for function type field
*
* @param namespace {String} current namespace
* @param serverType {String} server type string
* @param serviceName {String} delegated service name
* @param methodName {String} delegated method name
* @param origin {Object} origin object
* @param proxyCB {Functoin} proxy callback function
* @returns function proxy
*/
private genFunctionProxy(serviceName: string, methodName: string, origin: any, attach: RemoteServerCode) {
let self = this;
return (function (): Proxy {
// 兼容旧的api
let proxy: any = function () {
let len = arguments.length;
if (len < 1) {
logger.error('[omelox-rpc] invalid rpc invoke, arguments length less than 1, namespace: %j, serverType, %j, serviceName: %j, methodName: %j',
attach.namespace, attach.serverType, serviceName, methodName);
return Promise.reject(new Error('[omelox-rpc] invalid rpc invoke, arguments length less than 1'));
}
let routeParam = arguments[0];
let args = new Array(len - 1);
for (let i = 1; i < len; i++) {
args[i - 1] = arguments[i];
}
return self.rpcToRoute(routeParam, serviceName, methodName, args, attach);
};
// 新的api,通过路由参数决定发往哪个服务器
proxy.route = (routeParam: any, notify?: boolean) => {
return function (...args: any[]) {
return self.rpcToRoute(routeParam, serviceName, methodName, args, attach, notify);
};
};
// 新的api,发往指定的服务器id
proxy.to = (serverId: string, notify?: boolean) => {
return function (...args: any[]) {
return self.rpcToSpecifiedServer(serverId, serviceName, methodName, args, attach, notify);
};
};
// 新的api,广播出去
proxy.broadcast = function (...args: any[]) {
return self.rpcToSpecifiedServer('*', serviceName, methodName, args, attach);
};
// 新的api,使用默认路由调用
proxy.defaultRoute = function (...args: any[]) {
return self.rpcToRoute(null, serviceName, methodName, args, attach);
};
// 兼容旧的api
proxy.toServer = function () {
let len = arguments.length;
if (len < 1) {
logger.error('[omelox-rpc] invalid rpc invoke, arguments length less than 1, namespace: %j, serverType, %j, serviceName: %j, methodName: %j',
attach.namespace, attach.serverType, serviceName, methodName);
return Promise.reject(new Error('[omelox-rpc] invalid rpc invoke, arguments length less than 1'));
}
let routeParam = arguments[0];
let args = new Array(len - 1);
for (let i = 1; i < len; i++) {
args[i - 1] = arguments[i];
}
return self.rpcToSpecifiedServer(routeParam, serviceName, methodName, args, attach);
};
return proxy;
})();
}
/**
* Calculate remote target server id for rpc client.
*
* @param client {Object} current client instance.
* @param serverType {String} remote server type.
* @param msg {Object} RpcMsg
* @param routeParam {Object} mailbox init context parameter.
* @param cb {Function} return rpc remote target server id.
*
* @api private
*/
private getRouteFunction(): TargetRouterFunction {
if (!!this.routerType) {
let method: (client: RpcClient, serverType: string, msg: RpcMsg, cb: (err: Error, serverId?: string) => void) => void;
switch (this.routerType) {
case constants.SCHEDULE.ROUNDROBIN:
method = router.rr;
break;
case constants.SCHEDULE.WEIGHT_ROUNDROBIN:
method = router.wrr;
break;
case constants.SCHEDULE.LEAST_ACTIVE:
method = router.la;
break;
case constants.SCHEDULE.CONSISTENT_HASH:
method = router.ch;
break;
default:
method = router.rd;
break;
}
return (serverType: string, msg: RpcMsg, routeParam: object, cb: (err: Error, serverId: string) => void) => {
method.call(null, this, serverType, msg, function (err: Error, serverId: string) {
cb(err, serverId);
});
};
} else {
let route: RouterFunction, target: Object;
if (typeof this.router === 'function') {
route = this.router;
target = null;
} else if (typeof this.router.route === 'function') {
route = this.router.route;
target = this.router;
} else {
logger.error('[omelox-rpc] invalid route function.');
return;
}
return (serverType: string, msg: RpcMsg, routeParam: object, cb: (err: Error, serverId: string) => void) => {
route.call(target, routeParam, msg, this._routeContext, function (err: Error, serverId: string) {
cb(err, serverId);
});
};
}
}
}
/**
* Create mail station.
*
* @param opts {Object} construct parameters.
*
* @api private
*/
function createStation(opts: RpcClientOpts) {
return Station.createMailStation(opts);
}
/**
* Add proxy into array.
*
* @param proxies {Object} rpc proxies
* @param namespace {String} rpc namespace sys/user
* @param serverType {String} rpc remote server type
* @param proxy {Object} rpc proxy
*
* @api private
*/
function insertProxy(proxies: Proxies, namespace: string, serverType: string, proxy: { [key: string]: any }) {
proxies[namespace] = proxies[namespace] || {};
if (proxies[namespace][serverType]) {
for (let attr in proxy) {
proxies[namespace][serverType][attr] = proxy[attr];
}
} else {
proxies[namespace][serverType] = proxy;
}
}
/**
* RPC client factory method.
*
* @param {Object} opts client init parameter.
* opts.context: mail box init parameter,
* opts.router: (optional) rpc message route function, route(routeParam, msg, cb),
* opts.mailBoxFactory: (optional) mail box factory instance.
* @return {Object} client instance.
*/
export function createClient(opts: RpcClientOpts) {
return new RpcClient(opts);
}
// module.exports.WSMailbox from ('./mailboxes/ws-mailbox'); // socket.io
// module.exports.WS2Mailbox from ('./mailboxes/ws2-mailbox'); // ws
// export { create as MQTTMailbox } from './mailboxes/mqtt-mailbox'; // mqtt