WebsocketLoadBalancer.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. const Settings = require('@overleaf/settings')
  2. const logger = require('@overleaf/logger')
  3. const RedisClientManager = require('./RedisClientManager')
  4. const SafeJsonParse = require('./SafeJsonParse')
  5. const EventLogger = require('./EventLogger')
  6. const HealthCheckManager = require('./HealthCheckManager')
  7. const RoomManager = require('./RoomManager')
  8. const ChannelManager = require('./ChannelManager')
  9. const ConnectedUsersManager = require('./ConnectedUsersManager')
  10. const RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST = [
  11. 'connectionAccepted',
  12. 'otUpdateApplied',
  13. 'otUpdateError',
  14. 'joinDoc',
  15. 'reciveNewDoc',
  16. 'reciveNewFile',
  17. 'reciveNewFolder',
  18. 'removeEntity',
  19. 'accept-changes',
  20. ]
  21. let WebsocketLoadBalancer
  22. module.exports = WebsocketLoadBalancer = {
  23. rclientPubList: RedisClientManager.createClientList(Settings.redis.pubsub),
  24. rclientSubList: RedisClientManager.createClientList(Settings.redis.pubsub),
  25. emitToRoom(roomId, message, ...payload) {
  26. if (!roomId) {
  27. logger.warn(
  28. { message, payload },
  29. 'no room_id provided, ignoring emitToRoom'
  30. )
  31. return
  32. }
  33. const data = JSON.stringify({
  34. room_id: roomId,
  35. message,
  36. payload,
  37. })
  38. logger.debug(
  39. { roomId, message, payload, length: data.length },
  40. 'emitting to room'
  41. )
  42. this.rclientPubList.map(rclientPub =>
  43. ChannelManager.publish(rclientPub, 'editor-events', roomId, data)
  44. )
  45. },
  46. emitToAll(message, ...payload) {
  47. this.emitToRoom('all', message, ...payload)
  48. },
  49. listenForEditorEvents(io) {
  50. logger.debug(
  51. { rclients: this.rclientSubList.length },
  52. 'listening for editor events'
  53. )
  54. for (const rclientSub of this.rclientSubList) {
  55. rclientSub.subscribe('editor-events')
  56. rclientSub.on('message', function (channel, message) {
  57. if (Settings.debugEvents > 0) {
  58. EventLogger.debugEvent(channel, message)
  59. }
  60. WebsocketLoadBalancer._processEditorEvent(io, channel, message)
  61. })
  62. }
  63. this.handleRoomUpdates(this.rclientSubList)
  64. },
  65. handleRoomUpdates(rclientSubList) {
  66. const roomEvents = RoomManager.eventSource()
  67. roomEvents.on('project-active', function (projectId) {
  68. const subscribePromises = rclientSubList.map(rclient =>
  69. ChannelManager.subscribe(rclient, 'editor-events', projectId)
  70. )
  71. RoomManager.emitOnCompletion(
  72. subscribePromises,
  73. `project-subscribed-${projectId}`
  74. )
  75. })
  76. roomEvents.on('project-empty', projectId =>
  77. rclientSubList.map(rclient =>
  78. ChannelManager.unsubscribe(rclient, 'editor-events', projectId)
  79. )
  80. )
  81. },
  82. _processEditorEvent(io, channel, message) {
  83. SafeJsonParse.parse(message, function (error, message) {
  84. if (error) {
  85. logger.error({ err: error, channel }, 'error parsing JSON')
  86. return
  87. }
  88. if (message.room_id === 'all') {
  89. io.sockets.emit(message.message, ...message.payload)
  90. } else if (
  91. message.message === 'clientTracking.refresh' &&
  92. message.room_id
  93. ) {
  94. const clientList = io.sockets.clients(message.room_id)
  95. logger.debug(
  96. {
  97. channel,
  98. message: message.message,
  99. roomId: message.room_id,
  100. messageId: message._id,
  101. socketIoClients: clientList.map(client => client.id),
  102. },
  103. 'refreshing client list'
  104. )
  105. for (const client of clientList) {
  106. ConnectedUsersManager.refreshClient(message.room_id, client.publicId)
  107. }
  108. } else if (message.room_id) {
  109. if (message._id && Settings.checkEventOrder) {
  110. const status = EventLogger.checkEventOrder(
  111. 'editor-events',
  112. message._id,
  113. message
  114. )
  115. if (status === 'duplicate') {
  116. return // skip duplicate events
  117. }
  118. }
  119. const isRestrictedMessage =
  120. !RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message)
  121. // send messages only to unique clients (due to duplicate entries in io.sockets.clients)
  122. const clientList = io.sockets
  123. .clients(message.room_id)
  124. .filter(
  125. client =>
  126. !(isRestrictedMessage && client.ol_context.is_restricted_user)
  127. )
  128. // avoid unnecessary work if no clients are connected
  129. if (clientList.length === 0) {
  130. return
  131. }
  132. logger.debug(
  133. {
  134. channel,
  135. message: message.message,
  136. roomId: message.room_id,
  137. messageId: message._id,
  138. socketIoClients: clientList.map(client => client.id),
  139. },
  140. 'distributing event to clients'
  141. )
  142. const seen = new Map()
  143. for (const client of clientList) {
  144. if (!seen.has(client.id)) {
  145. seen.set(client.id, true)
  146. client.emit(message.message, ...message.payload)
  147. }
  148. }
  149. } else if (message.health_check) {
  150. logger.debug(
  151. { message },
  152. 'got health check message in editor events channel'
  153. )
  154. HealthCheckManager.check(channel, message.key)
  155. }
  156. })
  157. },
  158. }