DocumentUpdaterManager.js 4.3 KB

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