ChannelManager.js 3.4 KB

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