PersistenceManager.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. const Settings = require('@overleaf/settings')
  2. const Errors = require('./Errors')
  3. const Metrics = require('./Metrics')
  4. const logger = require('@overleaf/logger')
  5. const request = require('requestretry').defaults({
  6. maxAttempts: 2,
  7. retryDelay: 10,
  8. })
  9. // We have to be quick with HTTP calls because we're holding a lock that
  10. // expires after 30 seconds. We can't let any errors in the rest of the stack
  11. // hold us up, and need to bail out quickly if there is a problem.
  12. const MAX_HTTP_REQUEST_LENGTH = 5000 // 5 seconds
  13. function updateMetric(method, error, response) {
  14. // find the status, with special handling for connection timeouts
  15. // https://github.com/request/request#timeouts
  16. let status
  17. if (error && error.connect === true) {
  18. status = `${error.code} (connect)`
  19. } else if (error) {
  20. status = error.code
  21. } else if (response) {
  22. status = response.statusCode
  23. }
  24. Metrics.inc(method, 1, { status })
  25. if (error && error.attempts > 1) {
  26. Metrics.inc(`${method}-retries`, 1, { status: 'error' })
  27. }
  28. if (response && response.attempts > 1) {
  29. Metrics.inc(`${method}-retries`, 1, { status: 'success' })
  30. }
  31. }
  32. function getDoc(projectId, docId, options = {}, _callback) {
  33. const timer = new Metrics.Timer('persistenceManager.getDoc')
  34. if (typeof options === 'function') {
  35. _callback = options
  36. options = {}
  37. }
  38. const callback = function (...args) {
  39. timer.done()
  40. _callback(...args)
  41. }
  42. const urlPath = `/project/${projectId}/doc/${docId}`
  43. const requestParams = {
  44. url: `${Settings.apis.web.url}${urlPath}`,
  45. method: 'GET',
  46. headers: {
  47. accept: 'application/json',
  48. },
  49. auth: {
  50. user: Settings.apis.web.user,
  51. pass: Settings.apis.web.pass,
  52. sendImmediately: true,
  53. },
  54. jar: false,
  55. timeout: MAX_HTTP_REQUEST_LENGTH,
  56. }
  57. if (options.peek) {
  58. requestParams.qs = { peek: 'true' }
  59. }
  60. request(requestParams, (error, res, body) => {
  61. updateMetric('getDoc', error, res)
  62. if (error) {
  63. logger.error({ err: error, projectId, docId }, 'web API request failed')
  64. return callback(new Error('error connecting to web API'))
  65. }
  66. if (res.statusCode >= 200 && res.statusCode < 300) {
  67. try {
  68. body = JSON.parse(body)
  69. } catch (e) {
  70. return callback(e)
  71. }
  72. if (body.lines == null) {
  73. return callback(new Error('web API response had no doc lines'))
  74. }
  75. if (body.version == null) {
  76. return callback(new Error('web API response had no valid doc version'))
  77. }
  78. if (body.pathname == null) {
  79. return callback(new Error('web API response had no valid doc pathname'))
  80. }
  81. callback(
  82. null,
  83. body.lines,
  84. body.version,
  85. body.ranges,
  86. body.pathname,
  87. body.projectHistoryId,
  88. body.projectHistoryType
  89. )
  90. } else if (res.statusCode === 404) {
  91. callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
  92. } else {
  93. callback(
  94. new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
  95. )
  96. }
  97. })
  98. }
  99. function setDoc(
  100. projectId,
  101. docId,
  102. lines,
  103. version,
  104. ranges,
  105. lastUpdatedAt,
  106. lastUpdatedBy,
  107. _callback
  108. ) {
  109. const timer = new Metrics.Timer('persistenceManager.setDoc')
  110. const callback = function (...args) {
  111. timer.done()
  112. _callback(...args)
  113. }
  114. const urlPath = `/project/${projectId}/doc/${docId}`
  115. request(
  116. {
  117. url: `${Settings.apis.web.url}${urlPath}`,
  118. method: 'POST',
  119. json: {
  120. lines,
  121. ranges,
  122. version,
  123. lastUpdatedBy,
  124. lastUpdatedAt,
  125. },
  126. auth: {
  127. user: Settings.apis.web.user,
  128. pass: Settings.apis.web.pass,
  129. sendImmediately: true,
  130. },
  131. jar: false,
  132. timeout: MAX_HTTP_REQUEST_LENGTH,
  133. },
  134. (error, res, body) => {
  135. updateMetric('setDoc', error, res)
  136. if (error) {
  137. logger.error({ err: error, projectId, docId }, 'web API request failed')
  138. return callback(new Error('error connecting to web API'))
  139. }
  140. if (res.statusCode >= 200 && res.statusCode < 300) {
  141. callback(null)
  142. } else if (res.statusCode === 404) {
  143. callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
  144. } else {
  145. callback(
  146. new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
  147. )
  148. }
  149. }
  150. )
  151. }
  152. module.exports = { getDoc, setDoc }