WebApiManager.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { callbackify } from 'node:util'
  2. import { setTimeout } from 'node:timers/promises'
  3. import logger from '@overleaf/logger'
  4. import Metrics from '@overleaf/metrics'
  5. import Settings from '@overleaf/settings'
  6. import {
  7. fetchNothing,
  8. fetchJson,
  9. RequestFailedError,
  10. } from '@overleaf/fetch-utils'
  11. import * as Errors from './Errors.js'
  12. import * as RedisManager from './RedisManager.js'
  13. let RETRY_TIMEOUT_MS = 5000
  14. async function getHistoryId(projectId) {
  15. Metrics.inc('history_id_cache_requests_total')
  16. const cachedHistoryId =
  17. await RedisManager.promises.getCachedHistoryId(projectId)
  18. if (cachedHistoryId) {
  19. Metrics.inc('history_id_cache_hits_total')
  20. return cachedHistoryId
  21. } else {
  22. const project = await _getProjectDetails(projectId)
  23. const historyId =
  24. project.overleaf &&
  25. project.overleaf.history &&
  26. project.overleaf.history.id
  27. if (historyId != null) {
  28. await RedisManager.promises.setCachedHistoryId(projectId, historyId)
  29. }
  30. return historyId
  31. }
  32. }
  33. async function requestResync(projectId, opts = {}) {
  34. try {
  35. const body = {}
  36. if (opts.historyRangesMigration) {
  37. body.historyRangesMigration = opts.historyRangesMigration
  38. }
  39. if (opts.resyncProjectStructureOnly) {
  40. body.resyncProjectStructureOnly = opts.resyncProjectStructureOnly
  41. }
  42. await fetchNothing(
  43. `${Settings.apis.web.url}/project/${projectId}/history/resync`,
  44. {
  45. method: 'POST',
  46. signal: AbortSignal.timeout(6 * 60000),
  47. basicAuth: {
  48. user: Settings.apis.web.user,
  49. password: Settings.apis.web.pass,
  50. },
  51. json: body,
  52. }
  53. )
  54. } catch (err) {
  55. if (err instanceof RequestFailedError && err.response.status === 404) {
  56. throw new Errors.NotFoundError('got a 404 from web api').withCause(err)
  57. } else {
  58. throw err
  59. }
  60. }
  61. }
  62. async function _getProjectDetails(projectId, callback) {
  63. logger.debug({ projectId }, 'getting project details from web')
  64. let attempts = 0
  65. while (true) {
  66. attempts += 1
  67. try {
  68. return await fetchJson(
  69. `${Settings.apis.web.url}/project/${projectId}/details`,
  70. {
  71. signal: AbortSignal.timeout(16000),
  72. basicAuth: {
  73. user: Settings.apis.web.user,
  74. password: Settings.apis.web.pass,
  75. },
  76. }
  77. )
  78. } catch (err) {
  79. if (err instanceof RequestFailedError && err.response.status === 404) {
  80. throw new Errors.NotFoundError('got a 404 from web api').withCause(err)
  81. } else if (attempts < 2) {
  82. // retry after 5 seconds
  83. await setTimeout(RETRY_TIMEOUT_MS)
  84. } else {
  85. throw err
  86. }
  87. }
  88. }
  89. }
  90. /**
  91. * Adjust the retry timeout in tests
  92. */
  93. export async function setRetryTimeoutMs(timeoutMs) {
  94. RETRY_TIMEOUT_MS = timeoutMs
  95. }
  96. // EXPORTS
  97. const getHistoryIdCb = callbackify(getHistoryId)
  98. const requestResyncCb = callbackify(requestResync)
  99. export { getHistoryIdCb as getHistoryId, requestResyncCb as requestResync }
  100. export const promises = {
  101. getHistoryId,
  102. requestResync,
  103. }