WebsocketLoadBalancer.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import Settings from '@overleaf/settings'
  2. import logger from '@overleaf/logger'
  3. import Metrics from '@overleaf/metrics'
  4. import RedisClientManager from './RedisClientManager.js'
  5. import SafeJsonParse from './SafeJsonParse.js'
  6. import EventLogger from './EventLogger.js'
  7. import HealthCheckManager from './HealthCheckManager.js'
  8. import RoomManager from './RoomManager.js'
  9. import ChannelManager from './ChannelManager.js'
  10. import ConnectedUsersManager from './ConnectedUsersManager.js'
  11. const RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST = [
  12. 'otUpdateApplied',
  13. 'otUpdateError',
  14. 'joinDoc',
  15. 'reciveNewDoc',
  16. 'reciveNewFile',
  17. 'reciveNewFolder',
  18. 'reciveEntityMove',
  19. 'reciveEntityRename',
  20. 'removeEntity',
  21. 'accept-changes',
  22. 'projectNameUpdated',
  23. 'rootDocUpdated',
  24. 'toggle-track-changes',
  25. 'projectRenamedOrDeletedByExternalSource',
  26. ]
  27. const BANDWIDTH_BUCKETS = [0]
  28. // 64 bytes ... 8MB
  29. for (let i = 5; i <= 22; i++) {
  30. BANDWIDTH_BUCKETS.push(2 << i)
  31. }
  32. let WebsocketLoadBalancer
  33. export default WebsocketLoadBalancer = {
  34. rclientPubList: RedisClientManager.createClientList(Settings.redis.pubsub),
  35. rclientSubList: RedisClientManager.createClientList(Settings.redis.pubsub),
  36. shouldDisconnectClient(client, message) {
  37. const userId = client.ol_context.user_id
  38. if (message?.message === 'userRemovedFromProject') {
  39. if (message?.payload?.includes(userId)) {
  40. return true
  41. }
  42. } else if (message?.message === 'project:publicAccessLevel:changed') {
  43. const [info] = message.payload
  44. if (
  45. info.newAccessLevel === 'private' &&
  46. !client.ol_context.is_invited_member
  47. ) {
  48. return true
  49. }
  50. } else if (message?.message === 'project:collaboratorAccessLevel:changed') {
  51. const changedUserId = message.payload[0].userId
  52. return userId === changedUserId
  53. }
  54. return false
  55. },
  56. emitToRoom(roomId, message, ...payload) {
  57. if (!roomId) {
  58. logger.warn(
  59. { message, payload },
  60. 'no room_id provided, ignoring emitToRoom'
  61. )
  62. return
  63. }
  64. const data = JSON.stringify({
  65. room_id: roomId,
  66. message,
  67. payload,
  68. })
  69. logger.debug(
  70. { roomId, message, payload, length: data.length },
  71. 'emitting to room'
  72. )
  73. this.rclientPubList.map(rclientPub =>
  74. ChannelManager.publish(rclientPub, 'editor-events', roomId, data)
  75. )
  76. },
  77. emitToAll(message, ...payload) {
  78. this.emitToRoom('all', message, ...payload)
  79. },
  80. listenForEditorEvents(io) {
  81. logger.debug(
  82. { rclients: this.rclientSubList.length },
  83. 'listening for editor events'
  84. )
  85. for (const rclientSub of this.rclientSubList) {
  86. rclientSub.subscribe('editor-events')
  87. rclientSub.on('message', function (channel, message) {
  88. if (Settings.debugEvents > 0) {
  89. EventLogger.debugEvent(channel, message)
  90. }
  91. WebsocketLoadBalancer._processEditorEvent(io, channel, message)
  92. })
  93. }
  94. this.handleRoomUpdates(this.rclientSubList)
  95. },
  96. handleRoomUpdates(rclientSubList) {
  97. const roomEvents = RoomManager.eventSource()
  98. roomEvents.on('project-active', function (projectId) {
  99. const subscribePromises = rclientSubList.map(rclient =>
  100. ChannelManager.subscribe(rclient, 'editor-events', projectId)
  101. )
  102. RoomManager.emitOnCompletion(
  103. subscribePromises,
  104. `project-subscribed-${projectId}`
  105. )
  106. })
  107. roomEvents.on('project-empty', projectId =>
  108. rclientSubList.map(rclient =>
  109. ChannelManager.unsubscribe(rclient, 'editor-events', projectId)
  110. )
  111. )
  112. },
  113. _processEditorEvent(io, channel, message) {
  114. SafeJsonParse.parse(message, function (error, message) {
  115. if (error) {
  116. logger.error({ err: error, channel }, 'error parsing JSON')
  117. return
  118. }
  119. if (message.room_id === 'all') {
  120. io.sockets.emit(message.message, ...message.payload)
  121. } else if (
  122. message.message === 'clientTracking.refresh' &&
  123. message.room_id
  124. ) {
  125. const clientList = io.sockets.clients(message.room_id)
  126. logger.debug(
  127. {
  128. channel,
  129. message: message.message,
  130. roomId: message.room_id,
  131. messageId: message._id,
  132. socketIoClients: clientList.map(client => client.id),
  133. },
  134. 'refreshing client list'
  135. )
  136. for (const client of clientList) {
  137. ConnectedUsersManager.refreshClient(message.room_id, client.publicId)
  138. }
  139. } else if (message.message === 'canary-applied-op') {
  140. const { ack, broadcast, source, projectId, docId } = message.payload
  141. const estimateBandwidth = (room, path) => {
  142. const seen = new Set()
  143. for (const client of io.sockets.clients(room)) {
  144. if (seen.has(client.id)) continue
  145. seen.add(client.id)
  146. let v = client.id === source ? ack : broadcast
  147. if (v === 0) {
  148. // Acknowledgements with update.dup===true will not get sent to other clients.
  149. continue
  150. }
  151. v += `5:::{"name":"otUpdateApplied","args":[]}`.length
  152. Metrics.histogram(
  153. 'estimated-applied-ops-bandwidth',
  154. v,
  155. BANDWIDTH_BUCKETS,
  156. { path }
  157. )
  158. }
  159. }
  160. estimateBandwidth(projectId, 'per-project')
  161. estimateBandwidth(docId, 'per-doc')
  162. } else if (message.room_id) {
  163. if (message._id && Settings.checkEventOrder) {
  164. const status = EventLogger.checkEventOrder(
  165. 'editor-events',
  166. message._id,
  167. message
  168. )
  169. if (status === 'duplicate') {
  170. return // skip duplicate events
  171. }
  172. }
  173. const isRestrictedMessage =
  174. !RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST.includes(message.message)
  175. // send messages only to unique clients (due to duplicate entries in io.sockets.clients)
  176. const clientList = io.sockets.clients(message.room_id)
  177. // avoid unnecessary work if no clients are connected
  178. if (clientList.length === 0) {
  179. return
  180. }
  181. logger.debug(
  182. {
  183. channel,
  184. message: message.message,
  185. roomId: message.room_id,
  186. messageId: message._id,
  187. socketIoClients: clientList.map(client => client.id),
  188. },
  189. 'distributing event to clients'
  190. )
  191. const seen = new Map()
  192. for (const client of clientList) {
  193. if (!seen.has(client.id)) {
  194. seen.set(client.id, true)
  195. if (WebsocketLoadBalancer.shouldDisconnectClient(client, message)) {
  196. logger.debug(
  197. {
  198. message,
  199. userId: client?.ol_context?.user_id,
  200. projectId: client?.ol_context?.project_id,
  201. },
  202. 'disconnecting client'
  203. )
  204. if (
  205. message?.message !== 'project:collaboratorAccessLevel:changed'
  206. ) {
  207. client.emit('project:access:revoked')
  208. }
  209. client.disconnect()
  210. } else {
  211. if (isRestrictedMessage && client.ol_context.is_restricted_user) {
  212. // hide restricted message
  213. logger.debug(
  214. {
  215. message,
  216. clientId: client.id,
  217. userId: client.ol_context.user_id,
  218. projectId: client.ol_context.project_id,
  219. },
  220. 'hiding restricted message from client'
  221. )
  222. } else {
  223. client.emit(message.message, ...message.payload)
  224. }
  225. }
  226. }
  227. }
  228. } else if (message.health_check) {
  229. logger.debug(
  230. { message },
  231. 'got health check message in editor events channel'
  232. )
  233. HealthCheckManager.check(channel, message.key)
  234. }
  235. })
  236. },
  237. }