PersistenceManager.js 5.7 KB

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