DocumentUpdaterManager.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. const request = require('request')
  2. const _ = require('lodash')
  3. const OError = require('@overleaf/o-error')
  4. const logger = require('@overleaf/logger')
  5. const settings = require('@overleaf/settings')
  6. const metrics = require('@overleaf/metrics')
  7. const {
  8. ClientRequestedMissingOpsError,
  9. DocumentUpdaterRequestFailedError,
  10. NullBytesInOpError,
  11. UpdateTooLargeError,
  12. } = require('./Errors')
  13. const rclient = require('@overleaf/redis-wrapper').createClient(
  14. settings.redis.documentupdater
  15. )
  16. const Keys = settings.redis.documentupdater.key_schema
  17. const DocumentUpdaterManager = {
  18. getDocument(projectId, docId, fromVersion, callback) {
  19. const timer = new metrics.Timer('get-document')
  20. const url = `${settings.apis.documentupdater.url}/project/${projectId}/doc/${docId}?fromVersion=${fromVersion}`
  21. logger.debug(
  22. { projectId, docId, fromVersion },
  23. 'getting doc from document updater'
  24. )
  25. request.get(url, function (err, res, body) {
  26. timer.done()
  27. if (err) {
  28. OError.tag(err, 'error getting doc from doc updater')
  29. return callback(err)
  30. }
  31. if (res.statusCode >= 200 && res.statusCode < 300) {
  32. logger.debug(
  33. { projectId, docId },
  34. 'got doc from document document updater'
  35. )
  36. try {
  37. body = JSON.parse(body)
  38. } catch (error) {
  39. OError.tag(error, 'error parsing doc updater response')
  40. return callback(error)
  41. }
  42. body = body || {}
  43. callback(
  44. null,
  45. body.lines,
  46. body.version,
  47. body.ranges,
  48. body.ops,
  49. body.ttlInS
  50. )
  51. } else if (res.statusCode === 422 && body?.firstVersionInRedis) {
  52. callback(new ClientRequestedMissingOpsError(422, body))
  53. } else if ([404, 422].includes(res.statusCode)) {
  54. callback(new ClientRequestedMissingOpsError(res.statusCode))
  55. } else {
  56. callback(
  57. new DocumentUpdaterRequestFailedError('getDocument', res.statusCode)
  58. )
  59. }
  60. })
  61. },
  62. checkDocument(projectId, docId, callback) {
  63. // in this call fromVersion = -1 means get document without docOps
  64. DocumentUpdaterManager.getDocument(projectId, docId, -1, callback)
  65. },
  66. flushProjectToMongoAndDelete(projectId, callback) {
  67. // this method is called when the last connected user leaves the project
  68. logger.debug({ projectId }, 'deleting project from document updater')
  69. const timer = new metrics.Timer('delete.mongo.project')
  70. // flush the project in the background when all users have left
  71. const url =
  72. `${settings.apis.documentupdater.url}/project/${projectId}?background=true` +
  73. (settings.shutDownInProgress ? '&shutdown=true' : '')
  74. request.del(url, function (err, res) {
  75. timer.done()
  76. if (err) {
  77. OError.tag(err, 'error deleting project from document updater')
  78. callback(err)
  79. } else if (res.statusCode >= 200 && res.statusCode < 300) {
  80. logger.debug({ projectId }, 'deleted project from document updater')
  81. callback(null)
  82. } else {
  83. callback(
  84. new DocumentUpdaterRequestFailedError(
  85. 'flushProjectToMongoAndDelete',
  86. res.statusCode
  87. )
  88. )
  89. }
  90. })
  91. },
  92. _getPendingUpdateListKey() {
  93. const shard = _.random(0, settings.pendingUpdateListShardCount - 1)
  94. if (shard === 0) {
  95. return 'pending-updates-list'
  96. } else {
  97. return `pending-updates-list-${shard}`
  98. }
  99. },
  100. queueChange(projectId, docId, change, callback) {
  101. const allowedKeys = [
  102. 'doc',
  103. 'op',
  104. 'v',
  105. 'dupIfSource',
  106. 'meta',
  107. 'lastV',
  108. 'hash',
  109. ]
  110. change = _.pick(change, allowedKeys)
  111. const jsonChange = JSON.stringify(change)
  112. if (jsonChange.indexOf('\u0000') !== -1) {
  113. // memory corruption check
  114. return callback(new NullBytesInOpError(jsonChange))
  115. }
  116. const updateSize = jsonChange.length
  117. if (updateSize > settings.maxUpdateSize) {
  118. return callback(new UpdateTooLargeError(updateSize))
  119. }
  120. // record metric for each update added to queue
  121. metrics.summary('redis.pendingUpdates', updateSize, { status: 'push' })
  122. const docKey = `${projectId}:${docId}`
  123. // Push onto pendingUpdates for doc_id first, because once the doc updater
  124. // gets an entry on pending-updates-list, it starts processing.
  125. rclient.rpush(
  126. Keys.pendingUpdates({ doc_id: docId }),
  127. jsonChange,
  128. function (error) {
  129. if (error) {
  130. error = new OError('error pushing update into redis').withCause(error)
  131. return callback(error)
  132. }
  133. const queueKey = DocumentUpdaterManager._getPendingUpdateListKey()
  134. rclient.rpush(queueKey, docKey, function (error) {
  135. if (error) {
  136. error = new OError('error pushing doc_id into redis')
  137. .withInfo({ queueKey })
  138. .withCause(error)
  139. }
  140. callback(error)
  141. })
  142. }
  143. )
  144. },
  145. }
  146. module.exports = DocumentUpdaterManager