pr_27173.patch 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  2. index 5a590e347a9..3be1c8a5407 100644
  3. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  4. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  5. @@ -1,28 +1,20 @@
  6. // @ts-check
  7. -import Crypto from 'node:crypto'
  8. import Events from 'node:events'
  9. import fs from 'node:fs'
  10. import Path from 'node:path'
  11. import { performance } from 'node:perf_hooks'
  12. import Stream from 'node:stream'
  13. -import zLib from 'node:zlib'
  14. import { setTimeout } from 'node:timers/promises'
  15. -import { Binary, ObjectId } from 'mongodb'
  16. +import { ObjectId } from 'mongodb'
  17. import pLimit from 'p-limit'
  18. import logger from '@overleaf/logger'
  19. import {
  20. batchedUpdate,
  21. objectIdFromInput,
  22. renderObjectId,
  23. - READ_PREFERENCE_SECONDARY,
  24. } from '@overleaf/mongo-utils/batchedUpdate.js'
  25. import OError from '@overleaf/o-error'
  26. -import {
  27. - AlreadyWrittenError,
  28. - NoKEKMatchedError,
  29. - NotFoundError,
  30. -} from '@overleaf/object-persistor/src/Errors.js'
  31. -import { backupPersistor, projectBlobsBucket } from '../lib/backupPersistor.mjs'
  32. +import { NotFoundError } from '@overleaf/object-persistor/src/Errors.js'
  33. import {
  34. BlobStore,
  35. GLOBAL_BLOBS,
  36. @@ -30,9 +22,8 @@ import {
  37. getProjectBlobsBatch,
  38. getStringLengthOfFile,
  39. makeBlobForFile,
  40. - makeProjectKey,
  41. } from '../lib/blob_store/index.js'
  42. -import { backedUpBlobs as backedUpBlobsCollection, db } from '../lib/mongodb.js'
  43. +import { db } from '../lib/mongodb.js'
  44. import commandLineArgs from 'command-line-args'
  45. import readline from 'node:readline'
  46. @@ -88,7 +79,7 @@ ObjectId.cacheHexString = true
  47. */
  48. /**
  49. - * @return {{PROJECT_IDS_FROM: string, PROCESS_HASHED_FILES: boolean, LOGGING_IDENTIFIER: string, BATCH_RANGE_START: string, PROCESS_BLOBS: boolean, BATCH_RANGE_END: string, PROCESS_NON_DELETED_PROJECTS: boolean, PROCESS_DELETED_PROJECTS: boolean, COLLECT_BACKED_UP_BLOBS: boolean}}
  50. + * @return {{PROJECT_IDS_FROM: string, PROCESS_HASHED_FILES: boolean, LOGGING_IDENTIFIER: string, BATCH_RANGE_START: string, PROCESS_BLOBS: boolean, BATCH_RANGE_END: string, PROCESS_NON_DELETED_PROJECTS: boolean, PROCESS_DELETED_PROJECTS: boolean}}
  51. */
  52. function parseArgs() {
  53. const PUBLIC_LAUNCH_DATE = new Date('2012-01-01T00:00:00Z')
  54. @@ -98,7 +89,6 @@ function parseArgs() {
  55. { name: 'processHashedFiles', type: String, defaultValue: 'false' },
  56. { name: 'processBlobs', type: String, defaultValue: 'true' },
  57. { name: 'projectIdsFrom', type: String, defaultValue: '' },
  58. - { name: 'collectBackedUpBlobs', type: String, defaultValue: 'true' },
  59. {
  60. name: 'BATCH_RANGE_START',
  61. type: String,
  62. @@ -130,7 +120,6 @@ function parseArgs() {
  63. PROCESS_DELETED_PROJECTS: boolVal('processDeletedProjects'),
  64. PROCESS_BLOBS: boolVal('processBlobs'),
  65. PROCESS_HASHED_FILES: boolVal('processHashedFiles'),
  66. - COLLECT_BACKED_UP_BLOBS: boolVal('collectBackedUpBlobs'),
  67. BATCH_RANGE_START,
  68. BATCH_RANGE_END,
  69. LOGGING_IDENTIFIER: args['LOGGING_IDENTIFIER'] || BATCH_RANGE_START,
  70. @@ -143,7 +132,6 @@ const {
  71. PROCESS_DELETED_PROJECTS,
  72. PROCESS_BLOBS,
  73. PROCESS_HASHED_FILES,
  74. - COLLECT_BACKED_UP_BLOBS,
  75. BATCH_RANGE_START,
  76. BATCH_RANGE_END,
  77. LOGGING_IDENTIFIER,
  78. @@ -232,7 +220,6 @@ async function processConcurrently(array, fn) {
  79. const STATS = {
  80. projects: 0,
  81. blobs: 0,
  82. - backedUpBlobs: 0,
  83. filesWithHash: 0,
  84. filesWithoutHash: 0,
  85. filesDuplicated: 0,
  86. @@ -246,14 +233,8 @@ const STATS = {
  87. projectHardDeleted: 0,
  88. fileHardDeleted: 0,
  89. mongoUpdates: 0,
  90. - deduplicatedWriteToAWSLocalCount: 0,
  91. - deduplicatedWriteToAWSLocalEgress: 0,
  92. - deduplicatedWriteToAWSRemoteCount: 0,
  93. - deduplicatedWriteToAWSRemoteEgress: 0,
  94. readFromGCSCount: 0,
  95. readFromGCSIngress: 0,
  96. - writeToAWSCount: 0,
  97. - writeToAWSEgress: 0,
  98. writeToGCSCount: 0,
  99. writeToGCSEgress: 0,
  100. }
  101. @@ -275,7 +256,7 @@ function toMiBPerSecond(v, ms) {
  102. /**
  103. * @param {any} stats
  104. * @param {number} ms
  105. - * @return {{writeToAWSThroughputMiBPerSecond: number, readFromGCSThroughputMiBPerSecond: number}}
  106. + * @return {{readFromGCSThroughputMiBPerSecond: number}}
  107. */
  108. function bandwidthStats(stats, ms) {
  109. return {
  110. @@ -283,10 +264,6 @@ function bandwidthStats(stats, ms) {
  111. stats.readFromGCSIngress,
  112. ms
  113. ),
  114. - writeToAWSThroughputMiBPerSecond: toMiBPerSecond(
  115. - stats.writeToAWSEgress,
  116. - ms
  117. - ),
  118. }
  119. }
  120. @@ -382,9 +359,6 @@ async function processFile(entry, filePath) {
  121. throw err // disable retries for not found in filestore bucket case
  122. }
  123. }
  124. - if (err instanceof NoKEKMatchedError) {
  125. - throw err // disable retries when upload to S3 will fail again
  126. - }
  127. STATS.filesRetries++
  128. const {
  129. ctx: { projectId },
  130. @@ -417,32 +391,8 @@ async function processFileOnce(entry, filePath) {
  131. if (entry.blob) {
  132. const { blob } = entry
  133. const hash = blob.getHash()
  134. - if (entry.ctx.hasBackedUpBlob(hash)) {
  135. - STATS.deduplicatedWriteToAWSLocalCount++
  136. - STATS.deduplicatedWriteToAWSLocalEgress += estimateBlobSize(blob)
  137. - return hash
  138. - }
  139. - entry.ctx.recordPendingBlob(hash)
  140. - STATS.readFromGCSCount++
  141. - const src = await blobStore.getStream(hash)
  142. - const dst = fs.createWriteStream(filePath, {
  143. - highWaterMark: STREAM_HIGH_WATER_MARK,
  144. - })
  145. - try {
  146. - await Stream.promises.pipeline(src, dst)
  147. - } finally {
  148. - STATS.readFromGCSIngress += dst.bytesWritten
  149. - }
  150. - await uploadBlobToAWS(entry, blob, filePath)
  151. return hash
  152. }
  153. - if (entry.hash && entry.ctx.hasBackedUpBlob(entry.hash)) {
  154. - STATS.deduplicatedWriteToAWSLocalCount++
  155. - const blob = entry.ctx.getCachedHistoryBlob(entry.hash)
  156. - // blob might not exist on re-run with --PROCESS_BLOBS=false
  157. - if (blob) STATS.deduplicatedWriteToAWSLocalEgress += estimateBlobSize(blob)
  158. - return entry.hash
  159. - }
  160. STATS.readFromGCSCount++
  161. // make a fetch request to filestore itself
  162. @@ -469,16 +419,14 @@ async function processFileOnce(entry, filePath) {
  163. STATS.globalBlobsEgress += estimateBlobSize(blob)
  164. return hash
  165. }
  166. - if (entry.ctx.hasBackedUpBlob(hash)) {
  167. - STATS.deduplicatedWriteToAWSLocalCount++
  168. - STATS.deduplicatedWriteToAWSLocalEgress += estimateBlobSize(blob)
  169. + if (entry.ctx.hasCompletedBlob(hash)) {
  170. return hash
  171. }
  172. entry.ctx.recordPendingBlob(hash)
  173. try {
  174. await uploadBlobToGCS(blobStore, entry, blob, hash, filePath)
  175. - await uploadBlobToAWS(entry, blob, filePath)
  176. + entry.ctx.recordCompletedBlob(hash) // mark upload as completed
  177. } catch (err) {
  178. entry.ctx.recordFailedBlob(hash)
  179. throw err
  180. @@ -515,76 +463,6 @@ async function uploadBlobToGCS(blobStore, entry, blob, hash, filePath) {
  181. const GZ_SUFFIX = '.gz'
  182. -/**
  183. - * @param {QueueEntry} entry
  184. - * @param {Blob} blob
  185. - * @param {string} filePath
  186. - * @return {Promise<void>}
  187. - */
  188. -async function uploadBlobToAWS(entry, blob, filePath) {
  189. - const { historyId } = entry.ctx
  190. - let backupSource
  191. - let contentEncoding
  192. - const md5 = Crypto.createHash('md5')
  193. - let size
  194. - if (blob.getStringLength()) {
  195. - const filePathCompressed = filePath + GZ_SUFFIX
  196. - backupSource = filePathCompressed
  197. - contentEncoding = 'gzip'
  198. - size = 0
  199. - await Stream.promises.pipeline(
  200. - fs.createReadStream(filePath, { highWaterMark: STREAM_HIGH_WATER_MARK }),
  201. - zLib.createGzip(),
  202. - async function* (source) {
  203. - for await (const chunk of source) {
  204. - size += chunk.byteLength
  205. - md5.update(chunk)
  206. - yield chunk
  207. - }
  208. - },
  209. - fs.createWriteStream(filePathCompressed, {
  210. - highWaterMark: STREAM_HIGH_WATER_MARK,
  211. - })
  212. - )
  213. - } else {
  214. - backupSource = filePath
  215. - size = blob.getByteLength()
  216. - await Stream.promises.pipeline(
  217. - fs.createReadStream(filePath, { highWaterMark: STREAM_HIGH_WATER_MARK }),
  218. - md5
  219. - )
  220. - }
  221. - const backendKeyPath = makeProjectKey(historyId, blob.getHash())
  222. - const persistor = await entry.ctx.getCachedPersistor(backendKeyPath)
  223. - try {
  224. - STATS.writeToAWSCount++
  225. - await persistor.sendStream(
  226. - projectBlobsBucket,
  227. - backendKeyPath,
  228. - fs.createReadStream(backupSource, {
  229. - highWaterMark: STREAM_HIGH_WATER_MARK,
  230. - }),
  231. - {
  232. - contentEncoding,
  233. - contentType: 'application/octet-stream',
  234. - contentLength: size,
  235. - sourceMd5: md5.digest('hex'),
  236. - ifNoneMatch: '*', // de-duplicate write (we pay for the request, but avoid egress)
  237. - }
  238. - )
  239. - STATS.writeToAWSEgress += size
  240. - } catch (err) {
  241. - if (err instanceof AlreadyWrittenError) {
  242. - STATS.deduplicatedWriteToAWSRemoteCount++
  243. - STATS.deduplicatedWriteToAWSRemoteEgress += size
  244. - } else {
  245. - STATS.writeToAWSEgress += size
  246. - throw err
  247. - }
  248. - }
  249. - entry.ctx.recordBackedUpBlob(blob.getHash())
  250. -}
  251. -
  252. /**
  253. * @param {Array<QueueEntry>} files
  254. * @return {Promise<void>}
  255. @@ -670,23 +548,18 @@ async function queueNextBatch(batch, prefix = 'rootFolder.0') {
  256. * @return {Promise<void>}
  257. */
  258. async function processBatch(batch, prefix = 'rootFolder.0') {
  259. - const [{ nBlobs, blobs }, { nBackedUpBlobs, backedUpBlobs }] =
  260. - await Promise.all([collectProjectBlobs(batch), collectBackedUpBlobs(batch)])
  261. - const files = Array.from(findFileInBatch(batch, prefix, blobs, backedUpBlobs))
  262. + const { nBlobs, blobs } = await collectProjectBlobs(batch)
  263. + const files = Array.from(findFileInBatch(batch, prefix, blobs))
  264. STATS.projects += batch.length
  265. STATS.blobs += nBlobs
  266. - STATS.backedUpBlobs += nBackedUpBlobs
  267. // GC
  268. batch.length = 0
  269. blobs.clear()
  270. - backedUpBlobs.clear()
  271. // The files are currently ordered by project-id.
  272. // Order them by file-id ASC then blobs ASC to
  273. // - process files before blobs
  274. - // - avoid head-of-line blocking from many project-files waiting on the generation of the projects DEK (round trip to AWS)
  275. - // - bonus: increase chance of de-duplicating write to AWS
  276. files.sort(
  277. /**
  278. * @param {QueueEntry} a
  279. @@ -903,23 +776,15 @@ function* findFiles(ctx, folder, path, isInputLoop = false) {
  280. * @param {Array<Project>} projects
  281. * @param {string} prefix
  282. * @param {Map<string,Array<Blob>>} blobs
  283. - * @param {Map<string,Array<string>>} backedUpBlobs
  284. * @return Generator<QueueEntry>
  285. */
  286. -function* findFileInBatch(projects, prefix, blobs, backedUpBlobs) {
  287. +function* findFileInBatch(projects, prefix, blobs) {
  288. for (const project of projects) {
  289. const projectIdS = project._id.toString()
  290. const historyIdS = project.overleaf.history.id.toString()
  291. const projectBlobs = blobs.get(historyIdS) || []
  292. - const projectBackedUpBlobs = new Set(backedUpBlobs.get(projectIdS) || [])
  293. - const ctx = new ProjectContext(
  294. - project._id,
  295. - historyIdS,
  296. - projectBlobs,
  297. - projectBackedUpBlobs
  298. - )
  299. + const ctx = new ProjectContext(project._id, historyIdS, projectBlobs)
  300. for (const blob of projectBlobs) {
  301. - if (projectBackedUpBlobs.has(blob.getHash())) continue
  302. ctx.remainingQueueEntries++
  303. yield {
  304. ctx,
  305. @@ -951,42 +816,11 @@ async function collectProjectBlobs(batch) {
  306. return await getProjectBlobsBatch(batch.map(p => p.overleaf.history.id))
  307. }
  308. -/**
  309. - * @param {Array<Project>} projects
  310. - * @return {Promise<{nBackedUpBlobs:number,backedUpBlobs:Map<string,Array<string>>}>}
  311. - */
  312. -async function collectBackedUpBlobs(projects) {
  313. - let nBackedUpBlobs = 0
  314. - const backedUpBlobs = new Map()
  315. - if (!COLLECT_BACKED_UP_BLOBS) return { nBackedUpBlobs, backedUpBlobs }
  316. -
  317. - const cursor = backedUpBlobsCollection.find(
  318. - { _id: { $in: projects.map(p => p._id) } },
  319. - {
  320. - readPreference: READ_PREFERENCE_SECONDARY,
  321. - sort: { _id: 1 },
  322. - }
  323. - )
  324. - for await (const record of cursor) {
  325. - const blobs = record.blobs.map(b => b.toString('hex'))
  326. - backedUpBlobs.set(record._id.toString(), blobs)
  327. - nBackedUpBlobs += blobs.length
  328. - }
  329. - return { nBackedUpBlobs, backedUpBlobs }
  330. -}
  331. -
  332. -const BATCH_HASH_WRITES = 1_000
  333. const BATCH_FILE_UPDATES = 100
  334. const MONGO_PATH_SKIP_WRITE_HASH_TO_FILE_TREE = 'skip-write-to-file-tree'
  335. class ProjectContext {
  336. - /** @type {Promise<CachedPerProjectEncryptedS3Persistor> | null} */
  337. - #cachedPersistorPromise = null
  338. -
  339. - /** @type {Set<string>} */
  340. - #backedUpBlobs
  341. -
  342. /** @type {Map<string, Blob>} */
  343. #historyBlobs
  344. @@ -1000,12 +834,10 @@ class ProjectContext {
  345. * @param {ObjectId} projectId
  346. * @param {string} historyId
  347. * @param {Array<Blob>} blobs
  348. - * @param {Set<string>} backedUpBlobs
  349. */
  350. - constructor(projectId, historyId, blobs, backedUpBlobs) {
  351. + constructor(projectId, historyId, blobs) {
  352. this.projectId = projectId
  353. this.historyId = historyId
  354. - this.#backedUpBlobs = backedUpBlobs
  355. this.#historyBlobs = new Map(blobs.map(b => [b.getHash(), b]))
  356. }
  357. @@ -1034,75 +866,17 @@ class ProjectContext {
  358. return false
  359. }
  360. - /**
  361. - * @param {string} key
  362. - * @return {Promise<CachedPerProjectEncryptedS3Persistor>}
  363. - */
  364. - getCachedPersistor(key) {
  365. - if (!this.#cachedPersistorPromise) {
  366. - // Fetch DEK once, but only if needed -- upon the first use
  367. - this.#cachedPersistorPromise = this.#getCachedPersistorWithRetries(key)
  368. - }
  369. - return this.#cachedPersistorPromise
  370. - }
  371. -
  372. - /**
  373. - * @param {string} key
  374. - * @return {Promise<CachedPerProjectEncryptedS3Persistor>}
  375. - */
  376. - async #getCachedPersistorWithRetries(key) {
  377. - // Optimization: Skip GET on DEK in case no blobs are marked as backed up yet.
  378. - let tryGenerateDEKFirst = this.#backedUpBlobs.size === 0
  379. - for (let attempt = 0; attempt < RETRIES; attempt++) {
  380. - try {
  381. - if (tryGenerateDEKFirst) {
  382. - try {
  383. - return await backupPersistor.generateDataEncryptionKey(
  384. - projectBlobsBucket,
  385. - key
  386. - )
  387. - } catch (err) {
  388. - if (err instanceof AlreadyWrittenError) {
  389. - tryGenerateDEKFirst = false
  390. - // fall back to GET below
  391. - } else {
  392. - throw err
  393. - }
  394. - }
  395. - }
  396. - return await backupPersistor.forProject(projectBlobsBucket, key)
  397. - } catch (err) {
  398. - if (gracefulShutdownInitiated) throw err
  399. - if (err instanceof NoKEKMatchedError) {
  400. - throw err
  401. - } else {
  402. - logger.warn(
  403. - { err, projectId: this.projectId, attempt },
  404. - 'failed to get DEK, trying again'
  405. - )
  406. - const jitter = Math.random() * RETRY_DELAY_MS
  407. - await setTimeout(RETRY_DELAY_MS + jitter)
  408. - }
  409. - }
  410. - }
  411. - return await backupPersistor.forProject(projectBlobsBucket, key)
  412. - }
  413. -
  414. async flushMongoQueuesIfNeeded() {
  415. if (this.remainingQueueEntries === 0) {
  416. await this.flushMongoQueues()
  417. }
  418. - if (this.#completedBlobs.size > BATCH_HASH_WRITES) {
  419. - await this.#storeBackedUpBlobs()
  420. - }
  421. if (this.#pendingFileWrites.length > BATCH_FILE_UPDATES) {
  422. await this.#storeFileHashes()
  423. }
  424. }
  425. async flushMongoQueues() {
  426. - await this.#storeBackedUpBlobs()
  427. await this.#storeFileHashes()
  428. }
  429. @@ -1111,20 +885,6 @@ class ProjectContext {
  430. /** @type {Set<string>} */
  431. #completedBlobs = new Set()
  432. - async #storeBackedUpBlobs() {
  433. - if (this.#completedBlobs.size === 0) return
  434. - const blobs = Array.from(this.#completedBlobs).map(
  435. - hash => new Binary(Buffer.from(hash, 'hex'))
  436. - )
  437. - this.#completedBlobs.clear()
  438. - STATS.mongoUpdates++
  439. - await backedUpBlobsCollection.updateOne(
  440. - { _id: this.projectId },
  441. - { $addToSet: { blobs: { $each: blobs } } },
  442. - { upsert: true }
  443. - )
  444. - }
  445. -
  446. /**
  447. * @param {string} hash
  448. */
  449. @@ -1142,8 +902,7 @@ class ProjectContext {
  450. /**
  451. * @param {string} hash
  452. */
  453. - recordBackedUpBlob(hash) {
  454. - this.#backedUpBlobs.add(hash)
  455. + recordCompletedBlob(hash) {
  456. this.#completedBlobs.add(hash)
  457. this.#pendingBlobs.delete(hash)
  458. }
  459. @@ -1152,12 +911,8 @@ class ProjectContext {
  460. * @param {string} hash
  461. * @return {boolean}
  462. */
  463. - hasBackedUpBlob(hash) {
  464. - return (
  465. - this.#pendingBlobs.has(hash) ||
  466. - this.#completedBlobs.has(hash) ||
  467. - this.#backedUpBlobs.has(hash)
  468. - )
  469. + hasCompletedBlob(hash) {
  470. + return this.#pendingBlobs.has(hash) || this.#completedBlobs.has(hash)
  471. }
  472. /** @type {Array<QueueEntry>} */
  473. diff --git a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  474. index 8f861d39345..62b0b1de25f 100644
  475. --- a/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  476. +++ b/services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs
  477. @@ -4,23 +4,17 @@ import Stream from 'node:stream'
  478. import { setTimeout } from 'node:timers/promises'
  479. import { promisify } from 'node:util'
  480. import { ObjectId, Binary } from 'mongodb'
  481. -import {
  482. - db,
  483. - backedUpBlobs,
  484. - globalBlobs,
  485. -} from '../../../../storage/lib/mongodb.js'
  486. +import { db, globalBlobs } from '../../../../storage/lib/mongodb.js'
  487. import cleanup from './support/cleanup.js'
  488. import testProjects from '../api/support/test_projects.js'
  489. import { execFile } from 'node:child_process'
  490. import chai, { expect } from 'chai'
  491. import chaiExclude from 'chai-exclude'
  492. -import config from 'config'
  493. import { WritableBuffer } from '@overleaf/stream-utils'
  494. import {
  495. backupPersistor,
  496. projectBlobsBucket,
  497. } from '../../../../storage/lib/backupPersistor.mjs'
  498. -import projectKey from '../../../../storage/lib/project_key.js'
  499. import {
  500. BlobStore,
  501. makeProjectKey,
  502. @@ -31,9 +25,6 @@ import express from 'express'
  503. chai.use(chaiExclude)
  504. const TIMEOUT = 20 * 1_000
  505. -const { deksBucket } = config.get('backupStore')
  506. -const { tieringStorageClass } = config.get('backupPersistor')
  507. -
  508. const projectsCollection = db.collection('projects')
  509. const deletedProjectsCollection = db.collection('deletedProjects')
  510. @@ -117,17 +108,6 @@ function binaryForGitBlobHash(gitBlobHash) {
  511. return new Binary(Buffer.from(gitBlobHash, 'hex'))
  512. }
  513. -async function listS3Bucket(bucket, wantStorageClass) {
  514. - const client = backupPersistor._getClientForBucket(bucket)
  515. - const response = await client.listObjectsV2({ Bucket: bucket }).promise()
  516. -
  517. - for (const object of response.Contents || []) {
  518. - expect(object).to.have.property('StorageClass', wantStorageClass)
  519. - }
  520. -
  521. - return (response.Contents || []).map(item => item.Key || '')
  522. -}
  523. -
  524. function objectIdFromTime(timestamp) {
  525. return ObjectId.createFromTime(new Date(timestamp).getTime() / 1000)
  526. }
  527. @@ -591,11 +571,7 @@ describe('back_fill_file_hash script', function () {
  528. expect((await fs.promises.readdir('/tmp')).join(';')).to.not.match(
  529. /back_fill_file_hash/
  530. )
  531. - const extraStatsKeys = [
  532. - 'eventLoop',
  533. - 'readFromGCSThroughputMiBPerSecond',
  534. - 'writeToAWSThroughputMiBPerSecond',
  535. - ]
  536. + const extraStatsKeys = ['eventLoop', 'readFromGCSThroughputMiBPerSecond']
  537. const stats = JSON.parse(
  538. result.stderr
  539. .split('\n')
  540. @@ -610,7 +586,6 @@ describe('back_fill_file_hash script', function () {
  541. delete stats.time
  542. if (shouldHaveWritten) {
  543. expect(stats.readFromGCSThroughputMiBPerSecond).to.be.greaterThan(0)
  544. - expect(stats.writeToAWSThroughputMiBPerSecond).to.be.greaterThan(0)
  545. }
  546. for (const key of extraStatsKeys) {
  547. delete stats[key]
  548. @@ -856,109 +831,6 @@ describe('back_fill_file_hash script', function () {
  549. },
  550. },
  551. ])
  552. - expect(
  553. - (await backedUpBlobs.find({}, { sort: { _id: 1 } }).toArray()).map(
  554. - entry => {
  555. - // blobs are pushed unordered into mongo. Sort the list for consistency.
  556. - entry.blobs.sort()
  557. - return entry
  558. - }
  559. - )
  560. - ).to.deep.equal([
  561. - {
  562. - _id: projectId0,
  563. - blobs: [
  564. - binaryForGitBlobHash(gitBlobHash(fileId0)),
  565. - binaryForGitBlobHash(hashFile7),
  566. - binaryForGitBlobHash(hashTextBlob0),
  567. - ].sort(),
  568. - },
  569. - {
  570. - _id: projectId1,
  571. - blobs: [
  572. - binaryForGitBlobHash(gitBlobHash(fileId1)),
  573. - binaryForGitBlobHash(hashTextBlob1),
  574. - ].sort(),
  575. - },
  576. - {
  577. - _id: projectId2,
  578. - blobs: [binaryForGitBlobHash(hashTextBlob2)]
  579. - .concat(
  580. - processHashedFiles
  581. - ? [binaryForGitBlobHash(gitBlobHash(fileId2))]
  582. - : []
  583. - )
  584. - .sort(),
  585. - },
  586. - {
  587. - _id: projectIdDeleted0,
  588. - blobs: [binaryForGitBlobHash(gitBlobHash(fileId4))].sort(),
  589. - },
  590. - {
  591. - _id: projectId3,
  592. - blobs: [binaryForGitBlobHash(gitBlobHash(fileId3))].sort(),
  593. - },
  594. - ...(processHashedFiles
  595. - ? [
  596. - {
  597. - _id: projectIdDeleted1,
  598. - blobs: [binaryForGitBlobHash(gitBlobHash(fileId5))].sort(),
  599. - },
  600. - ]
  601. - : []),
  602. - {
  603. - _id: projectIdBadFileTree0,
  604. - blobs: [binaryForGitBlobHash(hashTextBlob3)].sort(),
  605. - },
  606. - {
  607. - _id: projectIdBadFileTree3,
  608. - blobs: [binaryForGitBlobHash(gitBlobHash(fileId9))].sort(),
  609. - },
  610. - ])
  611. - })
  612. - it('should have backed up all the files', async function () {
  613. - expect(tieringStorageClass).to.exist
  614. - const blobs = await listS3Bucket(projectBlobsBucket, tieringStorageClass)
  615. - expect(blobs.sort()).to.deep.equal(
  616. - Array.from(
  617. - new Set(
  618. - writtenBlobs
  619. - .map(({ historyId, fileId, hash }) =>
  620. - makeProjectKey(historyId, hash || gitBlobHash(fileId))
  621. - )
  622. - .sort()
  623. - )
  624. - )
  625. - )
  626. - for (let { historyId, fileId, hash, content } of writtenBlobs) {
  627. - hash = hash || gitBlobHash(fileId.toString())
  628. - const s = await backupPersistor.getObjectStream(
  629. - projectBlobsBucket,
  630. - makeProjectKey(historyId, hash),
  631. - { autoGunzip: true }
  632. - )
  633. - const buf = new WritableBuffer()
  634. - await Stream.promises.pipeline(s, buf)
  635. - expect(gitBlobHashBuffer(buf.getContents())).to.equal(hash)
  636. - if (content) {
  637. - expect(buf.getContents()).to.deep.equal(content)
  638. - } else {
  639. - const id = buf.getContents().toString('utf-8')
  640. - expect(id).to.equal(fileId.toString())
  641. - // double check we are not comparing 'undefined' or '[object Object]' above
  642. - expect(id).to.match(/^[a-f0-9]{24}$/)
  643. - }
  644. - }
  645. - const deks = await listS3Bucket(deksBucket, 'STANDARD')
  646. - expect(deks.sort()).to.deep.equal(
  647. - Array.from(
  648. - new Set(
  649. - writtenBlobs.map(
  650. - ({ historyId }) => projectKey.format(historyId) + '/dek'
  651. - )
  652. - )
  653. - ).sort()
  654. - )
  655. })
  656. it('should have written the back filled files to history v1', async function () {
  657. for (const { historyId, hash, fileId, content } of writtenBlobs) {
  658. @@ -991,14 +863,13 @@ describe('back_fill_file_hash script', function () {
  659. // We still need to iterate over all the projects and blobs.
  660. projects: 10,
  661. blobs: 10,
  662. - backedUpBlobs: 10,
  663. +
  664. badFileTrees: 4,
  665. }
  666. if (processHashedFiles) {
  667. stats = sumStats(stats, {
  668. ...STATS_ALL_ZERO,
  669. blobs: 2,
  670. - backedUpBlobs: 2,
  671. })
  672. }
  673. expect(rerun.stats).deep.equal(stats)
  674. @@ -1024,7 +895,6 @@ describe('back_fill_file_hash script', function () {
  675. const STATS_ALL_ZERO = {
  676. projects: 0,
  677. blobs: 0,
  678. - backedUpBlobs: 0,
  679. filesWithHash: 0,
  680. filesWithoutHash: 0,
  681. filesDuplicated: 0,
  682. @@ -1038,21 +908,14 @@ describe('back_fill_file_hash script', function () {
  683. fileHardDeleted: 0,
  684. badFileTrees: 0,
  685. mongoUpdates: 0,
  686. - deduplicatedWriteToAWSLocalCount: 0,
  687. - deduplicatedWriteToAWSLocalEgress: 0,
  688. - deduplicatedWriteToAWSRemoteCount: 0,
  689. - deduplicatedWriteToAWSRemoteEgress: 0,
  690. readFromGCSCount: 0,
  691. readFromGCSIngress: 0,
  692. - writeToAWSCount: 0,
  693. - writeToAWSEgress: 0,
  694. writeToGCSCount: 0,
  695. writeToGCSEgress: 0,
  696. }
  697. const STATS_UP_TO_PROJECT1 = {
  698. projects: 2,
  699. blobs: 2,
  700. - backedUpBlobs: 0,
  701. filesWithHash: 0,
  702. filesWithoutHash: 5,
  703. filesDuplicated: 1,
  704. @@ -1065,22 +928,15 @@ describe('back_fill_file_hash script', function () {
  705. projectHardDeleted: 0,
  706. fileHardDeleted: 0,
  707. badFileTrees: 0,
  708. - mongoUpdates: 4,
  709. - deduplicatedWriteToAWSLocalCount: 0,
  710. - deduplicatedWriteToAWSLocalEgress: 0,
  711. - deduplicatedWriteToAWSRemoteCount: 0,
  712. - deduplicatedWriteToAWSRemoteEgress: 0,
  713. - readFromGCSCount: 6,
  714. - readFromGCSIngress: 4000086,
  715. - writeToAWSCount: 5,
  716. - writeToAWSEgress: 4026,
  717. + mongoUpdates: 2, // 4-2 blobs written to backedUpBlobs collection
  718. + readFromGCSCount: 4,
  719. + readFromGCSIngress: 4000072,
  720. writeToGCSCount: 3,
  721. writeToGCSEgress: 4000048,
  722. }
  723. const STATS_UP_FROM_PROJECT1_ONWARD = {
  724. projects: 8,
  725. blobs: 2,
  726. - backedUpBlobs: 0,
  727. filesWithHash: 0,
  728. filesWithoutHash: 4,
  729. filesDuplicated: 0,
  730. @@ -1093,26 +949,18 @@ describe('back_fill_file_hash script', function () {
  731. projectHardDeleted: 0,
  732. fileHardDeleted: 0,
  733. badFileTrees: 4,
  734. - mongoUpdates: 8,
  735. - deduplicatedWriteToAWSLocalCount: 1,
  736. - deduplicatedWriteToAWSLocalEgress: 30,
  737. - deduplicatedWriteToAWSRemoteCount: 0,
  738. - deduplicatedWriteToAWSRemoteEgress: 0,
  739. - readFromGCSCount: 6,
  740. - readFromGCSIngress: 110,
  741. - writeToAWSCount: 5,
  742. - writeToAWSEgress: 143,
  743. + mongoUpdates: 3, // previously 5 blobs written to backedUpBlobs collection
  744. + readFromGCSCount: 4,
  745. + readFromGCSIngress: 96,
  746. writeToGCSCount: 3,
  747. writeToGCSEgress: 72,
  748. }
  749. const STATS_FILES_HASHED_EXTRA = {
  750. ...STATS_ALL_ZERO,
  751. filesWithHash: 2,
  752. - mongoUpdates: 2,
  753. + mongoUpdates: 0, // previously 2 blobs written to backedUpBlobs collection
  754. readFromGCSCount: 2,
  755. readFromGCSIngress: 48,
  756. - writeToAWSCount: 2,
  757. - writeToAWSEgress: 60,
  758. writeToGCSCount: 2,
  759. writeToGCSEgress: 48,
  760. }
  761. @@ -1144,8 +992,6 @@ describe('back_fill_file_hash script', function () {
  762. ...STATS_ALL_ZERO,
  763. filesFailed: 1,
  764. readFromGCSIngress: -24,
  765. - writeToAWSCount: -1,
  766. - writeToAWSEgress: -28,
  767. writeToGCSCount: -1,
  768. writeToGCSEgress: -24,
  769. })
  770. @@ -1269,13 +1115,14 @@ describe('back_fill_file_hash script', function () {
  771. before('run script with hashed files', async function () {
  772. output2 = await runScript(['--processHashedFiles=true'], {})
  773. })
  774. - it('should print stats', function () {
  775. + it('should print stats for the first run without hashed files', function () {
  776. expect(output1.stats).deep.equal(STATS_ALL)
  777. + })
  778. + it('should print stats for the hashed files run', function () {
  779. expect(output2.stats).deep.equal({
  780. ...STATS_FILES_HASHED_EXTRA,
  781. projects: 10,
  782. blobs: 10,
  783. - backedUpBlobs: 10,
  784. badFileTrees: 4,
  785. })
  786. })
  787. @@ -1322,9 +1169,7 @@ describe('back_fill_file_hash script', function () {
  788. ...STATS_FILES_HASHED_EXTRA,
  789. readFromGCSCount: 3,
  790. readFromGCSIngress: 72,
  791. - deduplicatedWriteToAWSLocalCount: 1,
  792. - deduplicatedWriteToAWSLocalEgress: 30,
  793. - mongoUpdates: 1,
  794. + mongoUpdates: 0,
  795. filesWithHash: 3,
  796. })
  797. )
  798. @@ -1354,48 +1199,6 @@ describe('back_fill_file_hash script', function () {
  799. expect(output.stats).deep.equal(
  800. sumStats(STATS_ALL, {
  801. ...STATS_ALL_ZERO,
  802. - // one remote deduplicate
  803. - deduplicatedWriteToAWSRemoteCount: 1,
  804. - deduplicatedWriteToAWSRemoteEgress: 28,
  805. - writeToAWSEgress: -28, // subtract skipped egress
  806. - })
  807. - )
  808. - })
  809. - commonAssertions()
  810. - })
  811. -
  812. - describe('with something in the bucket and marked as processed', function () {
  813. - before('prepare environment', prepareEnvironment)
  814. - before('create a file in s3', async function () {
  815. - await backupPersistor.sendStream(
  816. - projectBlobsBucket,
  817. - makeProjectKey(historyId0, hashTextBlob0),
  818. - Stream.Readable.from([contentTextBlob0]),
  819. - { contentLength: contentTextBlob0.byteLength }
  820. - )
  821. - await backedUpBlobs.insertMany([
  822. - {
  823. - _id: projectId0,
  824. - blobs: [binaryForGitBlobHash(hashTextBlob0)],
  825. - },
  826. - ])
  827. - })
  828. - let output
  829. - before('run script', async function () {
  830. - output = await runScript([], {
  831. - CONCURRENCY: '1',
  832. - })
  833. - })
  834. -
  835. - it('should print stats', function () {
  836. - expect(output.stats).deep.equal(
  837. - sumStats(STATS_ALL, {
  838. - ...STATS_ALL_ZERO,
  839. - backedUpBlobs: 1,
  840. - writeToAWSCount: -1,
  841. - writeToAWSEgress: -27,
  842. - readFromGCSCount: -1,
  843. - readFromGCSIngress: -7,
  844. })
  845. )
  846. })
  847. @@ -1418,8 +1221,10 @@ describe('back_fill_file_hash script', function () {
  848. })
  849. })
  850. - it('should print stats', function () {
  851. + it('should print stats for part 0', function () {
  852. expect(outputPart0.stats).to.deep.equal(STATS_UP_TO_PROJECT1)
  853. + })
  854. + it('should print stats for part 1', function () {
  855. expect(outputPart1.stats).to.deep.equal(STATS_UP_FROM_PROJECT1_ONWARD)
  856. })
  857. commonAssertions()
  858. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  859. index 3be1c8a5407..c9ed13c6cb4 100644
  860. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  861. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  862. @@ -388,12 +388,6 @@ async function processFileOnce(entry, filePath) {
  863. fileId,
  864. } = entry
  865. const blobStore = new BlobStore(historyId)
  866. - if (entry.blob) {
  867. - const { blob } = entry
  868. - const hash = blob.getHash()
  869. - return hash
  870. - }
  871. -
  872. STATS.readFromGCSCount++
  873. // make a fetch request to filestore itself
  874. const src = await fetchFromFilestore(projectId, fileId)
  875. @@ -784,16 +778,6 @@ function* findFileInBatch(projects, prefix, blobs) {
  876. const historyIdS = project.overleaf.history.id.toString()
  877. const projectBlobs = blobs.get(historyIdS) || []
  878. const ctx = new ProjectContext(project._id, historyIdS, projectBlobs)
  879. - for (const blob of projectBlobs) {
  880. - ctx.remainingQueueEntries++
  881. - yield {
  882. - ctx,
  883. - cacheKey: blob.getHash(),
  884. - path: MONGO_PATH_SKIP_WRITE_HASH_TO_FILE_TREE,
  885. - blob,
  886. - hash: blob.getHash(),
  887. - }
  888. - }
  889. try {
  890. yield* findFiles(ctx, project.rootFolder?.[0], prefix, true)
  891. } catch (err) {
  892. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  893. index c9ed13c6cb4..f24ce4a6605 100644
  894. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  895. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  896. @@ -387,6 +387,13 @@ async function processFileOnce(entry, filePath) {
  897. ctx: { projectId, historyId },
  898. fileId,
  899. } = entry
  900. + if (entry.hash && entry.ctx.hasCompletedBlob(entry.hash)) {
  901. + // We can enter this case for two identical files in the same project,
  902. + // one with hash, the other without. When the one without hash gets
  903. + // processed first, we can skip downloading the other one we already
  904. + // know the hash of.
  905. + return entry.hash
  906. + }
  907. const blobStore = new BlobStore(historyId)
  908. STATS.readFromGCSCount++
  909. // make a fetch request to filestore itself
  910. diff --git a/services/history-v1/storage/scripts/back_fill_file_hash.mjs b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  911. index f24ce4a6605..0ccadaf5a95 100644
  912. --- a/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  913. +++ b/services/history-v1/storage/scripts/back_fill_file_hash.mjs
  914. @@ -559,8 +559,9 @@ async function processBatch(batch, prefix = 'rootFolder.0') {
  915. blobs.clear()
  916. // The files are currently ordered by project-id.
  917. - // Order them by file-id ASC then blobs ASC to
  918. - // - process files before blobs
  919. + // Order them by file-id ASC then hash ASC to
  920. + // increase the hit rate on the "already processed
  921. + // hash for project" checks.
  922. files.sort(
  923. /**
  924. * @param {QueueEntry} a