backupGenerator.mjs 5.3 KB

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