PersistenceManager.js 5.1 KB

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