persist_buffer.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. // @ts-check
  2. 'use strict'
  3. const logger = require('@overleaf/logger')
  4. const OError = require('@overleaf/o-error')
  5. const assert = require('./assert')
  6. const chunkStore = require('./chunk_store')
  7. const { BlobStore } = require('./blob_store')
  8. const BatchBlobStore = require('./batch_blob_store')
  9. const persistChanges = require('./persist_changes')
  10. const redisBackend = require('./chunk_store/redis')
  11. /**
  12. * Persist the changes from Redis buffer to the main storage
  13. *
  14. * Algorithm Outline:
  15. * 1. Get the latest chunk's endVersion from the database
  16. * 2. Get non-persisted changes from Redis that are after this endVersion.
  17. * 3. If no such changes, exit.
  18. * 4. Load file blobs for these Redis changes.
  19. * 5. Run the persistChanges() algorithm to store these changes into a new chunk(s) in GCS.
  20. * - This must not decrease the endVersion. If changes were processed, it must advance.
  21. * 6. Set the new persisted version (endVersion of the latest persisted chunk) in Redis.
  22. *
  23. * @param {string} projectId
  24. * @param {Object} limits
  25. * @throws {Error | OError} If a critical error occurs during persistence.
  26. */
  27. async function persistBuffer(projectId, limits) {
  28. assert.projectId(projectId)
  29. logger.debug({ projectId }, 'starting persistBuffer operation')
  30. // 1. Get the latest chunk's endVersion from GCS/main store
  31. let endVersion
  32. const latestChunkMetadata = await chunkStore.getLatestChunkMetadata(projectId)
  33. if (latestChunkMetadata) {
  34. endVersion = latestChunkMetadata.endVersion
  35. } else {
  36. endVersion = 0 // No chunks found, start from version 0
  37. logger.debug({ projectId }, 'no existing chunks found in main storage')
  38. }
  39. logger.debug({ projectId, endVersion }, 'got latest persisted chunk')
  40. // 2. Get non-persisted changes from Redis
  41. const changesToPersist = await redisBackend.getNonPersistedChanges(
  42. projectId,
  43. endVersion
  44. )
  45. if (changesToPersist.length === 0) {
  46. logger.debug(
  47. { projectId, endVersion },
  48. 'no new changes in Redis buffer to persist'
  49. )
  50. // No changes to persist, update the persisted version in Redis
  51. // to match the current endVersion. This shouldn't be needed
  52. // unless a worker failed to update the persisted version.
  53. await redisBackend.setPersistedVersion(projectId, endVersion)
  54. return
  55. }
  56. logger.debug(
  57. {
  58. projectId,
  59. endVersion,
  60. count: changesToPersist.length,
  61. },
  62. 'found changes in Redis to persist'
  63. )
  64. // 4. Load file blobs for these Redis changes. Errors will propagate.
  65. const blobStore = new BlobStore(projectId)
  66. const batchBlobStore = new BatchBlobStore(blobStore)
  67. const blobHashes = new Set()
  68. for (const change of changesToPersist) {
  69. change.findBlobHashes(blobHashes)
  70. }
  71. if (blobHashes.size > 0) {
  72. await batchBlobStore.preload(Array.from(blobHashes))
  73. }
  74. for (const change of changesToPersist) {
  75. await change.loadFiles('lazy', blobStore)
  76. }
  77. // 5. Run the persistChanges() algorithm. Errors will propagate.
  78. logger.debug(
  79. {
  80. projectId,
  81. endVersion,
  82. changeCount: changesToPersist.length,
  83. },
  84. 'calling persistChanges'
  85. )
  86. const persistResult = await persistChanges(
  87. projectId,
  88. changesToPersist,
  89. limits,
  90. endVersion
  91. )
  92. if (!persistResult || !persistResult.currentChunk) {
  93. throw new OError(
  94. 'persistChanges did not produce a new chunk for non-empty changes',
  95. {
  96. projectId,
  97. endVersion,
  98. changeCount: changesToPersist.length,
  99. }
  100. )
  101. }
  102. const newPersistedChunk = persistResult.currentChunk
  103. const newEndVersion = newPersistedChunk.getEndVersion()
  104. if (newEndVersion <= endVersion) {
  105. throw new OError(
  106. 'persisted chunk endVersion must be greater than current persisted chunk end version for non-empty changes',
  107. {
  108. projectId,
  109. newEndVersion,
  110. endVersion,
  111. changeCount: changesToPersist.length,
  112. }
  113. )
  114. }
  115. logger.debug(
  116. {
  117. projectId,
  118. oldVersion: endVersion,
  119. newVersion: newEndVersion,
  120. },
  121. 'successfully persisted changes from Redis to main storage'
  122. )
  123. // 6. Set the persisted version in Redis. Errors will propagate.
  124. const status = await redisBackend.setPersistedVersion(
  125. projectId,
  126. newEndVersion
  127. )
  128. if (status !== 'ok') {
  129. throw new OError('failed to update persisted version in Redis', {
  130. projectId,
  131. newEndVersion,
  132. status,
  133. })
  134. }
  135. logger.debug(
  136. { projectId, newEndVersion },
  137. 'updated persisted version in Redis'
  138. )
  139. logger.debug(
  140. { projectId, finalPersistedVersion: newEndVersion },
  141. 'persistBuffer operation completed successfully'
  142. )
  143. }
  144. module.exports = persistBuffer