ChannelManager.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. const logger = require('logger-sharelatex')
  2. const metrics = require('metrics-sharelatex')
  3. const settings = require('settings-sharelatex')
  4. const OError = require('@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. module.exports = {
  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. const p = rclient.subscribe(channel)
  23. p.finally(function () {
  24. if (clientChannelMap.get(channel) === subscribePromise) {
  25. clientChannelMap.delete(channel)
  26. }
  27. })
  28. .then(function () {
  29. logger.log({ channel }, 'subscribed to channel')
  30. metrics.inc(`subscribe.${baseChannel}`)
  31. })
  32. .catch(function (err) {
  33. logger.error({ channel, err }, 'failed to subscribe to channel')
  34. metrics.inc(`subscribe.failed.${baseChannel}`)
  35. // add context for the stack-trace at the call-site
  36. OError.tag(err, 'failed to subscribe to channel', { channel })
  37. })
  38. return p
  39. }
  40. const pendingActions = clientChannelMap.get(channel) || Promise.resolve()
  41. const subscribePromise = pendingActions.then(
  42. actualSubscribe,
  43. actualSubscribe
  44. )
  45. clientChannelMap.set(channel, subscribePromise)
  46. logger.log({ channel }, 'planned to subscribe to channel')
  47. return subscribePromise
  48. },
  49. unsubscribe(rclient, baseChannel, id) {
  50. const clientChannelMap = this.getClientMapEntry(rclient)
  51. const channel = `${baseChannel}:${id}`
  52. const actualUnsubscribe = function () {
  53. // unsubscribe is happening in the background, it should not reject
  54. return rclient
  55. .unsubscribe(channel)
  56. .finally(function () {
  57. if (clientChannelMap.get(channel) === unsubscribePromise) {
  58. clientChannelMap.delete(channel)
  59. }
  60. })
  61. .then(function () {
  62. logger.log({ channel }, 'unsubscribed from channel')
  63. metrics.inc(`unsubscribe.${baseChannel}`)
  64. })
  65. .catch(function (err) {
  66. logger.error({ channel, err }, 'unsubscribed from channel')
  67. metrics.inc(`unsubscribe.failed.${baseChannel}`)
  68. })
  69. }
  70. const pendingActions = clientChannelMap.get(channel) || Promise.resolve()
  71. const unsubscribePromise = pendingActions.then(
  72. actualUnsubscribe,
  73. actualUnsubscribe
  74. )
  75. clientChannelMap.set(channel, unsubscribePromise)
  76. logger.log({ channel }, 'planned to unsubscribe from channel')
  77. return unsubscribePromise
  78. },
  79. publish(rclient, baseChannel, id, data) {
  80. let channel
  81. metrics.summary(`redis.publish.${baseChannel}`, data.length)
  82. if (id === 'all' || !settings.publishOnIndividualChannels) {
  83. channel = baseChannel
  84. } else {
  85. channel = `${baseChannel}:${id}`
  86. }
  87. // we publish on a different client to the subscribe, so we can't
  88. // check for the channel existing here
  89. rclient.publish(channel, data)
  90. }
  91. }