backupGenerator.mjs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * Provides a generator function to back up project chunks and blobs.
  3. */
  4. import chunkStore from './chunk_store/index.js'
  5. import {
  6. GLOBAL_BLOBS, // NOTE: must call loadGlobalBlobs() before using this
  7. BlobStore,
  8. } from './blob_store/index.js'
  9. import assert from './assert.js'
  10. async function lookBehindForSeenBlobs(
  11. projectId,
  12. chunk,
  13. lastBackedUpVersion,
  14. seenBlobs
  15. ) {
  16. if (chunk.startVersion === 0) {
  17. return // this is the first chunk, no need to check for blobs in the previous chunk
  18. }
  19. if (chunk.startVersion > 0 && lastBackedUpVersion > chunk.startVersion) {
  20. return // the snapshot in this chunk has already been backed up
  21. }
  22. if (
  23. chunk.startVersion > 0 &&
  24. lastBackedUpVersion === chunk.startVersion // same as previousChunk.endVersion
  25. ) {
  26. // the snapshot in this chunk has not been backed up
  27. // so we find the set of backed up blobs from the previous chunk
  28. const previousChunk = await chunkStore.loadAtVersion(
  29. projectId,
  30. lastBackedUpVersion,
  31. { persistedOnly: true }
  32. )
  33. const previousChunkHistory = previousChunk.getHistory()
  34. previousChunkHistory.findBlobHashes(seenBlobs)
  35. }
  36. }
  37. /**
  38. * Records blob hashes that have been previously seen in a chunk's history.
  39. *
  40. * @param {Object} chunk - The chunk containing history data
  41. * @param {number} currentBackedUpVersion - The version number that has been backed up
  42. * @param {Set<string>} seenBlobs - Set to collect previously seen blob hashes
  43. * @returns {void}
  44. */
  45. function recordPreviouslySeenBlobs(chunk, currentBackedUpVersion, seenBlobs) {
  46. // We need to look at the chunk and decide how far we have backed up.
  47. // If we have not backed up this chunk at all, we need to backup the blobs
  48. // in the snapshot. Otherwise we need to backup the blobs in the changes
  49. // that have occurred since the last backup.
  50. const history = chunk.getHistory()
  51. const startVersion = chunk.getStartVersion()
  52. if (currentBackedUpVersion === 0) {
  53. // If we have only backed up version 0 (i.e. the first change)
  54. // then that includes the initial snapshot, so we consider
  55. // the blobs of the initial snapshot as seen. If the project
  56. // has not been backed up at all then currentBackedUpVersion
  57. // will be undefined.
  58. history.snapshot.findBlobHashes(seenBlobs)
  59. } else if (currentBackedUpVersion > startVersion) {
  60. history.snapshot.findBlobHashes(seenBlobs)
  61. for (let i = 0; i < currentBackedUpVersion - startVersion; i++) {
  62. history.changes[i].findBlobHashes(seenBlobs)
  63. }
  64. }
  65. }
  66. /**
  67. * Collects new blob objects that need to be backed up from a given chunk.
  68. *
  69. * @param {Object} chunk - The chunk object containing history data
  70. * @param {Object} blobStore - Storage interface for retrieving blobs
  71. * @param {Set<string>} seenBlobs - Set of blob hashes that have already been processed
  72. * @returns {Promise<Object[]>} Array of blob objects that need to be backed up
  73. * @throws {Error} If blob retrieval fails
  74. */
  75. async function collectNewBlobsForBackup(chunk, blobStore, seenBlobs) {
  76. /** @type {Set<string>} */
  77. const blobHashes = new Set()
  78. const history = chunk.getHistory()
  79. // Get all the blobs in this chunk, then exclude the seenBlobs and global blobs
  80. history.findBlobHashes(blobHashes)
  81. const blobsToBackup = await blobStore.getBlobs(
  82. [...blobHashes].filter(
  83. hash =>
  84. hash &&
  85. !seenBlobs.has(hash) &&
  86. (!GLOBAL_BLOBS.has(hash) || GLOBAL_BLOBS.get(hash).demoted)
  87. )
  88. )
  89. return blobsToBackup
  90. }
  91. /**
  92. * Asynchronously generates backups for a project based on provided versions.
  93. * @param {string} projectId - The ID of the project's history to back up.
  94. * @param {number} lastBackedUpVersion - The last version that was successfully backed up.
  95. * @yields {AsyncGenerator<{ chunkRecord: object, chunkToBackup: object, chunkBuffer: Buffer, blobsToBackup: object[] }>}
  96. * Yields chunk records and corresponding data needed for backups.
  97. */
  98. export async function* backupGenerator(projectId, lastBackedUpVersion) {
  99. assert.projectId(projectId, 'bad projectId')
  100. assert.maybe.integer(lastBackedUpVersion, 'bad lastBackedUpVersion')
  101. const blobStore = new BlobStore(projectId)
  102. /** @type {Set<string>} */
  103. const seenBlobs = new Set() // records the blobs that are already backed up
  104. const firstPendingVersion =
  105. lastBackedUpVersion >= 0 ? lastBackedUpVersion + 1 : 0
  106. let isStartingChunk = true
  107. let currentBackedUpVersion = lastBackedUpVersion
  108. const chunkRecordIterator = chunkStore.getProjectChunksFromVersion(
  109. projectId,
  110. firstPendingVersion
  111. )
  112. for await (const chunkRecord of chunkRecordIterator) {
  113. const { chunk, chunkBuffer } = await chunkStore.loadByChunkRecord(
  114. projectId,
  115. chunkRecord
  116. )
  117. if (isStartingChunk) {
  118. await lookBehindForSeenBlobs(
  119. projectId,
  120. chunkRecord,
  121. lastBackedUpVersion,
  122. seenBlobs
  123. )
  124. isStartingChunk = false
  125. }
  126. recordPreviouslySeenBlobs(chunk, currentBackedUpVersion, seenBlobs)
  127. const blobsToBackup = await collectNewBlobsForBackup(
  128. chunk,
  129. blobStore,
  130. seenBlobs
  131. )
  132. yield { chunkRecord, chunkToBackup: chunk, chunkBuffer, blobsToBackup }
  133. // After we generate a backup of this chunk, mark the backed up blobs as seen
  134. blobsToBackup.forEach(blob => seenBlobs.add(blob.getHash()))
  135. currentBackedUpVersion = chunkRecord.endVersion
  136. }
  137. }