PersistenceManager.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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. if (!body.pathname) {
  82. logger.warn(
  83. { projectId, docId },
  84. 'missing pathname in PersistenceManager getDoc'
  85. )
  86. Metrics.inc('pathname', 1, {
  87. path: 'PersistenceManager.getDoc',
  88. status: body.pathname === '' ? 'zero-length' : 'undefined',
  89. })
  90. }
  91. callback(
  92. null,
  93. body.lines,
  94. body.version,
  95. body.ranges,
  96. body.pathname,
  97. body.projectHistoryId?.toString()
  98. )
  99. } else if (res.statusCode === 404) {
  100. callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
  101. } else if (res.statusCode === 413) {
  102. callback(
  103. new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`)
  104. )
  105. } else {
  106. callback(
  107. new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
  108. )
  109. }
  110. })
  111. }
  112. function setDoc(
  113. projectId,
  114. docId,
  115. lines,
  116. version,
  117. ranges,
  118. lastUpdatedAt,
  119. lastUpdatedBy,
  120. _callback
  121. ) {
  122. const timer = new Metrics.Timer('persistenceManager.setDoc')
  123. const callback = function (...args) {
  124. timer.done()
  125. _callback(...args)
  126. }
  127. const urlPath = `/project/${projectId}/doc/${docId}`
  128. request(
  129. {
  130. url: `${Settings.apis.web.url}${urlPath}`,
  131. method: 'POST',
  132. json: {
  133. lines,
  134. ranges,
  135. version,
  136. lastUpdatedBy,
  137. lastUpdatedAt,
  138. },
  139. auth: {
  140. user: Settings.apis.web.user,
  141. pass: Settings.apis.web.pass,
  142. sendImmediately: true,
  143. },
  144. jar: false,
  145. timeout: MAX_HTTP_REQUEST_LENGTH,
  146. },
  147. (error, res, body) => {
  148. updateMetric('setDoc', error, res)
  149. if (error) {
  150. logger.error({ err: error, projectId, docId }, 'web API request failed')
  151. return callback(new Error('error connecting to web API'))
  152. }
  153. if (res.statusCode >= 200 && res.statusCode < 300) {
  154. callback(null, body)
  155. } else if (res.statusCode === 404) {
  156. callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
  157. } else if (res.statusCode === 413) {
  158. callback(
  159. new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`)
  160. )
  161. } else {
  162. callback(
  163. new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
  164. )
  165. }
  166. }
  167. )
  168. }
  169. module.exports = { getDoc, setDoc }