DocumentUpdaterManager.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. const request = require('request')
  2. const _ = require('underscore')
  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(null, body.lines, body.version, body.ranges, body.ops)
  44. } else if ([404, 422].includes(res.statusCode)) {
  45. callback(new ClientRequestedMissingOpsError(res.statusCode))
  46. } else {
  47. callback(
  48. new DocumentUpdaterRequestFailedError('getDocument', res.statusCode)
  49. )
  50. }
  51. })
  52. },
  53. checkDocument(projectId, docId, callback) {
  54. // in this call fromVersion = -1 means get document without docOps
  55. DocumentUpdaterManager.getDocument(projectId, docId, -1, callback)
  56. },
  57. flushProjectToMongoAndDelete(projectId, callback) {
  58. // this method is called when the last connected user leaves the project
  59. logger.debug({ projectId }, 'deleting project from document updater')
  60. const timer = new metrics.Timer('delete.mongo.project')
  61. // flush the project in the background when all users have left
  62. const url =
  63. `${settings.apis.documentupdater.url}/project/${projectId}?background=true` +
  64. (settings.shutDownInProgress ? '&shutdown=true' : '')
  65. request.del(url, function (err, res) {
  66. timer.done()
  67. if (err) {
  68. OError.tag(err, 'error deleting project from document updater')
  69. callback(err)
  70. } else if (res.statusCode >= 200 && res.statusCode < 300) {
  71. logger.debug({ projectId }, 'deleted project from document updater')
  72. callback(null)
  73. } else {
  74. callback(
  75. new DocumentUpdaterRequestFailedError(
  76. 'flushProjectToMongoAndDelete',
  77. res.statusCode
  78. )
  79. )
  80. }
  81. })
  82. },
  83. _getPendingUpdateListKey() {
  84. const shard = _.random(0, settings.pendingUpdateListShardCount - 1)
  85. if (shard === 0) {
  86. return 'pending-updates-list'
  87. } else {
  88. return `pending-updates-list-${shard}`
  89. }
  90. },
  91. queueChange(projectId, docId, change, callback) {
  92. const allowedKeys = [
  93. 'doc',
  94. 'op',
  95. 'v',
  96. 'dupIfSource',
  97. 'meta',
  98. 'lastV',
  99. 'hash',
  100. ]
  101. change = _.pick(change, allowedKeys)
  102. const jsonChange = JSON.stringify(change)
  103. if (jsonChange.indexOf('\u0000') !== -1) {
  104. // memory corruption check
  105. return callback(new NullBytesInOpError(jsonChange))
  106. }
  107. const updateSize = jsonChange.length
  108. if (updateSize > settings.maxUpdateSize) {
  109. return callback(new UpdateTooLargeError(updateSize))
  110. }
  111. // record metric for each update added to queue
  112. metrics.summary('redis.pendingUpdates', updateSize, { status: 'push' })
  113. const docKey = `${projectId}:${docId}`
  114. // Push onto pendingUpdates for doc_id first, because once the doc updater
  115. // gets an entry on pending-updates-list, it starts processing.
  116. rclient.rpush(
  117. Keys.pendingUpdates({ doc_id: docId }),
  118. jsonChange,
  119. function (error) {
  120. if (error) {
  121. error = new OError('error pushing update into redis').withCause(error)
  122. return callback(error)
  123. }
  124. const queueKey = DocumentUpdaterManager._getPendingUpdateListKey()
  125. rclient.rpush(queueKey, docKey, function (error) {
  126. if (error) {
  127. error = new OError('error pushing doc_id into redis')
  128. .withInfo({ queueKey })
  129. .withCause(error)
  130. }
  131. callback(error)
  132. })
  133. }
  134. )
  135. },
  136. }
  137. module.exports = DocumentUpdaterManager