UNPKG

occaecatidicta

Version:
742 lines (663 loc) 25.1 kB
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