PersistenceManager.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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,
  98. body.projectHistoryType
  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 }