BlobManager.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import async from 'async'
  2. import logger from '@overleaf/logger'
  3. import OError from '@overleaf/o-error'
  4. import * as HistoryStoreManager from './HistoryStoreManager.js'
  5. import * as UpdateTranslator from './UpdateTranslator.js'
  6. // avoid creating too many blobs at the same time
  7. const MAX_CONCURRENT_REQUESTS = 4
  8. // number of retry attempts for blob creation
  9. const RETRY_ATTEMPTS = 3
  10. // delay between retries
  11. const RETRY_INTERVAL = 100
  12. export function createBlobsForUpdates(
  13. projectId,
  14. historyId,
  15. updates,
  16. extendLock,
  17. callback
  18. ) {
  19. // async.mapLimit runs jobs in parallel and returns on the first error. It
  20. // doesn't wait for concurrent jobs to finish. We want to make sure all jobs
  21. // are wrapped within our lock so we collect the first error enountered here
  22. // and wait for all jobs to finish before returning the error.
  23. let firstBlobCreationError = null
  24. function createBlobForUpdate(update, cb) {
  25. // For file additions we need to first create a blob in the history-store
  26. // with the contents of the file. Then we can create a change containing a
  27. // file addition operation which references the blob.
  28. //
  29. // To do this we decorate file creation updates with a blobHash
  30. if (!UpdateTranslator.isAddUpdate(update)) {
  31. return async.setImmediate(() => cb(null, { update }))
  32. }
  33. let attempts = 0
  34. // Since we may be creating O(1000) blobs in an update, allow for the
  35. // occasional failure to prevent the whole update failing.
  36. async.retry(
  37. {
  38. times: RETRY_ATTEMPTS,
  39. interval: RETRY_INTERVAL,
  40. },
  41. _cb => {
  42. attempts++
  43. if (attempts > 1) {
  44. logger.error(
  45. { projectId, doc: update.doc, file: update.file, attempts },
  46. 'previous createBlob attempt failed, retrying'
  47. )
  48. }
  49. // extend the lock for each file because large files may take a long time
  50. extendLock(err => {
  51. if (err) {
  52. return _cb(OError.tag(err))
  53. }
  54. HistoryStoreManager.createBlobForUpdate(
  55. projectId,
  56. historyId,
  57. update,
  58. (err, hash) => {
  59. if (err) {
  60. OError.tag(err, 'retry: error creating blob', {
  61. projectId,
  62. doc: update.doc,
  63. file: update.file,
  64. })
  65. _cb(err)
  66. } else {
  67. _cb(null, hash)
  68. }
  69. }
  70. )
  71. })
  72. },
  73. (error, blobHash) => {
  74. if (error) {
  75. if (!firstBlobCreationError) {
  76. firstBlobCreationError = error
  77. }
  78. return cb(null, { update, blobHash })
  79. }
  80. extendLock(error => {
  81. if (error) {
  82. if (!firstBlobCreationError) {
  83. firstBlobCreationError = error
  84. }
  85. }
  86. cb(null, { update, blobHash })
  87. })
  88. }
  89. )
  90. }
  91. async.mapLimit(
  92. updates,
  93. MAX_CONCURRENT_REQUESTS,
  94. createBlobForUpdate,
  95. (unusedError, updatesWithBlobs) => {
  96. // As indicated by the name this is unexpected, but changes in the future
  97. // could cause it to be set and ignoring it would be unexpected
  98. if (unusedError) {
  99. return callback(unusedError)
  100. }
  101. if (firstBlobCreationError) {
  102. return callback(firstBlobCreationError)
  103. }
  104. callback(null, updatesWithBlobs)
  105. }
  106. )
  107. }