2017-10-12 10:00:28 +08:00
/* global PowerQueue */
import Redis from 'redis' ;
import { Meteor } from 'meteor/meteor' ;
import { EventEmitter2 } from 'eventemitter2' ;
import { check } from 'meteor/check' ;
2020-12-08 04:57:33 +08:00
import fs from 'fs' ;
2017-10-12 10:00:28 +08:00
import Logger from './logger' ;
2020-12-10 23:07:06 +08:00
import Metrics from './metrics' ;
2017-10-12 10:00:28 +08:00
// Fake meetingId used for messages that have no meetingId
const NO _MEETING _ID = '_' ;
2020-12-11 01:05:22 +08:00
const { queueMetrics } = Meteor . settings . private . redis . metrics ;
2020-12-08 04:57:33 +08:00
2019-04-06 06:32:21 +08:00
const makeEnvelope = ( channel , eventName , header , body , routing ) => {
2017-10-12 10:00:28 +08:00
const envelope = {
envelope : {
name : eventName ,
2019-04-06 06:32:21 +08:00
routing : routing || {
2020-12-12 05:36:06 +08:00
sender : 'html5-server' ,
2017-10-12 10:00:28 +08:00
} ,
2019-10-19 00:50:38 +08:00
timestamp : Date . now ( ) ,
2017-10-12 10:00:28 +08:00
} ,
core : {
header ,
body ,
} ,
} ;
return JSON . stringify ( envelope ) ;
} ;
2019-02-16 03:45:42 +08:00
class MeetingMessageQueue {
2020-11-13 10:03:57 +08:00
constructor ( eventEmitter , asyncMessages = [ ] , redisDebugEnabled = false ) {
2017-10-12 10:00:28 +08:00
this . asyncMessages = asyncMessages ;
this . emitter = eventEmitter ;
this . queue = new PowerQueue ( ) ;
2020-11-13 10:03:57 +08:00
this . redisDebugEnabled = redisDebugEnabled ;
2017-10-12 10:00:28 +08:00
this . handleTask = this . handleTask . bind ( this ) ;
this . queue . taskHandler = this . handleTask ;
}
handleTask ( data , next ) {
const { channel } = data ;
const { envelope } = data . parsedMessage ;
const { header } = data . parsedMessage . core ;
const { body } = data . parsedMessage . core ;
2018-01-08 08:25:56 +08:00
const { meetingId } = header ;
2017-10-12 10:00:28 +08:00
const eventName = header . name ;
const isAsync = this . asyncMessages . includes ( channel )
|| this . asyncMessages . includes ( eventName ) ;
2020-12-10 02:06:25 +08:00
const beginHandleTimestamp = Date . now ( ) ;
2017-10-12 10:00:28 +08:00
let called = false ;
check ( eventName , String ) ;
check ( body , Object ) ;
const callNext = ( ) => {
if ( called ) return ;
2020-11-13 10:03:57 +08:00
if ( this . redisDebugEnabled ) {
Logger . debug ( ` Redis: ${ eventName } completed ${ isAsync ? 'async' : 'sync' } ` ) ;
}
2017-10-12 10:00:28 +08:00
called = true ;
2020-12-08 04:57:33 +08:00
if ( queueMetrics ) {
const queueId = meetingId || NO _MEETING _ID ;
const dataLength = JSON . stringify ( data ) . length ;
2020-12-10 23:07:06 +08:00
Metrics . processEvent ( queueId , eventName , dataLength , beginHandleTimestamp ) ;
2020-12-08 04:57:33 +08:00
}
2018-04-03 04:16:46 +08:00
const queueLength = this . queue . length ( ) ;
2020-11-25 23:32:45 +08:00
if ( queueLength > 100 ) {
2020-11-13 10:03:57 +08:00
Logger . warn ( ` Redis: MeetingMessageQueue for meetingId= ${ meetingId } has queue size= ${ queueLength } ` ) ;
2018-04-03 04:16:46 +08:00
}
2017-10-12 10:00:28 +08:00
next ( ) ;
} ;
const onError = ( reason ) => {
2019-06-29 02:51:26 +08:00
Logger . error ( ` ${ eventName } : ${ reason . stack ? reason . stack : reason } ` ) ;
2017-10-12 10:00:28 +08:00
callNext ( ) ;
} ;
try {
2020-11-13 10:03:57 +08:00
if ( this . redisDebugEnabled ) {
Logger . debug ( ` Redis: ${ JSON . stringify ( data . parsedMessage . core ) } emitted ` ) ;
}
2017-10-12 10:00:28 +08:00
if ( isAsync ) {
callNext ( ) ;
}
this . emitter
. emitAsync ( eventName , { envelope , header , body } , meetingId )
. then ( callNext )
. catch ( onError ) ;
} catch ( reason ) {
onError ( reason ) ;
}
}
add ( ... args ) {
return this . queue . add ( ... args ) ;
}
}
2017-10-13 03:07:02 +08:00
class RedisPubSub {
2017-10-12 10:00:28 +08:00
static handlePublishError ( err ) {
if ( err ) {
Logger . error ( err ) ;
}
}
constructor ( config = { } ) {
this . config = config ;
this . didSendRequestEvent = false ;
2019-04-13 03:55:25 +08:00
const host = process . env . REDIS _HOST || Meteor . settings . private . redis . host ;
const redisConf = Meteor . settings . private . redis ;
2020-12-01 00:09:35 +08:00
this . instanceMax = parseInt ( process . env . INSTANCE _MAX , 10 ) || 1 ;
2020-12-12 05:36:06 +08:00
this . instanceId = parseInt ( process . env . INSTANCE _ID , 10 ) || 1 ; // 1 also handles running in dev mode
2020-12-12 09:45:38 +08:00
this . customRedisChannel = ` to-html5-redis-channel ${ this . instanceId } ` ;
2020-11-19 04:31:36 +08:00
2019-04-13 03:55:25 +08:00
const { password , port } = redisConf ;
2019-06-29 02:51:26 +08:00
if ( password ) {
2019-04-13 03:55:25 +08:00
this . pub = Redis . createClient ( { host , port , password } ) ;
this . sub = Redis . createClient ( { host , port , password } ) ;
this . pub . auth ( password ) ;
this . sub . auth ( password ) ;
} else {
this . pub = Redis . createClient ( { host , port } ) ;
this . sub = Redis . createClient ( { host , port } ) ;
2019-04-10 01:58:56 +08:00
}
2020-12-08 20:37:59 +08:00
if ( queueMetrics ) {
2020-12-10 23:07:06 +08:00
Metrics . startDumpFile ( ) ;
2020-12-08 20:37:59 +08:00
}
2017-10-12 10:00:28 +08:00
this . emitter = new EventEmitter2 ( ) ;
this . mettingsQueues = { } ;
2020-11-19 04:31:36 +08:00
this . mettingsQueues [ NO _MEETING _ID ] = new MeetingMessageQueue ( this . emitter , this . config . async , this . config . debug ) ;
2017-10-12 10:00:28 +08:00
this . handleSubscribe = this . handleSubscribe . bind ( this ) ;
this . handleMessage = this . handleMessage . bind ( this ) ;
}
init ( ) {
this . sub . on ( 'psubscribe' , Meteor . bindEnvironment ( this . handleSubscribe ) ) ;
this . sub . on ( 'pmessage' , Meteor . bindEnvironment ( this . handleMessage ) ) ;
const channelsToSubscribe = this . config . subscribeTo ;
2020-12-12 09:45:38 +08:00
2020-12-15 09:55:57 +08:00
channelsToSubscribe . push ( this . customRedisChannel ) ;
2017-10-12 10:00:28 +08:00
channelsToSubscribe . forEach ( ( channel ) => {
this . sub . psubscribe ( channel ) ;
} ) ;
2020-11-13 10:03:57 +08:00
if ( this . redisDebugEnabled ) {
Logger . debug ( ` Redis: Subscribed to ' ${ channelsToSubscribe } ' ` ) ;
}
2017-10-12 10:00:28 +08:00
}
updateConfig ( config ) {
this . config = Object . assign ( { } , this . config , config ) ;
2020-11-13 10:03:57 +08:00
this . redisDebugEnabled = this . config . debug ;
2017-10-12 10:00:28 +08:00
}
2018-04-24 21:59:13 +08:00
2017-10-12 10:00:28 +08:00
// TODO: Move this out of this class, maybe pass as a callback to init?
handleSubscribe ( ) {
if ( this . didSendRequestEvent ) return ;
// populate collections with pre-existing data
2018-01-08 08:24:05 +08:00
const REDIS _CONFIG = Meteor . settings . private . redis ;
2017-10-12 10:00:28 +08:00
const CHANNEL = REDIS _CONFIG . channels . toAkkaApps ;
const EVENT _NAME = 'GetAllMeetingsReqMsg' ;
const body = {
requesterId : 'nodeJSapp' ,
2020-12-12 05:36:06 +08:00
html5InstanceId : this . instanceId ,
2017-10-12 10:00:28 +08:00
} ;
this . publishSystemMessage ( CHANNEL , EVENT _NAME , body ) ;
this . didSendRequestEvent = true ;
}
handleMessage ( pattern , channel , message ) {
const parsedMessage = JSON . parse ( message ) ;
const { name : eventName , meetingId } = parsedMessage . core . header ;
const { ignored : ignoredMessages , async } = this . config ;
if ( ignoredMessages . includes ( channel )
|| ignoredMessages . includes ( eventName ) ) {
2018-10-17 01:48:27 +08:00
if ( eventName === 'CheckAlivePongSysMsg' ) {
return ;
}
2020-11-13 10:03:57 +08:00
if ( this . redisDebugEnabled ) {
Logger . debug ( ` Redis: ${ eventName } skipped ` ) ;
}
2017-10-12 10:00:28 +08:00
return ;
}
const queueId = meetingId || NO _MEETING _ID ;
2020-12-12 09:45:38 +08:00
if ( eventName === 'MeetingCreatedEvtMsg' || eventName === 'SyncGetMeetingInfoRespMsg' ) {
2020-11-19 04:31:36 +08:00
const newIntId = parsedMessage . core . body . props . meetingProp . intId ;
2020-12-12 09:45:38 +08:00
const instanceId = parsedMessage . core . body . props . systemProps . html5InstanceId ;
2020-11-19 04:31:36 +08:00
2020-12-15 09:55:57 +08:00
Logger . warn ( ` ${ eventName } (name= ${ parsedMessage . core . body . props . meetingProp . name } ) received with meetingInstance: ${ instanceId } -- this is instance: ${ this . instanceId } ` ) ;
2020-11-19 04:31:36 +08:00
2020-11-24 23:13:09 +08:00
if ( instanceId === this . instanceId ) {
this . mettingsQueues [ newIntId ] = new MeetingMessageQueue ( this . emitter , async , this . redisDebugEnabled ) ;
2020-11-19 04:31:36 +08:00
} else {
2020-11-19 23:31:24 +08:00
// Logger.error('THIS NODEJS ' + this.instanceId + ' IS **NOT** PROCESSING EVENTS FOR THIS MEETING ' + instanceId)
2020-11-19 04:31:36 +08:00
}
2017-10-12 10:00:28 +08:00
}
2020-12-18 06:02:38 +08:00
// if (channel !== this.customRedisChannel && queueId in this.mettingsQueues) {
// Logger.error(`Consider routing ${eventName} to ${this.customRedisChannel}` );
// // Logger.error(`Consider routing ${eventName} to ${this.customRedisChannel}` + message);
// }
2020-12-12 09:45:38 +08:00
if ( channel === this . customRedisChannel || queueId in this . mettingsQueues ) {
2020-11-19 04:31:36 +08:00
this . mettingsQueues [ queueId ] . add ( {
pattern ,
channel ,
eventName ,
parsedMessage ,
} ) ;
}
2017-10-12 10:00:28 +08:00
}
destroyMeetingQueue ( id ) {
delete this . mettingsQueues [ id ] ;
}
on ( ... args ) {
return this . emitter . on ( ... args ) ;
}
publishVoiceMessage ( channel , eventName , voiceConf , payload ) {
const header = {
name : eventName ,
voiceConf ,
} ;
const envelope = makeEnvelope ( channel , eventName , header , payload ) ;
2017-10-13 03:07:02 +08:00
return this . pub . publish ( channel , envelope , RedisPubSub . handlePublishError ) ;
2017-10-12 10:00:28 +08:00
}
publishSystemMessage ( channel , eventName , payload ) {
const header = {
name : eventName ,
} ;
const envelope = makeEnvelope ( channel , eventName , header , payload ) ;
2017-10-13 03:07:02 +08:00
return this . pub . publish ( channel , envelope , RedisPubSub . handlePublishError ) ;
2017-10-12 10:00:28 +08:00
}
publishMeetingMessage ( channel , eventName , meetingId , payload ) {
const header = {
name : eventName ,
meetingId ,
} ;
const envelope = makeEnvelope ( channel , eventName , header , payload ) ;
2017-10-13 03:07:02 +08:00
return this . pub . publish ( channel , envelope , RedisPubSub . handlePublishError ) ;
2017-10-12 10:00:28 +08:00
}
publishUserMessage ( channel , eventName , meetingId , userId , payload ) {
const header = {
name : eventName ,
meetingId ,
userId ,
} ;
2020-05-22 22:45:28 +08:00
if ( ! meetingId || ! userId ) {
2020-05-24 20:22:10 +08:00
Logger . warn ( ` Publishing ${ eventName } with potentially missing data userId= ${ userId } meetingId= ${ meetingId } ` ) ;
2020-05-22 22:45:28 +08:00
}
2019-04-06 06:32:21 +08:00
const envelope = makeEnvelope ( channel , eventName , header , payload , { meetingId , userId } ) ;
2017-10-12 10:00:28 +08:00
2017-10-13 03:07:02 +08:00
return this . pub . publish ( channel , envelope , RedisPubSub . handlePublishError ) ;
2017-10-12 10:00:28 +08:00
}
}
2017-10-13 03:07:02 +08:00
const RedisPubSubSingleton = new RedisPubSub ( ) ;
2017-10-12 10:00:28 +08:00
Meteor . startup ( ( ) => {
2018-01-08 08:24:05 +08:00
const REDIS _CONFIG = Meteor . settings . private . redis ;
2017-10-12 10:00:28 +08:00
RedisPubSubSingleton . updateConfig ( REDIS _CONFIG ) ;
RedisPubSubSingleton . init ( ) ;
} ) ;
export default RedisPubSubSingleton ;