DocumentUpdaterManager.js 4.4 KB

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