UNPKG

scalra

Version:

node.js framework to prototype and scale rapidly

599 lines (473 loc) 17.2 kB
/* // // event_manager.js // // handles all event processing within a given frontier // including socket / websocket / http / https incoming events // function: createEvent(name, para, onResponse, from); dropEvent checkin checkout send waitSocketsEmtpy unpack(data, conn, token) event.session usage: ===================== // get value 取值 event.session['abc']; // undefined event.session; // {} // set value 設值 event.session['abc'] = 'def'; event.session.abc = 'def'; event.session['abc']; // 'def' event.session.abc; // 'def' event.session; // {abc: 'def'} // replace value 取代值 event.session = { abc: 'abc', def: 'def' }; event.session['abc']; // 'abc' event.session; // {abc: 'abc', def: 'def'} event._session usage: =================== // 取值 event._session('abc'); // null event._session(); // null // 設值 event._session('abc', 'def'); event._session('abc'); // def event._session(); // { "abc": "def" } // 取代值 event._session({ abc: 'abc', def: 'abc' }); event._session('abc'); // abc event._session(); // { "abc": "abc", "def": "abc" } */ // // todo 所有 event 記錄從 checkin 到 checkout 所花費的時間, 依收到的 command 歸類分析 // todo event emitter, retry times 過多時, 把正在 processing event 從 l_eventPool 移至 deadeventpool, 禁止其 checkout, (禁此相關運作 : 尚無法實作) // (lobby dispatcher 的 function 在 vm 上執行, checkout 時才把實際變動資料) // var l_name = 'SR.EventManager'; //----------------------------------------- // define local variables // //----------------------------------------- // # of messages pending to be sent var l_pendingMessageLimit = 1000; // only one event can be executed among different sockets var l_eventPool = {}; //----------------------------------------- // define local function // //----------------------------------------- // number of maximum events for a socket concurrently // NOTE: limit may reach when server is sending a lot of data between them // for example, when shutting down a game server // so queue size cannot be too small var l_queuedEventsPerSocket = 100; // function to store a event pending to send var l_queueEvent = function (event) { // if event is not from socket, no need to queue // TODO: remove connection-specific code from here if (event.conn.type !== 'socket') return true; var socket = event.conn.connector; // if no mechanism to store (such as from a bot), just ignore // TODO: this is not clean if (typeof socket.queuedEvents === 'undefined') socket.queuedEvents = {}; var queue_size = Object.keys(socket.queuedEvents).length; if (queue_size > l_queuedEventsPerSocket) { LOG.warn('queued event size: ' + queue_size + ' limit exceeded (' + l_queuedEventsPerSocket + ')', l_name); // DEBUG purpose (print out events queued) for (var i in socket.queuedEvents) LOG.sys('queuedEvents[' + i + '] =' + UTIL.stringify(socket.queuedEvents[i].data), l_name); return false; } // store event with the ID to socket's eventlist socket.queuedEvents[event.id] = event; return true; }; // opposite of queueEvent var l_unqueueEvent = function (event) { // check if connection object exists if (typeof event.conn === 'undefined') { LOG.error('no connection records, cannot respond to request', l_name); return false; } // if no mechanism to store (such as from a bot), just ignore // TODO: cleaner approach? if (event.conn.type !== 'socket' || event.conn.connector.queuedEvents === undefined) { return true; } var socket = event.conn.connector; // check if id exist if (socket.queuedEvents.hasOwnProperty(event.id) === false) { LOG.error('event not found. id = ' + event.id, l_name); LOG.stack(); return false; } // remove current event from the socket's event queues delete socket.queuedEvents[event.id]; return true; }; /* from: { host: 'string', port: 'number', type: 'string', // 'HTTP' 'HTTPS' 'relay' cookie: 'string', // client cookie pid: 'string' // polling id } */ // build an event to process exports.createEvent = function (name, para, onResponse, from) { var conn = SR.Conn.createConnObject(from.type, onResponse, from); var data = {}; data[SR.Tags.EVENT] = name; data[SR.Tags.PARA] = para; return l_unpack(data, conn, from.cookie); }; // force checkout on a given event var l_dropEvent = exports.dropEvent = function (event) { LOG.error('dropping event [' + event.msgtype + '] (' + event.id + ')', l_name); LOG.error('=== Please check if the event did not call event.done() correctly ===', l_name); LOG.error('event data: ', l_name); LOG.error(event.data, l_name); // drop first event l_checkout(event, {}); }; // setup default dispatcher var l_dispatcher = SR.Handler.get().dispatcher; //----------------------------------------- // store a event to be processed by dispatcher // will also check if the socket can still handle so many events exports.checkin = function (event, dispatcher) { // check if dispatcher exists // NOTE: better way to pass / handle this? dispatcher = dispatcher || l_dispatcher; // if we've checked in before if (event.checkin === true) { LOG.error('event already checkin', l_name); return false; } // if socket is available, then store to socket current event & check if exceed limit // refuse checkin if this socket has too many queued events if (l_queueEvent(event) === false) return false; // in process of executing event event.checkin = true; var msgsize = Object.keys(l_eventPool).length; if (msgsize > 0 && msgsize % l_pendingMessageLimit === 0) LOG.warn('eventPool size: ' + msgsize + ' exceeds limit: ' + l_pendingMessageLimit, l_name); // emit the event // process event regardless of whether there are pending events not yet done // (otherwise we'll need to wait) l_eventPool[event.id] = event; // process event via dispatcher dispatcher(event); return true; }; //----------------------------------------- // execute checkout and return checkout obj (if exist) to client var l_checkout = exports.checkout = function (event, res_obj) { // if this event should be returned by socket, but socket does not queue this event if (l_unqueueEvent(event) === false) return; // remove this event from pool to be processed delete l_eventPool[event.id]; // mark event as done by removing its id // NOTE: unqueueEvent may still need event.id delete event.id; // // send response to requester (socket or RESTful) // // TODO: check if response's format is legal l_send(res_obj, undefined, [event.conn], event.cid); }; // generate an update packet var l_createUpdatePacket = exports.createUpdatePacket = function (type, data) { var packet = {}; packet[SR.Tags.UPDATE] = type; packet[SR.Tags.PARA] = data; return packet; }; //----------------------------------------- // send message to an array of connections var l_send = exports.send = function (packet_type, para, connections, cid) { // convert a single destination into an array if (typeof connections === 'string') connections = [connections]; // check if target connections are valid if (connections instanceof Array === false) { LOG.error('connections undefined or is not an array, drop message', l_name); LOG.stack(); return false; } if (connections.length === 0) { LOG.sys('connection list is empty, drop message', l_name); return false; } // check if packet to send if valid // TODO: currently we support two formats: // 1) sending an object directly, // 2) 'packet_type' + 'para'.. // this is because checkout currently does not accept 'packet_type' + 'para' format // but should modify it if possible... var res_obj = {}; if (typeof packet_type === 'string') { // default to empty parameter para = para || {}; res_obj = l_createUpdatePacket(packet_type, para); } else if (typeof packet_type === 'object') { res_obj = packet_type; } else { LOG.error('packet type is undefined or incorrect format, drop message', l_name); return false; } // if nothing to be sent back to client, stop now if (Object.keys(res_obj).length === 0) { // need to go over all connections as HTTP requests still need to be terminated for (var i = 0; i < connections.length; ++i) { // NOTE: make sure empty paremters indicate a 'no-send' for types besides 'HTTP' if (typeof connections[i].connector === 'function') { connections[i].connector(); } else { LOG.warn('connector missing or not a function', l_name); LOG.warn(connections[i], l_name); LOG.stack(); } } return false; } // show if we're sending to more than one client if (connections.length > 1) LOG.sys(SR.Tags.SND + 'send to '+ connections.length + ' clients' + SR.Tags.END, l_name); // attach client defined id if exist (sent by the client in the event) // NOTE: client event ID (cid) is a requester-generated unique ID // returned by the server processing the request, to identify the message // TODO: combine with SR.RPC mechanism? if (cid) { res_obj._cid = cid; } // serialize object to a string (to send over socket or HTTP) // NOTE: we serialize here so this only needs to be done once for possibly // different connection types // NOTE: object may fail to serialize due to circular structure var data = UTIL.stringify(res_obj); if (!data) { LOG.stack(); return false; } // print message to send (partial up to 250 characters, adjustable in LENGTH_OUTMSG setting) // NOTE: this is a 'debug' level message so developer can also see it // avoid sending streaming data (skip it) // TODO: a better approach if (SR.Settings.HIDDEN_EVENT_TYPES.hasOwnProperty(res_obj[SR.Tags.UPDATE]) === false) LOG.debug(SR.Tags.SND + data.length + ' ' + data.substring(0, SR.Settings.LENGTH_OUTMSG) + SR.Tags.END, l_name); //else // LOG.debug(SR.Tags.SND + data.length + ' ' + res_obj[SR.Tags.UPDATE] + SR.Tags.END, l_name); // number of messages dropped due to invalid connection var droppedMessage = 0; // go through each connection and send for (var i = 0; i < connections.length; ++i) { // get current connection var conn = connections[i]; // check if this is a connID, translate to a connection object if (typeof conn === 'string') conn = SR.Conn.getConnObject(conn); if (typeof conn === 'undefined') { LOG.error('connection object is invalid, cannot send', l_name); continue; } // check if it's purely a socket (should not happen) if (typeof conn.connector === 'undefined') { LOG.error('connector not found', l_name); LOG.error(conn); continue; } // record size SR.Stat.add('net_out', data.length); LOG.sys('sending [' + conn.type + '] message...', l_name); // NOTE: both object (res_obj) and string (data) formats are passed for flexibility // NOTE: conn object is also passed because right now conn.pid (polling id) may be used by http response if (conn.connector(res_obj, data, conn) === false) droppedMessage++; } // for connection array // print out dropped message if (droppedMessage > 0) LOG.error(droppedMessage + ' messages dropped.', l_name); return true; }; //----------------------------------------- // TODO: move this to socket-specific processing // used in conn.js var l_waitSocketsEmptyPool = new SR.AdvQueue(); // wait for all events be done for a given socket exports.waitSocketsEmpty = function (socket, onDone) { if (socket.hasOwnProperty('queuedEvents') === false) { LOG.sys('socket does not have queuedEvents', l_name); return UTIL.safeCall(onDone); } l_waitSocketsEmptyPool.enqueue( { socket: socket, onComplete: onDone }, function (item) { // if conn still has pending item, re-queue and keep waiting if (Object.keys(item.socket.queuedEvents).length > 0) return false; UTIL.safeCall(item.onComplete); return true; } ); }; // get session content based on a token // returns empty collection if not found exports.getSession = function (token) { var session_token = new Buffer(token).toString('base64').substring(0,150); return SR.State.get(session_token); }; // // a Event object, for each incoming packet/request, // we turn it into a Event (with 'socket' and 'data') awaiting processing // by Event handlers // function Event (data, conn, token) { // internal id this.id = UTIL.createUUID(); // store connection object this.conn = conn; // incoming data this.data = data; // flags to indicate current status of Event this.checkin = false; // create session token and create/load session values // NOTE: do not use 'port' as part of token generation, as port can change during repeated HTTP requests // NOTE: if 'conn.host' is used, for some clients, their IP might change from request to request (due to load balancer) // then session will break //this.conn.session_token = new Buffer(token + conn.host).toString('base64').substring(0,150); // TODO: review potential security vulunarabilility here token = token || ''; this.conn.session_token = new Buffer(token).toString('base64').substring(0,150); // need to de-allocate the sessions when no longer used // TODO: set expire time? this.session = SR.State.get(this.conn.session_token); // record client id (if available) // NOTE: this id will be sent back as a '_cid' attribute in the response for this event // when the event is checkout, this is to ensure that // there's a unique response to each unqiue event // TODO: simplify this? combine with forwardEvent in SR.RPC? if (data.hasOwnProperty('_cid')) this.cid = data._cid; } // attach convenience functions Event.prototype.done = function (packet_type, para, connections) { // build response packet, if exist var response = {}; if (packet_type !== undefined) { // auto fill-in name if not provided if (typeof packet_type === 'object') { para = packet_type; packet_type = this.msgtype; } // check if we should return back rid (request id) to allow requesting client to handle response uniquely if (this.rid) { // NOTE: if para is not JSON object (such as string or array), rid won't be attachable if (para instanceof Array || typeof para === 'string') { LOG.error('[' + packet_type + '] return value is not JSON object (array or string), please return JSON objects to avoid potential client-side callback sequence misordering', l_name); } else { para['_rid'] = this.rid; } } response = l_createUpdatePacket(packet_type, para); } // perform checkout first //LOG.warn('checking out response:'); //LOG.warn(response); SR.EventManager.checkout(this, response); // send packet to other client(s) if connections are provided if (typeof connections !== 'undefined') this.send(packet_type, para, connections, false); }; // respond to a specific packet, or a number of other connections (if provided) // NOTE: default to not sending to self Event.prototype.send = function (packet_type, para, connections, to_self) { var send_self = (typeof to_self === 'undefined' ? true : to_self); // check if sockets are specified (if not, default is this event's socket) if (typeof connections === 'undefined' || connections instanceof Array === false) { connections = []; } else { // check if list of sockets has self for (var i=0; i < connections.length; i++) { if (connections[i].connID == this.conn.connID) { send_self = false; break; } } } if (send_self === true) { LOG.sys('send to self true, adding self...', l_name); connections.push(this.conn); } SR.EventManager.send(packet_type, para, connections); }; // print the source of the request for this event Event.prototype.printSource = function () { var src = this.conn.host + ':' + this.conn.port + ' ' + this.conn.type; return src; }; // session var l_sessionPool = {}; Event.prototype._session = function (query, data) { var token = this.session_token; // store directly if (typeof query === 'object') { // total replacement //l_sessionPool[token] = query; // incrementally add new keys for (key in query) l_sessionPool[token][key] = query[key]; } // store a key-value in string else if (typeof query === 'string' && typeof data !== 'undefined') { if (!l_sessionPool[token]) { l_sessionPool[token] = {}; } l_sessionPool[token][query] = data; } // get else if (typeof query === 'string' && typeof data === 'undefined') { if (l_sessionPool[token] && l_sessionPool[token][query]) { return l_sessionPool[token][query]; } else { return null; } } // return all else if (typeof query === 'undefined' && typeof data === 'undefined') { if (l_sessionPool[token]) { return l_sessionPool[token]; } else { return null; } } }; //----------------------------------------- // build an event from a general event and connection object var l_unpack = exports.unpack = function (data, conn, token) { return new Event(data, conn, token); }; exports.list = function (arg) { console.log('l_eventPool'); console.log(l_eventPool); return true; };