WebsocketLoadBalancer.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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. shouldDisconnectClient(client, message) {
  26. const userId = client.ol_context.user_id
  27. if (message?.message === 'userRemovedFromProject') {
  28. if (message?.payload?.includes(userId)) {
  29. return true
  30. }
  31. }
  32. return false
  33. },
  34. emitToRoom(roomId, message, ...payload) {
  35. if (!roomId) {
  36. logger.warn(
  37. { message, payload },
  38. 'no room_id provided, ignoring emitToRoom'
  39. )
  40. return
  41. }
  42. const data = JSON.stringify({
  43. room_id: roomId,
  44. message,
  45. payload,
  46. })
  47. logger.debug(
  48. { roomId, message, payload, length: data.length },
  49. 'emitting to room'
  50. )
  51. this.rclientPubList.map(rclientPub =>
  52. ChannelManager.publish(rclientPub, 'editor-events', roomId, data)
  53. )
  54. },
  55. emitToAll(message, ...payload) {
  56. this.emitToRoom('all', message, ...payload)
  57. },
  58. listenForEditorEvents(io) {
  59. logger.debug(
  60. { rclients: this.rclientSubList.length },
  61. 'listening for editor events'
  62. )
  63. for (const rclientSub of this.rclientSubList) {
  64. rclientSub.subscribe('editor-events')
  65. rclientSub.on('message', function (channel, message) {
  66. if (Settings.debugEvents > 0) {
  67. EventLogger.debugEvent(channel, message)
  68. }
  69. WebsocketLoadBalancer._processEditorEvent(io, channel, message)
  70. })
  71. }
  72. this.handleRoomUpdates(this.rclientSubList)
  73. },
  74. handleRoomUpdates(rclientSubList) {
  75. const roomEvents = RoomManager.eventSource()
  76. roomEvents.on('project-active', function (projectId) {
  77. const subscribePromises = rclientSubList.map(rclient =>
  78. ChannelManager.subscribe(rclient, 'editor-events', projectId)
  79. )
  80. RoomManager.emitOnCompletion(
  81. subscribePromises,
  82. `project-subscribed-${projectId}`
  83. )
  84. })
  85. roomEvents.on('project-empty', projectId =>
  86. rclientSubList.map(rclient =>
  87. ChannelManager.unsubscribe(rclient, 'editor-events', projectId)
  88. )
  89. )
  90. },
  91. _processEditorEvent(io, channel, message) {
  92. SafeJsonParse.parse(message, function (error, message) {
  93. if (error) {
  94. logger.error({ err: error, channel }, 'error parsing JSON')
  95. return
  96. }
  97. if (message.room_id === 'all') {
  98. io.sockets.emit(message.message, ...message.payload)
  99. } else if (
  100. message.message === 'clientTracking.refresh' &&
  101. message.room_id
  102. ) {
  103. const clientList = io.sockets.clients(message.room_id)
  104. logger.debug(
  105. {
  106. channel,
  107. message: message.message,
  108. roomId: message.room_id,
  109. messageId: message._id,
  110. socketIoClients: clientList.map(client => client.id),
  111. },
  112. 'refreshing client list'
  113. )
  114. for (const client of clientList) {
  115. ConnectedUsersManager.refreshClient(message.room_id, client.publicId)
  116. }
  117. } else if (message.room_id) {
  118. if (message._id && Settings.checkEventOrder) {
  119. const status = EventLogger.checkEventOrder(
  120. 'editor-events',
  121. message._id,
  122. message
  123. )
  124. if (status === 'duplicate') {
  125. return // skip duplicate events
  126. }
  127. }
  128. const isRestrictedMessage =
  129. !RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message)
  130. // send messages only to unique clients (due to duplicate entries in io.sockets.clients)
  131. const clientList = io.sockets
  132. .clients(message.room_id)
  133. .filter(
  134. client =>
  135. !(isRestrictedMessage && client.ol_context.is_restricted_user)
  136. )
  137. // avoid unnecessary work if no clients are connected
  138. if (clientList.length === 0) {
  139. return
  140. }
  141. logger.debug(
  142. {
  143. channel,
  144. message: message.message,
  145. roomId: message.room_id,
  146. messageId: message._id,
  147. socketIoClients: clientList.map(client => client.id),
  148. },
  149. 'distributing event to clients'
  150. )
  151. const seen = new Map()
  152. for (const client of clientList) {
  153. if (!seen.has(client.id)) {
  154. seen.set(client.id, true)
  155. if (WebsocketLoadBalancer.shouldDisconnectClient(client, message)) {
  156. logger.debug(
  157. {
  158. message,
  159. userId: client?.ol_context?.user_id,
  160. projectId: client?.ol_context?.project_id,
  161. },
  162. 'disconnecting client'
  163. )
  164. client.disconnect()
  165. } else {
  166. client.emit(message.message, ...message.payload)
  167. }
  168. }
  169. }
  170. } else if (message.health_check) {
  171. logger.debug(
  172. { message },
  173. 'got health check message in editor events channel'
  174. )
  175. HealthCheckManager.check(channel, message.key)
  176. }
  177. })
  178. },
  179. }