index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. // @ts-check
  2. 'use strict'
  3. /**
  4. * Manage {@link Chunk} and {@link History} storage.
  5. *
  6. * For storage, chunks are immutable. If we want to update a project with new
  7. * changes, we create a new chunk record and History object and delete the old
  8. * ones. If we compact a project's history, we similarly destroy the old chunk
  9. * (or chunks) and replace them with a new one. This is helpful when using S3,
  10. * because it guarantees only eventual consistency for updates but provides
  11. * stronger consistency guarantees for object creation.
  12. *
  13. * When a chunk record in the database is removed, we save its ID for later
  14. * in the `old_chunks` table, rather than deleting it immediately. This lets us
  15. * use batch deletion to reduce the number of delete requests to S3.
  16. *
  17. * The chunk store also caches data about which blobs are referenced by each
  18. * chunk, which allows us to find unused blobs without loading all of the data
  19. * for all projects from S3. Whenever we create a chunk, we also insert records
  20. * into the `chunk_blobs` table, to help with this bookkeeping.
  21. */
  22. const config = require('config')
  23. const OError = require('@overleaf/o-error')
  24. const metrics = require('@overleaf/metrics')
  25. const { Chunk, History, Snapshot } = require('overleaf-editor-core')
  26. const assert = require('../assert')
  27. const BatchBlobStore = require('../batch_blob_store')
  28. const { BlobStore } = require('../blob_store')
  29. const { historyStore } = require('../history_store')
  30. const mongoBackend = require('./mongo')
  31. const postgresBackend = require('./postgres')
  32. const redisBackend = require('./redis')
  33. const {
  34. ChunkVersionConflictError,
  35. VersionOutOfBoundsError,
  36. } = require('./errors')
  37. const { promiseMapWithLimit } = require('@overleaf/promise-utils')
  38. /**
  39. * @import { Change } from 'overleaf-editor-core'
  40. */
  41. const DEFAULT_DELETE_BATCH_SIZE = parseInt(config.get('maxDeleteKeys'), 10)
  42. const DEFAULT_DELETE_TIMEOUT_SECS = 3000 // 50 minutes
  43. const DEFAULT_DELETE_MIN_AGE_SECS = 86400 // 1 day
  44. /**
  45. * Create the initial chunk for a project.
  46. */
  47. async function initializeProject(projectId, snapshot) {
  48. if (projectId != null) {
  49. assert.projectId(projectId, 'bad projectId')
  50. } else {
  51. projectId = await postgresBackend.generateProjectId()
  52. }
  53. if (snapshot != null) {
  54. assert.instance(snapshot, Snapshot, 'bad snapshot')
  55. } else {
  56. snapshot = new Snapshot()
  57. }
  58. const blobStore = new BlobStore(projectId)
  59. await blobStore.initialize()
  60. const backend = getBackend(projectId)
  61. const chunkRecord = await backend.getLatestChunk(projectId)
  62. if (chunkRecord != null) {
  63. throw new AlreadyInitialized(projectId)
  64. }
  65. const history = new History(snapshot, [])
  66. const chunk = new Chunk(history, 0)
  67. await create(projectId, chunk)
  68. return projectId
  69. }
  70. /**
  71. * Clone the project data.
  72. * @param {string} sourceProjectId
  73. * @param {string} targetProjectId
  74. * @param {(string) => void} onProgress
  75. * @param {AbortSignal} signal
  76. */
  77. async function cloneProject(
  78. sourceProjectId,
  79. targetProjectId,
  80. onProgress,
  81. signal
  82. ) {
  83. assert.projectId(targetProjectId, 'bad target projectId')
  84. assert.projectId(sourceProjectId, 'bad source projectId')
  85. onProgress('existing history: checking')
  86. const backend = getBackend(targetProjectId)
  87. const chunkRecord = await backend.getLatestChunk(targetProjectId)
  88. if (!chunkRecord) {
  89. onProgress('existing history: not found, aborting')
  90. throw new OError('target project is not initialized yet')
  91. }
  92. if (chunkRecord?.endVersion > 0) {
  93. onProgress('existing history: found changes, aborting')
  94. throw new AlreadyInitialized(targetProjectId)
  95. }
  96. onProgress('existing history: deleting empty chunk')
  97. await backend.deleteChunk(targetProjectId, chunkRecord.id)
  98. onProgress('existing history: deleted empty chunk')
  99. async function cloneBlobs() {
  100. onProgress('cloning blobs metadata: pending')
  101. const blobStore = new BlobStore(targetProjectId)
  102. await blobStore.clone(sourceProjectId, onProgress, signal)
  103. onProgress('cloning blobs metadata: done')
  104. }
  105. async function cloneChunks() {
  106. onProgress('cloning chunks metadata: pending')
  107. const chunkIds = await backend.clone(sourceProjectId, targetProjectId)
  108. onProgress(`chunks-metadata-imported: ${chunkIds.size}`)
  109. let done = 0
  110. await promiseMapWithLimit(
  111. 50,
  112. Array.from(chunkIds.entries()),
  113. async ([sourceChunkId, targetChunkId]) => {
  114. if (signal.aborted) return
  115. await historyStore.cloneChunk(
  116. sourceProjectId,
  117. sourceChunkId,
  118. targetProjectId,
  119. targetChunkId
  120. )
  121. done++
  122. onProgress(`chunks-copied: ${done}`)
  123. }
  124. )
  125. onProgress('cloning chunks metadata: done')
  126. }
  127. await Promise.all([cloneBlobs(), cloneChunks()])
  128. }
  129. /**
  130. * Load the blobs referenced in the given history
  131. */
  132. async function lazyLoadHistoryFiles(history, batchBlobStore) {
  133. const blobHashes = new Set()
  134. history.findBlobHashes(blobHashes)
  135. await batchBlobStore.preload(Array.from(blobHashes))
  136. await history.loadFiles('lazy', batchBlobStore)
  137. }
  138. /**
  139. * Load the latest Chunk stored for a project, including blob metadata.
  140. *
  141. * @param {string} projectId
  142. * @param {Object} [opts]
  143. * @param {boolean} [opts.readOnly]
  144. * @return {Promise<{id: string, startVersion: number, endVersion: number, endTimestamp: Date}>}
  145. */
  146. async function getLatestChunkMetadata(projectId, opts) {
  147. assert.projectId(projectId, 'bad projectId')
  148. const backend = getBackend(projectId)
  149. const chunkMetadata = await backend.getLatestChunk(projectId, opts)
  150. if (chunkMetadata == null) {
  151. throw new Chunk.NotFoundError(projectId)
  152. }
  153. return chunkMetadata
  154. }
  155. /**
  156. * Load the latest Chunk stored for a project, including blob metadata.
  157. *
  158. * @param {string} projectId
  159. * @param {object} [opts]
  160. * @param {boolean} [opts.persistedOnly] - only include persisted changes
  161. * @return {Promise<Chunk>}
  162. */
  163. async function loadLatest(projectId, opts = {}) {
  164. const chunkMetadata = await getLatestChunkMetadata(projectId)
  165. const rawHistory = await historyStore.loadRaw(projectId, chunkMetadata.id)
  166. const history = History.fromRaw(rawHistory)
  167. if (!opts.persistedOnly) {
  168. const nonPersistedChanges = await getChunkExtension(
  169. projectId,
  170. chunkMetadata.endVersion
  171. )
  172. history.pushChanges(nonPersistedChanges)
  173. }
  174. const blobStore = new BlobStore(projectId)
  175. const batchBlobStore = new BatchBlobStore(blobStore)
  176. await lazyLoadHistoryFiles(history, batchBlobStore)
  177. return new Chunk(history, chunkMetadata.startVersion)
  178. }
  179. /**
  180. * Load the the chunk that contains the given version, including blob metadata.
  181. *
  182. * @param {string} projectId
  183. * @param {number} version
  184. * @param {object} [opts]
  185. * @param {boolean} [opts.persistedOnly] - only include persisted changes
  186. * @param {boolean} [opts.preferNewer] - If the version is at the boundary of
  187. * two chunks, return the newer chunk.
  188. */
  189. async function loadAtVersion(projectId, version, opts = {}) {
  190. assert.projectId(projectId, 'bad projectId')
  191. assert.integer(version, 'bad version')
  192. const backend = getBackend(projectId)
  193. const blobStore = new BlobStore(projectId)
  194. const batchBlobStore = new BatchBlobStore(blobStore)
  195. const latestChunkMetadata = await getLatestChunkMetadata(projectId)
  196. // When loading a chunk for a version there are three cases to consider:
  197. // 1. If `persistedOnly` is true, we always use the requested version
  198. // to fetch the chunk.
  199. // 2. If `persistedOnly` is false and the requested version is in the
  200. // persisted chunk version range, we use the requested version.
  201. // 3. If `persistedOnly` is false and the requested version is ahead of
  202. // the persisted chunk versions, we fetch the latest chunk and see if
  203. // the non-persisted changes include the requested version.
  204. const targetChunkVersion = opts.persistedOnly
  205. ? version
  206. : Math.min(latestChunkMetadata.endVersion, version)
  207. const chunkRecord = await backend.getChunkForVersion(
  208. projectId,
  209. targetChunkVersion,
  210. {
  211. preferNewer: opts.preferNewer,
  212. }
  213. )
  214. const rawHistory = await historyStore.loadRaw(projectId, chunkRecord.id)
  215. const history = History.fromRaw(rawHistory)
  216. const startVersion = chunkRecord.endVersion - history.countChanges()
  217. if (!opts.persistedOnly) {
  218. // Try to extend the chunk with any non-persisted changes that
  219. // follow the chunk's end version.
  220. const nonPersistedChanges = await getChunkExtension(
  221. projectId,
  222. chunkRecord.endVersion
  223. )
  224. history.pushChanges(nonPersistedChanges)
  225. // Check that the changes do actually contain the requested version
  226. if (version > chunkRecord.endVersion + nonPersistedChanges.length) {
  227. throw new Chunk.VersionNotFoundError(projectId, version)
  228. }
  229. }
  230. await lazyLoadHistoryFiles(history, batchBlobStore)
  231. return new Chunk(history, startVersion)
  232. }
  233. /**
  234. * Load the chunk that contains the version that was current at the given
  235. * timestamp, including blob metadata.
  236. *
  237. * @param {string} projectId
  238. * @param {Date} timestamp
  239. * @param {object} [opts]
  240. * @param {boolean} [opts.persistedOnly] - only include persisted changes
  241. */
  242. async function loadAtTimestamp(projectId, timestamp, opts = {}) {
  243. assert.projectId(projectId, 'bad projectId')
  244. assert.date(timestamp, 'bad timestamp')
  245. const backend = getBackend(projectId)
  246. const blobStore = new BlobStore(projectId)
  247. const batchBlobStore = new BatchBlobStore(blobStore)
  248. const chunkRecord = await backend.getChunkForTimestamp(projectId, timestamp)
  249. const rawHistory = await historyStore.loadRaw(projectId, chunkRecord.id)
  250. const history = History.fromRaw(rawHistory)
  251. const startVersion = chunkRecord.endVersion - history.countChanges()
  252. if (!opts.persistedOnly) {
  253. const nonPersistedChanges = await getChunkExtension(
  254. projectId,
  255. chunkRecord.endVersion
  256. )
  257. history.pushChanges(nonPersistedChanges)
  258. }
  259. await lazyLoadHistoryFiles(history, batchBlobStore)
  260. return new Chunk(history, startVersion)
  261. }
  262. /** Get the changes since a given version (since), including non-persisted changes.
  263. * Note that if there are multiple chunks since the given version, the changes from
  264. * the first chunk will be returned with a hasMore flag to indicate that there are
  265. * more changes available. The 'since' version is exclusive.
  266. * @param {string} projectId
  267. * @param {number} since - version to get changes since (exclusive)
  268. * @return {Promise<{changes: Change[], hasMore: boolean}>} - object with array of changes and boolean indicating if there are more changes available
  269. */
  270. async function getChangesSinceVersion(projectId, since) {
  271. assert.projectId(projectId, 'bad projectId')
  272. assert.integer(since, 'bad since version')
  273. // First try to get changes directly from Redis buffer
  274. const result = await redisBackend.getChangesSinceVersion(projectId, since)
  275. if (result.status === 'ok') {
  276. // Successfully got changes from Redis, no more changes available beyond what Redis has
  277. metrics.inc('chunk_store.get_changes_since_version', 1, {
  278. source: 'redis',
  279. hasMore: 'false',
  280. status: result.status,
  281. })
  282. return { changes: result.changes || [], hasMore: false }
  283. }
  284. // If status is 'not_found' or 'out_of_bounds', fall through to chunk-based approach
  285. const chunk = await loadAtVersion(projectId, since, {
  286. preferNewer: true,
  287. })
  288. // Validate that 'since' is within the bounds of the chunk
  289. if (since < chunk.getStartVersion()) {
  290. throw new VersionOutOfBoundsError('Chunk does not include since version', {
  291. projectId,
  292. since,
  293. })
  294. }
  295. // Extract the changes after 'since' from the chunk
  296. const changes = chunk.getChanges().slice(since - chunk.getStartVersion())
  297. // Check if there are more changes beyond the current chunk
  298. const latestChunkMetadata = await getLatestChunkMetadata(projectId)
  299. const hasMore = latestChunkMetadata.endVersion > chunk.getEndVersion()
  300. metrics.inc('chunk_store.get_changes_since_version', 1, {
  301. source: 'gcs',
  302. hasMore: hasMore ? 'true' : 'false',
  303. status: result.status,
  304. })
  305. return { changes, hasMore }
  306. }
  307. /**
  308. * Store the chunk and insert corresponding records in the database.
  309. *
  310. * @param {string} projectId
  311. * @param {Chunk} chunk
  312. * @param {Date} [earliestChangeTimestamp]
  313. */
  314. async function create(projectId, chunk, earliestChangeTimestamp) {
  315. assert.projectId(projectId, 'bad projectId')
  316. assert.instance(chunk, Chunk, 'bad chunk')
  317. assert.maybe.date(earliestChangeTimestamp, 'bad timestamp')
  318. const backend = getBackend(projectId)
  319. const chunkStart = chunk.getStartVersion()
  320. const opts = {}
  321. if (chunkStart > 0) {
  322. const oldChunk = await backend.getChunkForVersion(projectId, chunkStart)
  323. if (oldChunk.endVersion !== chunkStart) {
  324. throw new ChunkVersionConflictError(
  325. 'unexpected end version on chunk to be updated',
  326. {
  327. projectId,
  328. expectedVersion: chunkStart,
  329. actualVersion: oldChunk.endVersion,
  330. }
  331. )
  332. }
  333. opts.oldChunkId = oldChunk.id
  334. }
  335. if (earliestChangeTimestamp != null) {
  336. opts.earliestChangeTimestamp = earliestChangeTimestamp
  337. }
  338. const chunkId = await uploadChunk(projectId, chunk)
  339. await backend.confirmCreate(projectId, chunk, chunkId, opts)
  340. }
  341. /**
  342. * Upload the given chunk to object storage.
  343. *
  344. * This is used by the create and update methods.
  345. */
  346. async function uploadChunk(projectId, chunk) {
  347. const backend = getBackend(projectId)
  348. const blobStore = new BlobStore(projectId)
  349. const historyStoreConcurrency = parseInt(
  350. config.get('chunkStore.historyStoreConcurrency'),
  351. 10
  352. )
  353. const rawHistory = await chunk
  354. .getHistory()
  355. .store(blobStore, historyStoreConcurrency)
  356. const chunkId = await backend.insertPendingChunk(projectId, chunk)
  357. await historyStore.storeRaw(projectId, chunkId, rawHistory)
  358. return chunkId
  359. }
  360. /**
  361. * Extend the project's history by replacing the latest chunk with a new
  362. * chunk.
  363. *
  364. * @param {string} projectId
  365. * @param {Chunk} newChunk
  366. * @param {Date} [earliestChangeTimestamp]
  367. * @return {Promise}
  368. */
  369. async function update(projectId, newChunk, earliestChangeTimestamp) {
  370. assert.projectId(projectId, 'bad projectId')
  371. assert.instance(newChunk, Chunk, 'bad newChunk')
  372. assert.maybe.date(earliestChangeTimestamp, 'bad timestamp')
  373. const backend = getBackend(projectId)
  374. const oldChunk = await backend.getChunkForVersion(
  375. projectId,
  376. newChunk.getStartVersion(),
  377. { preferNewer: true }
  378. )
  379. if (oldChunk.startVersion !== newChunk.getStartVersion()) {
  380. throw new ChunkVersionConflictError(
  381. 'unexpected start version on chunk to be updated',
  382. {
  383. projectId,
  384. expectedVersion: newChunk.getStartVersion(),
  385. actualVersion: oldChunk.startVersion,
  386. }
  387. )
  388. }
  389. if (oldChunk.endVersion > newChunk.getEndVersion()) {
  390. throw new ChunkVersionConflictError(
  391. 'chunk update would decrease chunk version',
  392. {
  393. projectId,
  394. currentVersion: oldChunk.endVersion,
  395. newVersion: newChunk.getEndVersion(),
  396. }
  397. )
  398. }
  399. const newChunkId = await uploadChunk(projectId, newChunk)
  400. const opts = {}
  401. if (earliestChangeTimestamp != null) {
  402. opts.earliestChangeTimestamp = earliestChangeTimestamp
  403. }
  404. await backend.confirmUpdate(
  405. projectId,
  406. oldChunk.id,
  407. newChunk,
  408. newChunkId,
  409. opts
  410. )
  411. }
  412. /**
  413. * Find the chunk ID for a given version of a project.
  414. *
  415. * @param {string} projectId
  416. * @param {number} version
  417. * @return {Promise.<string>}
  418. */
  419. async function getChunkIdForVersion(projectId, version) {
  420. const backend = getBackend(projectId)
  421. const chunkRecord = await backend.getChunkForVersion(projectId, version)
  422. return chunkRecord.id
  423. }
  424. /**
  425. * Find the chunk metadata for a given version of a project.
  426. *
  427. * @param {string} projectId
  428. * @param {number} version
  429. * @return {Promise.<{id: string|number, startVersion: number, endVersion: number}>}
  430. */
  431. async function getChunkMetadataForVersion(projectId, version) {
  432. const backend = getBackend(projectId)
  433. const chunkRecord = await backend.getChunkForVersion(projectId, version)
  434. return chunkRecord
  435. }
  436. /**
  437. * Get all of a project's chunk ids
  438. */
  439. async function getProjectChunkIds(projectId) {
  440. const backend = getBackend(projectId)
  441. const chunkIds = await backend.getProjectChunkIds(projectId)
  442. return chunkIds
  443. }
  444. /**
  445. * Get all of a projects chunks directly
  446. */
  447. async function getProjectChunks(projectId) {
  448. const backend = getBackend(projectId)
  449. const chunkIds = await backend.getProjectChunks(projectId)
  450. return chunkIds
  451. }
  452. /**
  453. * Load the chunk for a given chunk record, including blob metadata.
  454. */
  455. async function loadByChunkRecord(projectId, chunkRecord) {
  456. const blobStore = new BlobStore(projectId)
  457. const batchBlobStore = new BatchBlobStore(blobStore)
  458. const { raw: rawHistory, buffer: chunkBuffer } =
  459. await historyStore.loadRawWithBuffer(projectId, chunkRecord.id)
  460. const history = History.fromRaw(rawHistory)
  461. await lazyLoadHistoryFiles(history, batchBlobStore)
  462. return {
  463. chunk: new Chunk(history, chunkRecord.endVersion - history.countChanges()),
  464. chunkBuffer,
  465. }
  466. }
  467. /**
  468. * Asynchronously retrieves project chunks starting from a specific version.
  469. *
  470. * This generator function yields chunk records for a given project starting from the specified version (inclusive).
  471. * It continues to fetch and yield subsequent chunk records until the end version of the latest chunk metadata is reached.
  472. * If you want to fetch all the chunks *after* a version V, call this function with V+1.
  473. *
  474. * @param {string} projectId - The ID of the project.
  475. * @param {number} version - The starting version to retrieve chunks from.
  476. * @returns {AsyncGenerator<Object, void, undefined>} An async generator that yields chunk records.
  477. */
  478. async function* getProjectChunksFromVersion(projectId, version) {
  479. const backend = getBackend(projectId)
  480. const latestChunkMetadata = await getLatestChunkMetadata(projectId)
  481. if (!latestChunkMetadata || version > latestChunkMetadata.endVersion) {
  482. return
  483. }
  484. let chunkRecord = await backend.getChunkForVersion(projectId, version)
  485. while (chunkRecord != null) {
  486. yield chunkRecord
  487. if (chunkRecord.endVersion >= latestChunkMetadata.endVersion) {
  488. break
  489. } else {
  490. chunkRecord = await backend.getChunkForVersion(
  491. projectId,
  492. chunkRecord.endVersion + 1
  493. )
  494. }
  495. }
  496. }
  497. /**
  498. * Delete the given chunk from the database.
  499. *
  500. * This doesn't delete the chunk from object storage yet. The old chunks
  501. * collection will do that.
  502. */
  503. async function destroy(projectId, chunkId) {
  504. const backend = getBackend(projectId)
  505. await backend.deleteChunk(projectId, chunkId)
  506. }
  507. /**
  508. * Delete all of a project's chunks from the database.
  509. */
  510. async function deleteProjectChunks(projectId) {
  511. const backend = getBackend(projectId)
  512. await backend.deleteProjectChunks(projectId)
  513. }
  514. /**
  515. * Delete a given number of old chunks from both the database
  516. * and from object storage.
  517. *
  518. * @param {object} options
  519. * @param {number} [options.batchSize] - number of chunks to delete in each
  520. * batch
  521. * @param {number} [options.maxBatches] - maximum number of batches to process
  522. * @param {number} [options.minAgeSecs] - minimum age of chunks to delete
  523. * @param {number} [options.timeout] - maximum time to spend deleting chunks
  524. *
  525. * @return {Promise<number>} number of chunks deleted
  526. */
  527. async function deleteOldChunks(options = {}) {
  528. const batchSize = options.batchSize ?? DEFAULT_DELETE_BATCH_SIZE
  529. const maxBatches = options.maxBatches ?? Number.MAX_SAFE_INTEGER
  530. const minAgeSecs = options.minAgeSecs ?? DEFAULT_DELETE_MIN_AGE_SECS
  531. const timeout = options.timeout ?? DEFAULT_DELETE_TIMEOUT_SECS
  532. assert.greater(batchSize, 0)
  533. assert.greater(timeout, 0)
  534. assert.greater(maxBatches, 0)
  535. assert.greaterOrEqual(minAgeSecs, 0)
  536. const timeoutAfter = Date.now() + timeout * 1000
  537. let deletedChunksTotal = 0
  538. for (const backend of [postgresBackend, mongoBackend]) {
  539. for (let i = 0; i < maxBatches; i++) {
  540. if (Date.now() > timeoutAfter) {
  541. break
  542. }
  543. const deletedChunks = await deleteOldChunksBatch(
  544. backend,
  545. batchSize,
  546. minAgeSecs
  547. )
  548. deletedChunksTotal += deletedChunks.length
  549. if (deletedChunks.length !== batchSize) {
  550. // Last batch was incomplete. There probably are no old chunks left
  551. break
  552. }
  553. }
  554. }
  555. return deletedChunksTotal
  556. }
  557. async function deleteOldChunksBatch(backend, count, minAgeSecs) {
  558. assert.greater(count, 0, 'bad count')
  559. assert.greaterOrEqual(minAgeSecs, 0, 'bad minAgeSecs')
  560. const oldChunks = await backend.getOldChunksBatch(count, minAgeSecs)
  561. if (oldChunks.length === 0) {
  562. return []
  563. }
  564. await historyStore.deleteChunks(oldChunks)
  565. await backend.deleteOldChunks(oldChunks.map(chunk => chunk.chunkId))
  566. return oldChunks
  567. }
  568. /**
  569. * Returns the appropriate backend for the given project id
  570. *
  571. * Numeric ids use the Postgres backend.
  572. * Strings of 24 characters use the Mongo backend.
  573. */
  574. function getBackend(projectId) {
  575. if (assert.POSTGRES_ID_REGEXP.test(projectId)) {
  576. return postgresBackend
  577. } else if (assert.MONGO_ID_REGEXP.test(projectId)) {
  578. return mongoBackend
  579. } else {
  580. throw new OError('bad project id', { projectId })
  581. }
  582. }
  583. /**
  584. * Gets non-persisted changes that could extend a chunk
  585. *
  586. * @param {string} projectId
  587. * @param {number} chunkEndVersion - end version of the chunk to extend
  588. *
  589. * @return {Promise<Change[]>}
  590. */
  591. async function getChunkExtension(projectId, chunkEndVersion) {
  592. try {
  593. const changes = await redisBackend.getNonPersistedChanges(
  594. projectId,
  595. chunkEndVersion
  596. )
  597. return changes
  598. } catch (err) {
  599. if (err instanceof VersionOutOfBoundsError) {
  600. // If we can't extend the chunk, simply return an empty list
  601. return []
  602. } else {
  603. throw err
  604. }
  605. }
  606. }
  607. class AlreadyInitialized extends OError {
  608. constructor(projectId) {
  609. super('Project is already initialized', { projectId })
  610. }
  611. }
  612. module.exports = {
  613. getBackend,
  614. initializeProject,
  615. cloneProject,
  616. loadLatest,
  617. getLatestChunkMetadata,
  618. loadAtVersion,
  619. loadAtTimestamp,
  620. loadByChunkRecord,
  621. create,
  622. update,
  623. destroy,
  624. getChunkIdForVersion,
  625. getChunkMetadataForVersion,
  626. getProjectChunkIds,
  627. getProjectChunks,
  628. getProjectChunksFromVersion,
  629. getChangesSinceVersion,
  630. deleteProjectChunks,
  631. deleteOldChunks,
  632. AlreadyInitialized,
  633. ChunkVersionConflictError,
  634. }