PersistenceManager.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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. body.historyRangesSupport || false,
  101. body.resolvedCommentIds || []
  102. )
  103. } else if (res.statusCode === 404) {
  104. callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
  105. } else if (res.statusCode === 413) {
  106. callback(
  107. new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`)
  108. )
  109. } else {
  110. callback(
  111. new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
  112. )
  113. }
  114. })
  115. }
  116. function setDoc(
  117. projectId,
  118. docId,
  119. lines,
  120. version,
  121. ranges,
  122. lastUpdatedAt,
  123. lastUpdatedBy,
  124. _callback
  125. ) {
  126. const timer = new Metrics.Timer('persistenceManager.setDoc')
  127. const callback = function (...args) {
  128. timer.done()
  129. _callback(...args)
  130. }
  131. const urlPath = `/project/${projectId}/doc/${docId}`
  132. request(
  133. {
  134. url: `${Settings.apis.web.url}${urlPath}`,
  135. method: 'POST',
  136. json: {
  137. lines,
  138. ranges,
  139. version,
  140. lastUpdatedBy,
  141. lastUpdatedAt,
  142. },
  143. auth: {
  144. user: Settings.apis.web.user,
  145. pass: Settings.apis.web.pass,
  146. sendImmediately: true,
  147. },
  148. jar: false,
  149. timeout: MAX_HTTP_REQUEST_LENGTH,
  150. },
  151. (error, res, body) => {
  152. updateMetric('setDoc', error, res)
  153. if (error) {
  154. logger.error({ err: error, projectId, docId }, 'web API request failed')
  155. return callback(new Error('error connecting to web API'))
  156. }
  157. if (res.statusCode >= 200 && res.statusCode < 300) {
  158. callback(null, body)
  159. } else if (res.statusCode === 404) {
  160. callback(new Errors.NotFoundError(`doc not not found: ${urlPath}`))
  161. } else if (res.statusCode === 413) {
  162. callback(
  163. new Errors.FileTooLargeError(`doc exceeds maximum size: ${urlPath}`)
  164. )
  165. } else {
  166. callback(
  167. new Error(`error accessing web API: ${urlPath} ${res.statusCode}`)
  168. )
  169. }
  170. }
  171. )
  172. }
  173. module.exports = {
  174. getDoc,
  175. setDoc,
  176. promises: {
  177. getDoc: promisifyMultiResult(getDoc, [
  178. 'lines',
  179. 'version',
  180. 'ranges',
  181. 'pathname',
  182. 'projectHistoryId',
  183. 'historyRangesSupport',
  184. 'resolvedCommentIds',
  185. ]),
  186. setDoc: promisify(setDoc),
  187. },
  188. }