ChannelManager.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import logger from '@overleaf/logger'
  2. import metrics from '@overleaf/metrics'
  3. import settings from '@overleaf/settings'
  4. import OError from '@overleaf/o-error'
  5. const ClientMap = new Map() // for each redis client, store a Map of subscribed channels (channelname -> subscribe promise)
  6. // Manage redis pubsub subscriptions for individual projects and docs, ensuring
  7. // that we never subscribe to a channel multiple times. The socket.io side is
  8. // handled by RoomManager.
  9. export default {
  10. getClientMapEntry(rclient) {
  11. // return the per-client channel map if it exists, otherwise create and
  12. // return an empty map for the client.
  13. return (
  14. ClientMap.get(rclient) || ClientMap.set(rclient, new Map()).get(rclient)
  15. )
  16. },
  17. subscribe(rclient, baseChannel, id) {
  18. const clientChannelMap = this.getClientMapEntry(rclient)
  19. const channel = `${baseChannel}:${id}`
  20. const actualSubscribe = function () {
  21. // subscribe is happening in the foreground and it should reject
  22. return rclient
  23. .subscribe(channel)
  24. .finally(function () {
  25. if (clientChannelMap.get(channel) === subscribePromise) {
  26. clientChannelMap.delete(channel)
  27. }
  28. })
  29. .then(function () {
  30. logger.debug({ channel }, 'subscribed to channel')
  31. metrics.inc(`subscribe.${baseChannel}`)
  32. })
  33. .catch(function (err) {
  34. logger.error({ channel, err }, 'failed to subscribe to channel')
  35. metrics.inc(`subscribe.failed.${baseChannel}`)
  36. // add context for the stack-trace at the call-site
  37. throw new OError('failed to subscribe to channel', {
  38. channel,
  39. }).withCause(err)
  40. })
  41. }
  42. const pendingActions = clientChannelMap.get(channel) || Promise.resolve()
  43. const subscribePromise = pendingActions.then(
  44. actualSubscribe,
  45. actualSubscribe
  46. )
  47. clientChannelMap.set(channel, subscribePromise)
  48. logger.debug({ channel }, 'planned to subscribe to channel')
  49. return subscribePromise
  50. },
  51. unsubscribe(rclient, baseChannel, id) {
  52. const clientChannelMap = this.getClientMapEntry(rclient)
  53. const channel = `${baseChannel}:${id}`
  54. const actualUnsubscribe = function () {
  55. // unsubscribe is happening in the background, it should not reject
  56. return rclient
  57. .unsubscribe(channel)
  58. .finally(function () {
  59. if (clientChannelMap.get(channel) === unsubscribePromise) {
  60. clientChannelMap.delete(channel)
  61. }
  62. })
  63. .then(function () {
  64. logger.debug({ channel }, 'unsubscribed from channel')
  65. metrics.inc(`unsubscribe.${baseChannel}`)
  66. })
  67. .catch(function (err) {
  68. logger.error({ channel, err }, 'unsubscribed from channel')
  69. metrics.inc(`unsubscribe.failed.${baseChannel}`)
  70. })
  71. }
  72. const pendingActions = clientChannelMap.get(channel) || Promise.resolve()
  73. const unsubscribePromise = pendingActions.then(
  74. actualUnsubscribe,
  75. actualUnsubscribe
  76. )
  77. clientChannelMap.set(channel, unsubscribePromise)
  78. logger.debug({ channel }, 'planned to unsubscribe from channel')
  79. return unsubscribePromise
  80. },
  81. publish(rclient, baseChannel, id, data) {
  82. let channel
  83. metrics.summary(`redis.publish.${baseChannel}`, data.length)
  84. if (id === 'all' || !settings.publishOnIndividualChannels) {
  85. channel = baseChannel
  86. } else {
  87. channel = `${baseChannel}:${id}`
  88. }
  89. // we publish on a different client to the subscribe, so we can't
  90. // check for the channel existing here
  91. rclient.publish(channel, data)
  92. },
  93. }