BlobManager.js 3.7 KB

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