PersistenceManager.js 5.2 KB

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