DocumentUpdaterManager.js 4.8 KB

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