RealTimeRedisManager.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. /* eslint-disable
  2. no-unused-vars,
  3. */
  4. // TODO: This file was created by bulk-decaffeinate.
  5. // Fix any style issues and re-enable lint.
  6. /*
  7. * decaffeinate suggestions:
  8. * DS101: Remove unnecessary use of Array.from
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * DS207: Consider shorter variations of null checks
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. const Settings = require('@overleaf/settings')
  14. const { promisifyAll } = require('@overleaf/promise-utils')
  15. const rclient = require('@overleaf/redis-wrapper').createClient(
  16. Settings.redis.documentupdater
  17. )
  18. const pubsubClient = require('@overleaf/redis-wrapper').createClient(
  19. Settings.redis.pubsub
  20. )
  21. const Keys = Settings.redis.documentupdater.key_schema
  22. const logger = require('@overleaf/logger')
  23. const os = require('node:os')
  24. const crypto = require('node:crypto')
  25. const metrics = require('./Metrics')
  26. const HOST = os.hostname()
  27. const RND = crypto.randomBytes(4).toString('hex') // generate a random key for this process
  28. let COUNT = 0
  29. const MAX_OPS_PER_ITERATION = 8 // process a limited number of ops for safety
  30. const RealTimeRedisManager = {
  31. getPendingUpdatesForDoc(docId, callback) {
  32. // Make sure that this MULTI operation only operates on doc
  33. // specific keys, i.e. keys that have the doc id in curly braces.
  34. // The curly braces identify a hash key for Redis and ensures that
  35. // the MULTI's operations are all done on the same node in a
  36. // cluster environment.
  37. const multi = rclient.multi()
  38. multi.llen(Keys.pendingUpdates({ doc_id: docId }))
  39. multi.lrange(
  40. Keys.pendingUpdates({ doc_id: docId }),
  41. 0,
  42. MAX_OPS_PER_ITERATION - 1
  43. )
  44. multi.ltrim(
  45. Keys.pendingUpdates({ doc_id: docId }),
  46. MAX_OPS_PER_ITERATION,
  47. -1
  48. )
  49. multi.exec(function (error, replys) {
  50. if (error != null) {
  51. return callback(error)
  52. }
  53. const [llen, jsonUpdates, _trimResult] = replys
  54. metrics.histogram(
  55. 'redis.pendingUpdates.llen',
  56. llen,
  57. [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 50, 75, 100]
  58. )
  59. for (const jsonUpdate of jsonUpdates) {
  60. // record metric for each update removed from queue
  61. metrics.summary('redis.pendingUpdates', jsonUpdate.length, {
  62. status: 'pop',
  63. })
  64. }
  65. const updates = []
  66. for (const jsonUpdate of jsonUpdates) {
  67. let update
  68. try {
  69. update = JSON.parse(jsonUpdate)
  70. } catch (e) {
  71. return callback(e)
  72. }
  73. updates.push(update)
  74. }
  75. return callback(error, updates)
  76. })
  77. },
  78. getUpdatesLength(docId, callback) {
  79. rclient.llen(Keys.pendingUpdates({ doc_id: docId }), callback)
  80. },
  81. sendCanaryAppliedOp({ projectId, docId, op }) {
  82. const ack = JSON.stringify({ v: op.v, doc: docId }).length
  83. // Updates with op.dup===true will not get sent to other clients, they only get acked.
  84. const broadcast = op.dup ? 0 : JSON.stringify(op).length
  85. const payload = JSON.stringify({
  86. message: 'canary-applied-op',
  87. payload: {
  88. ack,
  89. broadcast,
  90. docId,
  91. projectId,
  92. source: op.meta.source,
  93. },
  94. })
  95. // Publish on the editor-events channel of the project as real-time already listens to that before completing the connection startup.
  96. // publish on separate channels for individual projects and docs when
  97. // configured (needs realtime to be configured for this too).
  98. if (Settings.publishOnIndividualChannels) {
  99. return pubsubClient.publish(`editor-events:${projectId}`, payload)
  100. } else {
  101. return pubsubClient.publish('editor-events', payload)
  102. }
  103. },
  104. sendData(data) {
  105. // create a unique message id using a counter
  106. const messageId = `doc:${HOST}:${RND}-${COUNT++}`
  107. if (data != null) {
  108. data._id = messageId
  109. }
  110. const blob = JSON.stringify(data)
  111. metrics.summary('redis.publish.applied-ops', blob.length)
  112. // publish on separate channels for individual projects and docs when
  113. // configured (needs realtime to be configured for this too).
  114. if (Settings.publishOnIndividualChannels) {
  115. return pubsubClient.publish(`applied-ops:${data.doc_id}`, blob)
  116. } else {
  117. return pubsubClient.publish('applied-ops', blob)
  118. }
  119. },
  120. }
  121. module.exports = RealTimeRedisManager
  122. module.exports.promises = promisifyAll(RealTimeRedisManager, {
  123. without: ['sendCanaryAppliedOp', 'sendData'],
  124. })