|
|
@@ -12,6 +12,8 @@ const persistChanges = require('./persist_changes')
|
|
|
const resyncProject = require('./resync_project')
|
|
|
const redisBackend = require('./chunk_store/redis')
|
|
|
|
|
|
+const PERSIST_BATCH_SIZE = 50
|
|
|
+
|
|
|
/**
|
|
|
* Persist the changes from Redis buffer to the main storage
|
|
|
*
|
|
|
@@ -42,143 +44,166 @@ async function persistBuffer(projectId, limits) {
|
|
|
endVersion = 0 // No chunks found, start from version 0
|
|
|
logger.debug({ projectId }, 'no existing chunks found in main storage')
|
|
|
}
|
|
|
+ const originalEndVersion = endVersion
|
|
|
|
|
|
logger.debug({ projectId, endVersion }, 'got latest persisted chunk')
|
|
|
|
|
|
- // 2. Get non-persisted changes from Redis
|
|
|
- const changesToPersist = await redisBackend.getNonPersistedChanges(
|
|
|
- projectId,
|
|
|
- endVersion
|
|
|
- )
|
|
|
-
|
|
|
- if (changesToPersist.length === 0) {
|
|
|
- logger.debug(
|
|
|
- { projectId, endVersion },
|
|
|
- 'no new changes in Redis buffer to persist'
|
|
|
- )
|
|
|
- metrics.inc('persist_buffer', 1, { status: 'no_changes' })
|
|
|
- // No changes to persist, update the persisted version in Redis
|
|
|
- // to match the current endVersion. This shouldn't be needed
|
|
|
- // unless a worker failed to update the persisted version.
|
|
|
- await redisBackend.setPersistedVersion(projectId, endVersion)
|
|
|
- const { chunk } = await chunkStore.loadByChunkRecord(
|
|
|
+ // Process changes in batches
|
|
|
+ let numberOfChangesPersisted = 0
|
|
|
+ let currentChunk = null
|
|
|
+ let resyncNeeded = false
|
|
|
+ let resyncChangesWerePersisted = false
|
|
|
+ while (true) {
|
|
|
+ // 2. Get non-persisted changes from Redis
|
|
|
+ const changesToPersist = await redisBackend.getNonPersistedChanges(
|
|
|
projectId,
|
|
|
- latestChunkMetadata
|
|
|
+ endVersion,
|
|
|
+ { maxChanges: PERSIST_BATCH_SIZE }
|
|
|
)
|
|
|
- // Return the result in the same format as persistChanges
|
|
|
- // so that the caller can handle it uniformly.
|
|
|
- return {
|
|
|
- numberOfChangesPersisted: changesToPersist.length,
|
|
|
- originalEndVersion: endVersion,
|
|
|
- currentChunk: chunk,
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- logger.debug(
|
|
|
- {
|
|
|
- projectId,
|
|
|
- endVersion,
|
|
|
- count: changesToPersist.length,
|
|
|
- },
|
|
|
- 'found changes in Redis to persist'
|
|
|
- )
|
|
|
-
|
|
|
- // 4. Load file blobs for these Redis changes. Errors will propagate.
|
|
|
- const blobStore = new BlobStore(projectId)
|
|
|
- const batchBlobStore = new BatchBlobStore(blobStore)
|
|
|
-
|
|
|
- const blobHashes = new Set()
|
|
|
- for (const change of changesToPersist) {
|
|
|
- change.findBlobHashes(blobHashes)
|
|
|
- }
|
|
|
- if (blobHashes.size > 0) {
|
|
|
- await batchBlobStore.preload(Array.from(blobHashes))
|
|
|
- }
|
|
|
- for (const change of changesToPersist) {
|
|
|
- await change.loadFiles('lazy', blobStore)
|
|
|
- }
|
|
|
+ if (changesToPersist.length === 0) {
|
|
|
+ break
|
|
|
+ }
|
|
|
|
|
|
- // 5. Run the persistChanges() algorithm. Errors will propagate.
|
|
|
- logger.debug(
|
|
|
- {
|
|
|
- projectId,
|
|
|
- endVersion,
|
|
|
- changeCount: changesToPersist.length,
|
|
|
- },
|
|
|
- 'calling persistChanges'
|
|
|
- )
|
|
|
-
|
|
|
- const persistResult = await persistChanges(
|
|
|
- projectId,
|
|
|
- changesToPersist,
|
|
|
- limits,
|
|
|
- endVersion
|
|
|
- )
|
|
|
-
|
|
|
- if (!persistResult || !persistResult.currentChunk) {
|
|
|
- metrics.inc('persist_buffer', 1, { status: 'no-chunk-error' })
|
|
|
- throw new OError(
|
|
|
- 'persistChanges did not produce a new chunk for non-empty changes',
|
|
|
+ logger.debug(
|
|
|
{
|
|
|
projectId,
|
|
|
endVersion,
|
|
|
- changeCount: changesToPersist.length,
|
|
|
- }
|
|
|
+ count: changesToPersist.length,
|
|
|
+ },
|
|
|
+ 'found changes in Redis to persist'
|
|
|
)
|
|
|
- }
|
|
|
|
|
|
- const newPersistedChunk = persistResult.currentChunk
|
|
|
- const newEndVersion = newPersistedChunk.getEndVersion()
|
|
|
+ // 4. Load file blobs for these Redis changes. Errors will propagate.
|
|
|
+ const blobStore = new BlobStore(projectId)
|
|
|
+ const batchBlobStore = new BatchBlobStore(blobStore)
|
|
|
|
|
|
- if (newEndVersion <= endVersion) {
|
|
|
- metrics.inc('persist_buffer', 1, { status: 'chunk-version-mismatch' })
|
|
|
- throw new OError(
|
|
|
- 'persisted chunk endVersion must be greater than current persisted chunk end version for non-empty changes',
|
|
|
+ const blobHashes = new Set()
|
|
|
+ for (const change of changesToPersist) {
|
|
|
+ change.findBlobHashes(blobHashes)
|
|
|
+ }
|
|
|
+ if (blobHashes.size > 0) {
|
|
|
+ await batchBlobStore.preload(Array.from(blobHashes))
|
|
|
+ }
|
|
|
+ for (const change of changesToPersist) {
|
|
|
+ await change.loadFiles('lazy', blobStore)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. Run the persistChanges() algorithm. Errors will propagate.
|
|
|
+ logger.debug(
|
|
|
{
|
|
|
projectId,
|
|
|
- newEndVersion,
|
|
|
endVersion,
|
|
|
changeCount: changesToPersist.length,
|
|
|
- }
|
|
|
+ },
|
|
|
+ 'calling persistChanges'
|
|
|
)
|
|
|
- }
|
|
|
|
|
|
- logger.debug(
|
|
|
- {
|
|
|
+ const persistResult = await persistChanges(
|
|
|
projectId,
|
|
|
- oldVersion: endVersion,
|
|
|
- newVersion: newEndVersion,
|
|
|
- },
|
|
|
- 'successfully persisted changes from Redis to main storage'
|
|
|
- )
|
|
|
-
|
|
|
- // 6. Set the persisted version in Redis. Errors will propagate.
|
|
|
- const status = await redisBackend.setPersistedVersion(
|
|
|
- projectId,
|
|
|
- newEndVersion
|
|
|
- )
|
|
|
-
|
|
|
- if (status !== 'ok') {
|
|
|
- metrics.inc('persist_buffer', 1, { status: 'error-on-persisted-version' })
|
|
|
- throw new OError('failed to update persisted version in Redis', {
|
|
|
+ changesToPersist,
|
|
|
+ limits,
|
|
|
+ endVersion
|
|
|
+ )
|
|
|
+
|
|
|
+ if (!persistResult || !persistResult.currentChunk) {
|
|
|
+ metrics.inc('persist_buffer', 1, { status: 'no-chunk-error' })
|
|
|
+ throw new OError(
|
|
|
+ 'persistChanges did not produce a new chunk for non-empty changes',
|
|
|
+ {
|
|
|
+ projectId,
|
|
|
+ endVersion,
|
|
|
+ changeCount: changesToPersist.length,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ currentChunk = persistResult.currentChunk
|
|
|
+ const newEndVersion = currentChunk.getEndVersion()
|
|
|
+
|
|
|
+ if (newEndVersion <= endVersion) {
|
|
|
+ metrics.inc('persist_buffer', 1, { status: 'chunk-version-mismatch' })
|
|
|
+ throw new OError(
|
|
|
+ 'persisted chunk endVersion must be greater than current persisted chunk end version for non-empty changes',
|
|
|
+ {
|
|
|
+ projectId,
|
|
|
+ newEndVersion,
|
|
|
+ endVersion,
|
|
|
+ changeCount: changesToPersist.length,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.debug(
|
|
|
+ {
|
|
|
+ projectId,
|
|
|
+ oldVersion: endVersion,
|
|
|
+ newVersion: newEndVersion,
|
|
|
+ },
|
|
|
+ 'successfully persisted changes from Redis to main storage'
|
|
|
+ )
|
|
|
+
|
|
|
+ // 6. Set the persisted version in Redis. Errors will propagate.
|
|
|
+ const status = await redisBackend.setPersistedVersion(
|
|
|
projectId,
|
|
|
- newEndVersion,
|
|
|
- status,
|
|
|
- })
|
|
|
- }
|
|
|
+ newEndVersion
|
|
|
+ )
|
|
|
|
|
|
- logger.debug(
|
|
|
- { projectId, newEndVersion },
|
|
|
- 'updated persisted version in Redis'
|
|
|
- )
|
|
|
+ if (status !== 'ok') {
|
|
|
+ metrics.inc('persist_buffer', 1, { status: 'error-on-persisted-version' })
|
|
|
+ throw new OError('failed to update persisted version in Redis', {
|
|
|
+ projectId,
|
|
|
+ newEndVersion,
|
|
|
+ status,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.debug(
|
|
|
+ { projectId, newEndVersion },
|
|
|
+ 'updated persisted version in Redis'
|
|
|
+ )
|
|
|
+ numberOfChangesPersisted += persistResult.numberOfChangesPersisted
|
|
|
+ endVersion = newEndVersion
|
|
|
+
|
|
|
+ // Check if a resync might be needed
|
|
|
+ if (persistResult.resyncNeeded) {
|
|
|
+ resyncNeeded = true
|
|
|
+ }
|
|
|
|
|
|
- // 7. Resync the project if content hash validation failed
|
|
|
- if (limits.autoResync && persistResult.resyncNeeded) {
|
|
|
if (
|
|
|
changesToPersist.some(
|
|
|
change => change.getOrigin()?.getKind() === 'history-resync'
|
|
|
)
|
|
|
) {
|
|
|
+ resyncChangesWerePersisted = true
|
|
|
+ }
|
|
|
+
|
|
|
+ if (persistResult.numberOfChangesPersisted < PERSIST_BATCH_SIZE) {
|
|
|
+ // We reached the end of available changes
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (numberOfChangesPersisted === 0) {
|
|
|
+ logger.debug(
|
|
|
+ { projectId, endVersion },
|
|
|
+ 'no new changes in Redis buffer to persist'
|
|
|
+ )
|
|
|
+ metrics.inc('persist_buffer', 1, { status: 'no_changes' })
|
|
|
+ // No changes to persist, update the persisted version in Redis
|
|
|
+ // to match the current endVersion. This shouldn't be needed
|
|
|
+ // unless a worker failed to update the persisted version.
|
|
|
+ await redisBackend.setPersistedVersion(projectId, endVersion)
|
|
|
+ } else {
|
|
|
+ logger.debug(
|
|
|
+ { projectId, finalPersistedVersion: endVersion },
|
|
|
+ 'persistBuffer operation completed successfully'
|
|
|
+ )
|
|
|
+ metrics.inc('persist_buffer', 1, { status: 'persisted' })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (limits.autoResync && resyncNeeded) {
|
|
|
+ if (resyncChangesWerePersisted) {
|
|
|
// To avoid an infinite loop, do not resync if the current batch of
|
|
|
// changes contains a history resync.
|
|
|
logger.warn(
|
|
|
@@ -193,14 +218,20 @@ async function persistBuffer(projectId, limits) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- logger.debug(
|
|
|
- { projectId, finalPersistedVersion: newEndVersion },
|
|
|
- 'persistBuffer operation completed successfully'
|
|
|
- )
|
|
|
-
|
|
|
- metrics.inc('persist_buffer', 1, { status: 'persisted' })
|
|
|
+ if (currentChunk == null) {
|
|
|
+ const { chunk } = await chunkStore.loadByChunkRecord(
|
|
|
+ projectId,
|
|
|
+ latestChunkMetadata
|
|
|
+ )
|
|
|
+ currentChunk = chunk
|
|
|
+ }
|
|
|
|
|
|
- return persistResult
|
|
|
+ return {
|
|
|
+ numberOfChangesPersisted,
|
|
|
+ originalEndVersion,
|
|
|
+ currentChunk,
|
|
|
+ resyncNeeded,
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
module.exports = persistBuffer
|