PersistenceManager.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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 {
  103. callback(
  104. new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
  105. )
  106. }
  107. })
  108. }
  109. function setDoc(
  110. projectId,
  111. docId,
  112. lines,
  113. version,
  114. ranges,
  115. lastUpdatedAt,
  116. lastUpdatedBy,
  117. _callback
  118. ) {
  119. const timer = new Metrics.Timer('persistenceManager.setDoc')
  120. const callback = function (...args) {
  121. timer.done()
  122. _callback(...args)
  123. }
  124. const urlPath = `/project/${projectId}/doc/${docId}`
  125. request(
  126. {
  127. url: `${Settings.apis.web.url}${urlPath}`,
  128. method: 'POST',
  129. json: {
  130. lines,
  131. ranges,
  132. version,
  133. lastUpdatedBy,
  134. lastUpdatedAt,
  135. },
  136. auth: {
  137. user: Settings.apis.web.user,
  138. pass: Settings.apis.web.pass,
  139. sendImmediately: true,
  140. },
  141. jar: false,
  142. timeout: MAX_HTTP_REQUEST_LENGTH,
  143. },
  144. (error, res, body) => {
  145. updateMetric('setDoc', error, res)
  146. if (error) {
  147. logger.error({ err: error, projectId, docId }, 'web API request failed')
  148. return callback(new Error('error connecting to web API'))
  149. }
  150. if (res.statusCode >= 200 && res.statusCode < 300) {
  151. callback(null)
  152. } else if (res.statusCode === 404) {
  153. callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
  154. } else {
  155. callback(
  156. new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
  157. )
  158. }
  159. }
  160. )
  161. }
  162. module.exports = { getDoc, setDoc }