PersistenceManager.js 5.3 KB

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